diff --git a/.env.example b/.env.example index 763ea3f7..d7b0c3bb 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,6 @@ MASTODON_TOKEN= + +# Production only +NUXT_CLOUDFLARE_ACCOUNT_ID= +NUXT_CLOUDFLARE_NAMESPACE_ID= +NUXT_CLOUDFLARE_API_TOKEN= diff --git a/.gitignore b/.gitignore index 3aa45eb4..467d4664 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ dist .output .nuxt .env - -registered-apps.json diff --git a/nuxt.config.ts b/nuxt.config.ts index 8ecb43cb..d999c021 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -33,6 +33,10 @@ export default defineNuxtConfig({ }, }, runtimeConfig: { - registedAppsUrl: process.env.APPS_JSON_URL || 'http://localhost:3000/registered-apps.json', + cloudflare: { + accountId: '', + namespaceId: '', + apiToken: '', + }, }, }) diff --git a/package.json b/package.json index c3e42963..c8093823 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "dev": "nuxi dev", "start": "node .output/server/index.mjs", "lint": "eslint .", - "register-apps": "esno ./scripts/registerApps.ts", "postinstall": "nuxi prepare", "generate": "nuxi generate" }, diff --git a/scripts/registerApps.ts b/scripts/registerApps.ts deleted file mode 100644 index 3d17ed6d..00000000 --- a/scripts/registerApps.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from 'fs-extra' -import { $fetch } from 'ohmyfetch' -import { APP_NAME } from '~/constants' -import type { AppInfo } from '~/types' - -const KNOWN_SERVERS = [ - 'mastodon.social', - 'mas.to', - 'fosstodon.org', - 'm.cmx.im', - 'mastodon.world', -] - -const KNOWN_DOMAINS = [ - 'http://localhost:3000', - 'https://elk.netlify.app', - 'https://elk.zone', -] - -const filename = 'public/registered-apps.json' - -let registeredApps: Record = {} - -if (fs.existsSync(filename)) - registeredApps = await fs.readJSON(filename) - -for (const server of KNOWN_SERVERS) { - const redirect_uris = [ - 'urn:ietf:wg:oauth:2.0:oob', - ...KNOWN_DOMAINS.map(d => `${d}/api/${server}/oauth`), - ].join('\n') - - if (!registeredApps[server] || registeredApps[server].redirect_uri !== redirect_uris) { - const app = await $fetch(`https://${server}/api/v1/apps`, { - method: 'POST', - body: { - client_name: APP_NAME, - redirect_uris, - scopes: 'read write follow push', - }, - }) - - registeredApps[server] = app - - console.log(`Registered app for ${server}`) - } -} - -if (!fs.existsSync('public')) - await fs.mkdir('public') - -await fs.writeJSON(filename, registeredApps, { spaces: 2, EOL: '\n' }) diff --git a/server/cache-driver.ts b/server/cache-driver.ts new file mode 100644 index 00000000..3f18fad6 --- /dev/null +++ b/server/cache-driver.ts @@ -0,0 +1,40 @@ +import type { Driver } from 'unstorage' +// @ts-expect-error unstorage needs to provide backwards-compatible subpath types +import _memory from 'unstorage/drivers/memory' +import { defineDriver } from 'unstorage' + +const memory = _memory as typeof import('unstorage/dist/drivers/memory')['default'] + +export interface CacheDriverOptions { + driver: Driver +} + +export default defineDriver((driver: Driver = memory()) => { + const memoryDriver = memory() + return { + ...driver, + async hasItem(key: string) { + if (await memoryDriver.hasItem(key)) + return true + + return driver.hasItem(key) + }, + async setItem(key: string, value: any) { + await Promise.all([ + memoryDriver.setItem(key, value), + driver.setItem?.(key, value), + ]) + }, + async getItem(key: string) { + let value = await memoryDriver.getItem(key) + + if (value !== null) + return value + + value = await driver.getItem(key) + memoryDriver.setItem(key, value) + + return value + }, + } +}) diff --git a/server/shared.ts b/server/shared.ts index 64d14955..debc6dca 100644 --- a/server/shared.ts +++ b/server/shared.ts @@ -1,26 +1,69 @@ +// @ts-expect-error unstorage needs to provide backwards-compatible subpath types +import _fs from 'unstorage/drivers/fs' +// @ts-expect-error unstorage needs to provide backwards-compatible subpath types +import _kv from 'unstorage/drivers/cloudflare-kv-http' + import { $fetch } from 'ohmyfetch' +import type { Storage } from 'unstorage' + +import cached from './cache-driver' + import type { AppInfo } from '~/types' +import { APP_NAME } from '~/constants' -export const registeredApps: Record = {} +const fs = _fs as typeof import('unstorage/dist/drivers/fs')['default'] +const kv = _kv as typeof import('unstorage/dist/drivers/cloudflare-kv-http')['default'] -const runtimeConfig = useRuntimeConfig() -const promise = $fetch(runtimeConfig.registedAppsUrl, { responseType: 'json' }) - .then((r) => { - Object.assign(registeredApps, r) - // eslint-disable-next-line no-console - console.log(`\n${Object.keys(registeredApps).length} registered apps loaded from ${runtimeConfig.registedAppsUrl.split(/\/+/g)[1]}`) - // eslint-disable-next-line no-console - console.log(`${Object.keys(registeredApps).map(i => ` - ${i}`).join('\n')}\n`) - }) - .catch((e) => { - if (process.dev) - console.error('Failed to fetch registered apps,\nyou may need to run `nr register-apps` first') - else - console.error('Failed to fetch registered apps') - console.error(e) +const storage = useStorage() as Storage + +if (process.dev) { + storage.mount('servers', fs({ base: 'node_modules/.cache/servers' })) +} +else { + const config = useRuntimeConfig() + storage.mount('servers', cached(kv({ + accountId: config.cloudflare.accountId, + namespaceId: config.cloudflare.namespaceId, + apiToken: config.cloudflare.apiToken, + }))) +} + +const KNOWN_DOMAINS = [ + 'http://localhost:3000', + 'https://elk.netlify.app', + 'https://elk.zone', +] + +async function fetchAppInfo(server: string) { + const redirect_uris = [ + 'urn:ietf:wg:oauth:2.0:oob', + ...KNOWN_DOMAINS.map(d => `${d}/api/${server}/oauth`), + ].join('\n') + + const app: AppInfo = await $fetch(`https://${server}/api/v1/apps`, { + method: 'POST', + body: { + client_name: APP_NAME, + redirect_uris, + scopes: 'read write follow push', + }, }) + return app +} + +const serverKey = (server: string) => `servers:${server}.json` export async function getApp(server: string) { - await promise - return registeredApps[server] + const key = serverKey(server) + if (await storage.hasItem(key)) + return storage.getItem(key) as Promise + + try { + const appInfo = await fetchAppInfo(server) + await storage.setItem(key, appInfo) + return appInfo + } + catch { + return null + } }