mirror of
https://github.com/kremalicious/location.git
synced 2024-11-21 17:36:59 +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_PROFILE=xxx
|
||||||
NOMADLIST_KEY=xxx
|
NOMADLIST_KEY=xxx
|
||||||
|
FOURSQUARE_KEY=xxx
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vercel
|
.vercel
|
||||||
.env
|
.env
|
||||||
|
checkins.json
|
18
README.md
18
README.md
@ -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
|
||||||
|
69
api/index.ts
69
api/index.ts
@ -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
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",
|
"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
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