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_KEY=xxx
NOMADLIST_KEY=xxx
FOURSQUARE_KEY=xxx

3
.gitignore vendored
View File

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

View File

@ -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

View File

@ -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
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",
"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
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()