@chronicle.app/foursquare 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Louis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @chronicle.app/foursquare
2
+
3
+ Chronicle plugin that imports your Foursquare/Swarm check-in history.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @chronicle.app/foursquare
9
+ ```
10
+
11
+ ## Authentication
12
+
13
+ Check-ins come from the authenticated user (`users/self/checkins`), so the
14
+ plugin needs a user OAuth access token. Register a Foursquare developer app to
15
+ get a client ID/secret, set its redirect URI to `http://127.0.0.1:7463/callback`,
16
+ then run:
17
+
18
+ ```bash
19
+ chronicle auth login foursquare --client-id YOUR_ID --client-secret YOUR_SECRET
20
+ ```
21
+
22
+ This stores the access token; the extractor picks it up automatically. You can
23
+ also pass a token directly with `--access-token`.
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # Uses the stored token from `auth login`
29
+ chronicle extract foursquare
30
+
31
+ # With a token and date/size filters
32
+ chronicle extract foursquare --strategy api \
33
+ --access-token YOUR_TOKEN \
34
+ --since 2024-01-01 \
35
+ --limit 1000
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ The extractor supports the following options:
41
+
42
+ - `--access-token`: Foursquare user OAuth token (auto-retrieved from stored
43
+ credentials if omitted)
44
+ - `--since`: Only check-ins on or after this date
45
+ - `--limit`: Maximum number of check-ins to extract
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ # Install dependencies
51
+ npm install
52
+
53
+ # Build the plugin
54
+ npm run build -w @chronicle.app/foursquare
55
+
56
+ # Run the tests
57
+ npm test -w @chronicle.app/foursquare
58
+ ```
59
+
60
+ ## API Endpoints
61
+
62
+ - `users/self`: the authenticated user (actor), for the check-in agent
63
+ - `users/self/checkins`: check-in history, paged (50/page) with `afterTimestamp`
64
+ for `--since`
65
+
66
+ ## Chronicle schema output
67
+
68
+ Each check-in becomes a `CheckInAction` (keyed on the check-in id, timestamped at
69
+ `createdAt`) whose:
70
+
71
+ - `agent` is an `Agent` keyed on your Foursquare user id, with name, profile
72
+ `url`, and avatar `image`
73
+ - `object` is a `Venue` keyed on the Foursquare venue id, with `category` (the
74
+ venue's Foursquare categories) and a nested keyless `Location` (lat/long +
75
+ formatted address)
76
+
77
+ Because the `Venue` is keyed on `[@type, source: foursquare, sourceId]`, it
78
+ collapses onto the same entity as an Arc Timeline place that carries a `sameAs`
79
+ edge to the same Foursquare venue id.
80
+
81
+ ## Tests
82
+
83
+ The tests serve synthetic `users/self` and check-in responses from a local HTTP
84
+ server on 127.0.0.1, pass a fake access token in config, and point Chronicle's
85
+ credential store at an empty temporary directory, so they never call Foursquare
86
+ or read the host's stored credentials or accounts.
@@ -0,0 +1,9 @@
1
+ import { OAuthProvider } from '@chronicle.app/auth';
2
+ export declare class FoursquareOAuthProvider extends OAuthProvider {
3
+ static providerId: string;
4
+ static authorizationUrl: string;
5
+ static tokenUrl: string;
6
+ static scopes: string[];
7
+ static requiresClientSecret: boolean;
8
+ }
9
+ //# sourceMappingURL=FoursquareOAuthProvider.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { OAuthProvider } from '@chronicle.app/auth';
2
+ export class FoursquareOAuthProvider extends OAuthProvider {
3
+ static providerId = 'foursquare';
4
+ static authorizationUrl = 'https://foursquare.com/oauth2/authenticate';
5
+ static tokenUrl = 'https://foursquare.com/oauth2/access_token';
6
+ static scopes = [];
7
+ static requiresClientSecret = true;
8
+ }
@@ -0,0 +1,35 @@
1
+ import { Extractor, Record } from '@chronicle.app/etl';
2
+ import { z } from 'zod';
3
+ import FoursquareTransformer from './FoursquareTransformer.js';
4
+ export declare class FoursquareExtractor extends Extractor<typeof FoursquareExtractor> {
5
+ static source: string;
6
+ static description: string;
7
+ static delivery: "api";
8
+ static strategy: string;
9
+ static recordTypes: string[];
10
+ static default: boolean;
11
+ static defaultTransformer: typeof FoursquareTransformer;
12
+ static schema: z.ZodObject<{
13
+ since: z.ZodOptional<z.ZodDate>;
14
+ until: z.ZodOptional<z.ZodDate>;
15
+ limit: z.ZodOptional<z.ZodNumber>;
16
+ } & {
17
+ 'access-token': z.ZodOptional<z.ZodString>;
18
+ }, "strip", z.ZodTypeAny, {
19
+ limit?: number | undefined;
20
+ 'access-token'?: string | undefined;
21
+ since?: Date | undefined;
22
+ until?: Date | undefined;
23
+ }, {
24
+ limit?: number | undefined;
25
+ 'access-token'?: string | undefined;
26
+ since?: Date | undefined;
27
+ until?: Date | undefined;
28
+ }>;
29
+ private proxy;
30
+ private actor;
31
+ setup(): Promise<void>;
32
+ extract(): AsyncGenerator<Record>;
33
+ determineCount(): Promise<number | null>;
34
+ }
35
+ //# sourceMappingURL=FoursquareExtractor.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { Extractor } from '@chronicle.app/etl';
2
+ import { z } from 'zod';
3
+ import { resolveCredentials } from '@chronicle.app/auth';
4
+ import FoursquareProxy from '../utils/FoursquareProxy.js';
5
+ import FoursquareTransformer from './FoursquareTransformer.js';
6
+ export class FoursquareExtractor extends Extractor {
7
+ static source = 'foursquare';
8
+ static description = 'Check-ins';
9
+ static delivery = 'api';
10
+ static strategy = 'api';
11
+ static recordTypes = ['checkins'];
12
+ static default = true;
13
+ static defaultTransformer = FoursquareTransformer;
14
+ static schema = Extractor.schema.extend({
15
+ 'access-token': z
16
+ .string()
17
+ .optional()
18
+ .describe('Foursquare access token (auto-retrieved from stored credentials if not provided)'),
19
+ });
20
+ proxy;
21
+ actor;
22
+ async setup() {
23
+ const config = this.config;
24
+ // Use the token from the flag, else the one stored by `auth login foursquare`.
25
+ const { accessToken } = await resolveCredentials('foursquare', {
26
+ accessToken: { from: ['accessToken'] },
27
+ }, {
28
+ overrides: { accessToken: config['access-token'] },
29
+ errorMessage: 'Foursquare access token is required. Either pass --access-token or authenticate with: chronicle auth login foursquare --client-id YOUR_ID --client-secret YOUR_SECRET',
30
+ });
31
+ this.proxy = new FoursquareProxy(accessToken);
32
+ this.actor = await this.proxy.loadSelf();
33
+ }
34
+ async *extract() {
35
+ const config = this.config;
36
+ try {
37
+ const checkins = await this.proxy.loadCheckins({
38
+ since: config.since,
39
+ limit: config.limit,
40
+ });
41
+ this.logger.info(`Retrieved ${checkins.length} checkins from Foursquare`);
42
+ for (const checkin of checkins) {
43
+ yield this.createRecord(checkin, { actor: this.actor });
44
+ }
45
+ }
46
+ catch (error) {
47
+ this.logger.error('Error fetching Foursquare checkins');
48
+ throw error;
49
+ }
50
+ }
51
+ async determineCount() {
52
+ return null;
53
+ }
54
+ }
@@ -0,0 +1,10 @@
1
+ import { ChronicleTransformer, Record } from '@chronicle.app/etl';
2
+ import { ActionAndChildren } from '@chronicle.app/schema';
3
+ export default class FoursquareTransformer extends ChronicleTransformer {
4
+ transform(record: Record): Promise<ActionAndChildren[]>;
5
+ private buildCheckinActions;
6
+ private buildVenue;
7
+ private buildLocation;
8
+ private buildUser;
9
+ }
10
+ //# sourceMappingURL=FoursquareTransformer.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { ChronicleTransformer, createImageObject } from '@chronicle.app/etl';
2
+ export default class FoursquareTransformer extends ChronicleTransformer {
3
+ async transform(record) {
4
+ const actions = [];
5
+ if (record.extraction.recordType === 'checkins') {
6
+ actions.push(...this.buildCheckinActions(record));
7
+ }
8
+ return actions;
9
+ }
10
+ buildCheckinActions(record) {
11
+ const actions = [];
12
+ const checkin = record.data;
13
+ const actor = record.context?.actor;
14
+ const user = this.buildUser(actor);
15
+ const venue = this.buildVenue(checkin);
16
+ const action = {
17
+ '@type': 'CheckInAction',
18
+ '@key': ['@type', 'source', 'sourceId'],
19
+ timestamp: new Date(checkin.createdAt * 1000),
20
+ source: 'foursquare',
21
+ sourceId: checkin.id,
22
+ agent: user,
23
+ object: venue,
24
+ };
25
+ actions.push(action);
26
+ return actions;
27
+ }
28
+ // A Foursquare venue is a named point of interest → a Venue. Its
29
+ // coordinates/address are its own keyless Location value (content-interned),
30
+ // not entity properties on the Venue. Keyed [@type, source, sourceId] with
31
+ // source "foursquare", so an Arc place's sameAs edge to this id collapses onto
32
+ // the same entity.
33
+ buildVenue(checkin) {
34
+ const { venue } = checkin;
35
+ const { location } = venue;
36
+ const address = location.formattedAddress ? location.formattedAddress.join(', ') : undefined;
37
+ // Foursquare's category names (the primary one first), e.g. ["Park"].
38
+ const category = [...(venue.categories ?? [])]
39
+ .sort((a, b) => (b.primary ? 1 : 0) - (a.primary ? 1 : 0))
40
+ .map(c => c.name)
41
+ .filter(Boolean);
42
+ return {
43
+ '@type': 'Venue',
44
+ '@key': ['@type', 'source', 'sourceId'],
45
+ source: 'foursquare',
46
+ sourceId: venue.id,
47
+ name: venue.name,
48
+ location: this.buildLocation(location.lat, location.lng, address),
49
+ ...(category.length > 0 && { category }),
50
+ };
51
+ }
52
+ // The geo as a `Location` value — a keyless StructuredValue that shred
53
+ // content-interns into location_values (deduped by payload).
54
+ buildLocation(latitude, longitude, address) {
55
+ return {
56
+ '@type': 'Location',
57
+ latitude,
58
+ longitude,
59
+ ...(address && { address }),
60
+ };
61
+ }
62
+ buildUser(actor) {
63
+ // Foursquare photo URLs are assembled as `${prefix}${size}${suffix}`.
64
+ const avatar = actor.photo?.prefix
65
+ ? `${actor.photo.prefix}300x300${actor.photo.suffix}`
66
+ : undefined;
67
+ return {
68
+ '@type': 'Agent',
69
+ '@key': ['@type', 'source', 'sourceId'],
70
+ source: 'foursquare',
71
+ sourceId: actor.id,
72
+ name: `${actor.firstName} ${actor.lastName}`,
73
+ url: actor.canonicalUrl,
74
+ sameAs: ['@me'],
75
+ ...(avatar && { emblem: createImageObject({ url: avatar }) }),
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,4 @@
1
+ export { FoursquareExtractor } from './connectors/FoursquareExtractor.js';
2
+ export { default as FoursquareTransformer } from './connectors/FoursquareTransformer.js';
3
+ export { FoursquareOAuthProvider } from './auth/FoursquareOAuthProvider.js';
4
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { FoursquareExtractor } from './connectors/FoursquareExtractor.js';
2
+ export { default as FoursquareTransformer } from './connectors/FoursquareTransformer.js';
3
+ export { FoursquareOAuthProvider } from './auth/FoursquareOAuthProvider.js';
4
+ // Register OAuth provider
5
+ import { OAuthProviderRegistry } from '@chronicle.app/auth';
6
+ import { FoursquareOAuthProvider } from './auth/FoursquareOAuthProvider.js';
7
+ OAuthProviderRegistry.register(FoursquareOAuthProvider);
@@ -0,0 +1,48 @@
1
+ import { ApiProxy } from '@chronicle.app/etl';
2
+ export interface FoursquareCategory {
3
+ id: string;
4
+ name: string;
5
+ primary?: boolean;
6
+ }
7
+ export interface FoursquareCheckin {
8
+ id: string;
9
+ createdAt: number;
10
+ venue: {
11
+ id: string;
12
+ name: string;
13
+ categories?: FoursquareCategory[];
14
+ location: {
15
+ lat: number;
16
+ lng: number;
17
+ formattedAddress?: string[];
18
+ country?: string;
19
+ city?: string;
20
+ state?: string;
21
+ postalCode?: string;
22
+ address?: string;
23
+ };
24
+ };
25
+ }
26
+ export interface FoursquareUser {
27
+ id: string;
28
+ firstName: string;
29
+ lastName: string;
30
+ canonicalUrl: string;
31
+ photo: {
32
+ prefix: string;
33
+ suffix: string;
34
+ };
35
+ }
36
+ export default class FoursquareProxy extends ApiProxy {
37
+ private oauthToken;
38
+ constructor(accessToken: string);
39
+ initialize(): Promise<void>;
40
+ loadCheckins(options?: {
41
+ since?: Date;
42
+ limit?: number;
43
+ }): Promise<FoursquareCheckin[]>;
44
+ loadSelf(): Promise<FoursquareUser>;
45
+ private loadCheckinPage;
46
+ private loadEndpoint;
47
+ }
48
+ //# sourceMappingURL=FoursquareProxy.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { ApiProxy } from '@chronicle.app/etl';
2
+ const API_VERSION = 20_170_310;
3
+ export default class FoursquareProxy extends ApiProxy {
4
+ // Foursquare authenticates with an `oauth_token` query parameter, not a
5
+ // bearer header, so the token lives here and the base's accessToken stays unset.
6
+ oauthToken;
7
+ constructor(accessToken) {
8
+ super({
9
+ baseURL: 'https://api.foursquare.com/v2/',
10
+ headers: {
11
+ 'User-Agent': 'Chronicle/1.0',
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ });
15
+ this.oauthToken = accessToken;
16
+ }
17
+ async initialize() {
18
+ // The access token is resolved by the extractor's own CredentialManager
19
+ // lookup and handed to the constructor; nothing to load lazily here.
20
+ }
21
+ async loadCheckins(options = {}) {
22
+ return this.paginateOffset({
23
+ pageSize: 50,
24
+ limit: options.limit,
25
+ fetchPage: offset => this.loadCheckinPage({ limit: 50, offset, since: options.since }),
26
+ });
27
+ }
28
+ async loadSelf() {
29
+ const response = await this.loadEndpoint('users/self');
30
+ return response.response.user;
31
+ }
32
+ async loadCheckinPage(options) {
33
+ const params = {
34
+ limit: options.limit,
35
+ offset: options.offset,
36
+ };
37
+ if (options.since) {
38
+ params.afterTimestamp = Math.floor(options.since.getTime() / 1000);
39
+ }
40
+ const response = await this.loadEndpoint('users/self/checkins', params);
41
+ return response.response.checkins.items;
42
+ }
43
+ async loadEndpoint(endpoint, params = {}) {
44
+ return this.request({
45
+ url: endpoint,
46
+ method: 'GET',
47
+ params: {
48
+ ...params,
49
+ oauth_token: this.oauthToken,
50
+ v: API_VERSION,
51
+ },
52
+ });
53
+ }
54
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@chronicle.app/foursquare",
3
+ "version": "0.3.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "dist/**/*.d.ts"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -b tsconfig.lib.json",
12
+ "typecheck": "tsc -p tsconfig.json",
13
+ "lint": "eslint src --ext .ts,.js",
14
+ "test": "node --test src/*.test.js"
15
+ },
16
+ "keywords": [
17
+ "chronicle",
18
+ "foursquare"
19
+ ],
20
+ "author": "Andrew Louis",
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "description": "Chronicle plugin for Foursquare check-ins",
24
+ "devDependencies": {
25
+ "@chronicle.app/etl": "0.3.0",
26
+ "@chronicle.app/schema": "0.3.0",
27
+ "@chronicle.app/tsconfig": "0.3.0",
28
+ "@types/node": "^22.10.1",
29
+ "typescript": "^5.7.2"
30
+ },
31
+ "homepage": "https://chronicle.app",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/chronicle-app/chronicle.git",
35
+ "directory": "plugins/foursquare"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "dependencies": {
42
+ "@chronicle.app/auth": "0.3.0",
43
+ "axios": "^1.7.9",
44
+ "zod": "^3.24.1"
45
+ },
46
+ "peerDependencies": {
47
+ "@chronicle.app/etl": ">=0.1.0 <1.0.0",
48
+ "@chronicle.app/schema": ">=0.1.0 <1.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=22.13.0"
52
+ },
53
+ "exports": {
54
+ ".": {
55
+ "types": "./dist/index.d.ts",
56
+ "import": "./dist/index.js"
57
+ }
58
+ },
59
+ "chronicle": {
60
+ "plugin": true,
61
+ "sources": {
62
+ "foursquare": {
63
+ "strategies": {
64
+ "api": {
65
+ "delivery": "api",
66
+ "recordTypes": [
67
+ "checkins"
68
+ ],
69
+ "default": true
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }