mirror of
https://github.com/kremalicious/location.git
synced 2024-11-23 18:41:36 +01:00
add Foursquare/Swarm checkins
- expose last checkin in API response - script to manually fetch all checkins and write out into file
This commit is contained in:
parent
73a95344b9
commit
a4e07fc7fb
@ -1,2 +1,3 @@
|
||||
NOMADLIST_PROFILE=xxx
|
||||
NOMADLIST_KEY=xxx
|
||||
FOURSQUARE_KEY=xxx
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules
|
||||
.DS_Store
|
||||
.vercel
|
||||
.env
|
||||
checkins.json
|
18
README.md
18
README.md
@ -6,11 +6,12 @@
|
||||
|
||||
- [🏄 Usage](#-usage)
|
||||
- [⬆️ Deployment](#️-deployment)
|
||||
- [Development](#development)
|
||||
- [🏛 License](#-license)
|
||||
|
||||
## 🏄 Usage
|
||||
|
||||
Location is currently fetched from my (private) nomadlist.com profile, making sure the API key is hidden from any browser, and only the relevant location data is exposed instead of the whole profile data response.
|
||||
Location is currently fetched from my (private) nomadlist.com profile & Foursquare/Swarm check-ins, making sure any API keys are hidden from any browser, and only the relevant location data is exposed.
|
||||
|
||||
```text
|
||||
https://location.kremalicious.com
|
||||
@ -22,6 +23,21 @@ Used to display location on my [portfolio](https://matthiaskretschmann.com) & [b
|
||||
|
||||
Every branch or Pull Request is automatically deployed by [Vercel](https://vercel.com) with their GitHub integration. A link to a deployment will appear under each Pull Request.
|
||||
|
||||
## Development
|
||||
|
||||
Requires env vars:
|
||||
|
||||
- `NOMADLIST_PROFILE`
|
||||
- `NOMADLIST_KEY`
|
||||
- `FOURSQUARE_KEY`
|
||||
|
||||
```bash
|
||||
npm start
|
||||
|
||||
# fetches all Foursquare/Swarm checkins and writes them out to checkins.json
|
||||
npm run get-checkins
|
||||
```
|
||||
|
||||
## 🏛 License
|
||||
|
||||
```text
|
||||
|
69
api/index.ts
69
api/index.ts
@ -1,64 +1,33 @@
|
||||
interface NomadListLocation {
|
||||
city: string
|
||||
country: string
|
||||
country_code: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
epoch_start: number
|
||||
epoch_end: number
|
||||
date_start: string
|
||||
date_end: string
|
||||
place_photo: string
|
||||
}
|
||||
|
||||
interface NomadListLocationResponse {
|
||||
location: {
|
||||
now: NomadListLocation
|
||||
previous: NomadListLocation
|
||||
next: NomadListLocation
|
||||
}
|
||||
}
|
||||
import { getLastCheckin } from '../lib/foursquare'
|
||||
import { NomadListLocation, getNomadList } from '../lib/nomadlist'
|
||||
|
||||
export const config = {
|
||||
runtime: 'experimental-edge'
|
||||
}
|
||||
|
||||
function removeUnwantedKeys(location: NomadListLocation) {
|
||||
const { place_photo, latitude, longitude, epoch_start, epoch_end, ...rest } =
|
||||
location
|
||||
return rest
|
||||
interface Location extends NomadListLocation {
|
||||
lastCheckin?: string
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
declare type LocationResponse = {
|
||||
now: Location
|
||||
next: Location
|
||||
}
|
||||
|
||||
export default async function handler() {
|
||||
try {
|
||||
if (!process.env.NOMADLIST_PROFILE) {
|
||||
throw new Error('Missing NOMADLIST_PROFILE env variable')
|
||||
}
|
||||
if (!process.env.NOMADLIST_KEY) {
|
||||
throw new Error('Missing NOMADLIST_KEY env variable')
|
||||
}
|
||||
const nomadlist = await getNomadList()
|
||||
const foursquare = await getLastCheckin()
|
||||
|
||||
const response = await fetch(
|
||||
`https://nomadlist.com/@${process.env.NOMADLIST_PROFILE}.json?key=${process.env.NOMADLIST_KEY}`
|
||||
)
|
||||
if (!response || !response.ok || response.status !== 200) {
|
||||
throw new Error("Couldn't fetch data from NomadList")
|
||||
}
|
||||
const json = (await response.json()) as NomadListLocationResponse
|
||||
const response = {
|
||||
now: { ...nomadlist.now, ...(foursquare && { lastCheckin: foursquare }) },
|
||||
next: { ...nomadlist.next }
|
||||
} as LocationResponse
|
||||
|
||||
// return only parts of the data
|
||||
const final = {
|
||||
now: removeUnwantedKeys(json.location.now),
|
||||
next: removeUnwantedKeys(json.location.next)
|
||||
}
|
||||
return new Response(JSON.stringify(final), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 's-maxage=60, stale-while-revalidate'
|
||||
}
|
||||
return new Response(JSON.stringify(response, null, 2), {
|
||||
headers: { 'content-type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify(error), { status: 500 })
|
||||
return new Response(JSON.stringify(error, null, 2), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
28
lib/foursquare.ts
Normal file
28
lib/foursquare.ts
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// Get last checkin from foursquare
|
||||
//
|
||||
const url = `https://api.foursquare.com/v2/users/self/checkins?oauth_token=${process.env.FOURSQUARE_KEY}&v=20221201&limit=1`
|
||||
|
||||
export async function getLastCheckin() {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
if (!json || json?.meta?.code !== 200)
|
||||
throw new Error(json?.meta?.errorDetail)
|
||||
|
||||
const checkin = json?.response?.checkins?.items?.[0]
|
||||
|
||||
return checkin
|
||||
? {
|
||||
// convert date from UNIX timestamp to JS Date
|
||||
date: new Date(checkin.createdAt * 1000),
|
||||
venue: {
|
||||
name: checkin.venue.name,
|
||||
location: checkin.venue.location
|
||||
}
|
||||
}
|
||||
: null
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching data:', error.message)
|
||||
}
|
||||
}
|
49
lib/nomadlist.ts
Normal file
49
lib/nomadlist.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export interface NomadListLocation {
|
||||
city: string
|
||||
country: string
|
||||
country_code: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
epoch_start: number
|
||||
epoch_end: number
|
||||
date_start: string
|
||||
date_end: string
|
||||
place_photo: string
|
||||
}
|
||||
|
||||
export interface NomadListLocationResponse {
|
||||
location: {
|
||||
now: NomadListLocation
|
||||
previous: NomadListLocation
|
||||
next: NomadListLocation
|
||||
}
|
||||
}
|
||||
|
||||
function removeUnwantedKeys(location: NomadListLocation) {
|
||||
const { place_photo, latitude, longitude, epoch_start, epoch_end, ...rest } =
|
||||
location
|
||||
return rest
|
||||
}
|
||||
|
||||
export async function getNomadList() {
|
||||
if (!process.env.NOMADLIST_PROFILE) {
|
||||
throw new Error('Missing NOMADLIST_PROFILE env variable')
|
||||
}
|
||||
if (!process.env.NOMADLIST_KEY) {
|
||||
throw new Error('Missing NOMADLIST_KEY env variable')
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://nomadlist.com/@${process.env.NOMADLIST_PROFILE}.json?key=${process.env.NOMADLIST_KEY}`
|
||||
)
|
||||
if (!response || !response.ok || response.status !== 200) {
|
||||
throw new Error("Couldn't fetch data from NomadList")
|
||||
}
|
||||
const json = (await response.json()) as NomadListLocationResponse
|
||||
|
||||
// return only parts of the data
|
||||
return {
|
||||
now: removeUnwantedKeys(json?.location?.now),
|
||||
next: removeUnwantedKeys(json?.location?.next)
|
||||
}
|
||||
}
|
3967
package-lock.json
generated
3967
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -8,18 +8,23 @@
|
||||
"start": "vercel dev",
|
||||
"test": "npm run type-check",
|
||||
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"get-checkins": "node ./scripts/get-checkins.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"prettier": "^2.8.3",
|
||||
"typescript": "^4.9.4"
|
||||
"@types/node": "^20.4.9",
|
||||
"@vercel/node": "^2.15.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.46.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.0.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kremalicious/location"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
"node": "18"
|
||||
}
|
||||
}
|
||||
|
53
scripts/get-checkins.mjs
Normal file
53
scripts/get-checkins.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
//
|
||||
// Get all checkins from Foursquare API and save to checkins.json.
|
||||
// Paginated requests are made until no more checkins are returned.
|
||||
//
|
||||
import { writeFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const LIMIT = 250
|
||||
const checkins = []
|
||||
|
||||
const start = async (offset = 0) => {
|
||||
console.log('Requesting checkins at offset: ' + offset)
|
||||
const url = `https://api.foursquare.com/v2/users/self/checkins?oauth_token=${process.env.FOURSQUARE_KEY}&limit=${LIMIT}&offset=${offset}&v=20221201&m=swarm`
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
if (!json || json?.meta?.code !== 200)
|
||||
throw new Error(json?.meta?.errorDetail)
|
||||
|
||||
const { items } = json.response.checkins
|
||||
|
||||
if (!items || !items.length) {
|
||||
console.log('No more items.')
|
||||
const FILE = resolve(__dirname, '../checkins.json')
|
||||
console.log('DONE: writing file ' + FILE)
|
||||
writeFileSync(FILE, JSON.stringify(checkins, null, '\t'))
|
||||
return
|
||||
}
|
||||
|
||||
const firstCreatedAt = items[0].createdAt
|
||||
const date = new Date(firstCreatedAt * 1000)
|
||||
console.log(`Batch #${offset}: ${date.toDateString()}`)
|
||||
|
||||
items.forEach((item, i) => {
|
||||
try {
|
||||
checkins.push(item)
|
||||
} catch (e) {
|
||||
console.error(item)
|
||||
}
|
||||
})
|
||||
|
||||
start(offset + LIMIT)
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
start()
|
Loading…
Reference in New Issue
Block a user