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:
Matthias Kretschmann 2023-08-09 21:36:47 +01:00
parent 73a95344b9
commit a4e07fc7fb
Signed by: m
GPG Key ID: 606EEEF3C479A91F
9 changed files with 4116 additions and 89 deletions

View File

@ -1,2 +1,3 @@
NOMADLIST_PROFILE=xxx NOMADLIST_PROFILE=xxx
NOMADLIST_KEY=xxx NOMADLIST_KEY=xxx
FOURSQUARE_KEY=xxx

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules node_modules
.DS_Store .DS_Store
.vercel .vercel
.env .env
checkins.json

View File

@ -6,11 +6,12 @@
- [🏄 Usage](#-usage) - [🏄 Usage](#-usage)
- [⬆️ Deployment](#-deployment) - [⬆️ Deployment](#-deployment)
- [Development](#development)
- [🏛 License](#-license) - [🏛 License](#-license)
## 🏄 Usage ## 🏄 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 ```text
https://location.kremalicious.com 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. 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 ## 🏛 License
```text ```text

View File

@ -1,64 +1,33 @@
interface NomadListLocation { import { getLastCheckin } from '../lib/foursquare'
city: string import { NomadListLocation, getNomadList } from '../lib/nomadlist'
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
}
}
export const config = { export const config = {
runtime: 'experimental-edge' runtime: 'experimental-edge'
} }
function removeUnwantedKeys(location: NomadListLocation) { interface Location extends NomadListLocation {
const { place_photo, latitude, longitude, epoch_start, epoch_end, ...rest } = lastCheckin?: string
location
return rest
} }
export default async () => { declare type LocationResponse = {
now: Location
next: Location
}
export default async function handler() {
try { try {
if (!process.env.NOMADLIST_PROFILE) { const nomadlist = await getNomadList()
throw new Error('Missing NOMADLIST_PROFILE env variable') const foursquare = await getLastCheckin()
}
if (!process.env.NOMADLIST_KEY) {
throw new Error('Missing NOMADLIST_KEY env variable')
}
const response = await fetch( const response = {
`https://nomadlist.com/@${process.env.NOMADLIST_PROFILE}.json?key=${process.env.NOMADLIST_KEY}` now: { ...nomadlist.now, ...(foursquare && { lastCheckin: foursquare }) },
) next: { ...nomadlist.next }
if (!response || !response.ok || response.status !== 200) { } as LocationResponse
throw new Error("Couldn't fetch data from NomadList")
}
const json = (await response.json()) as NomadListLocationResponse
// return only parts of the data return new Response(JSON.stringify(response, null, 2), {
const final = { headers: { 'content-type': 'application/json' }
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'
}
}) })
} catch (error) { } 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -8,18 +8,23 @@
"start": "vercel dev", "start": "vercel dev",
"test": "npm run type-check", "test": "npm run type-check",
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write", "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": { "devDependencies": {
"@types/node": "^18.11.18", "@types/node": "^20.4.9",
"prettier": "^2.8.3", "@vercel/node": "^2.15.9",
"typescript": "^4.9.4" "dotenv": "^16.3.1",
"eslint": "^8.46.0",
"node-fetch": "^3.3.2",
"prettier": "^3.0.1",
"typescript": "^5.1.6"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/kremalicious/location" "url": "https://github.com/kremalicious/location"
}, },
"engines": { "engines": {
"node": "16" "node": "18"
} }
} }

53
scripts/get-checkins.mjs Normal file
View 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()