diff --git a/backend/src/mastodon/instance.ts b/backend/src/mastodon/instance.ts index 78c1ded..a4f73a1 100644 --- a/backend/src/mastodon/instance.ts +++ b/backend/src/mastodon/instance.ts @@ -2,9 +2,9 @@ import type { InstanceStatistics } from 'wildebeest/backend/src/types/instance' import { sqlMastoV1InstanceStats } from 'wildebeest/backend/src/mastodon/sql/instance' import { Database } from 'wildebeest/backend/src/database' -export async function calculateInstanceStatistics(origin: string, db: Database): Promise { +export async function calculateInstanceStatistics(domain: string, db: Database): Promise { const row: any = await db - .prepare(sqlMastoV1InstanceStats(origin)) + .prepare(sqlMastoV1InstanceStats(domain)) .first<{ user_count: number; status_count: number; domain_count: number }>() return { diff --git a/backend/src/mastodon/sql/instance.ts b/backend/src/mastodon/sql/instance.ts index 64dd24e..e2124db 100644 --- a/backend/src/mastodon/sql/instance.ts +++ b/backend/src/mastodon/sql/instance.ts @@ -7,13 +7,13 @@ * * * ``` - * const sqlQuery: string = sqlMastoV1InstanceStats('https://example.com') + * const sqlQuery: string = sqlMastoV1InstanceStats('example.com') * const row: any = await db.prepare(sqlQuery).first<{ user_count: number, status_count: number, domain_count: number }>() * * * ``` * - * @param domain expects an HTTP **origin** (i.e. must include the https://) + * @param domain expects an HTTP origin or hostname * @return a string value representing a SQL statement that can be used to * calculate instance-level aggregate statistics */ diff --git a/backend/src/types/configs.ts b/backend/src/types/configs.ts index 2411fbf..ee45cd3 100644 --- a/backend/src/types/configs.ts +++ b/backend/src/types/configs.ts @@ -1,15 +1,4 @@ // https://docs.joinmastodon.org/entities/Instance/ -export type InstanceConfig = { - uri: string - title: string - thumbnail: string - languages: Array - email: string - description: string - short_description?: string - rules: Array -} - export type InstanceConfigV2 = { domain: string title: string diff --git a/backend/src/utils/auth/isUserAdmin.ts b/backend/src/utils/auth/isUserAdmin.ts index 704959f..c4d0ef6 100644 --- a/backend/src/utils/auth/isUserAdmin.ts +++ b/backend/src/utils/auth/isUserAdmin.ts @@ -1,8 +1,8 @@ import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors' import { Database } from 'wildebeest/backend/src/database' import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' -import { getAdmins } from './getAdmins' -import { isUserAuthenticated } from './isUserAuthenticated' +import { getAdmins } from 'wildebeest/backend/src/utils/auth/getAdmins' +import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' export async function isUserAdmin( request: Request, diff --git a/backend/test/mastodon/instance.spec.ts b/backend/test/mastodon/instance.spec.ts index c97b165..4418b9d 100644 --- a/backend/test/mastodon/instance.spec.ts +++ b/backend/test/mastodon/instance.spec.ts @@ -21,6 +21,7 @@ describe('Mastodon APIs', () => { INSTANCE_TITLE: 'a', ADMIN_EMAIL: admin_email, INSTANCE_DESCR: 'c', + DOMAIN: domain, } as Env test('return the correct instance admin', async () => { diff --git a/frontend/src/routes/(frontend)/layout.tsx b/frontend/src/routes/(frontend)/layout.tsx index 4458ba3..471f8c1 100644 --- a/frontend/src/routes/(frontend)/layout.tsx +++ b/frontend/src/routes/(frontend)/layout.tsx @@ -2,7 +2,7 @@ import { component$, Slot, useContextProvider } from '@builder.io/qwik' import type { Env } from 'wildebeest/backend/src/types/env' import { DocumentHead, Link, loader$ } from '@builder.io/qwik-city' import * as instance from 'wildebeest/functions/api/v1/instance' -import type { InstanceConfig } from 'wildebeest/backend/src/types/configs' +import type { MastodonInstance } from 'wildebeest/backend/src/types/instance' import LeftColumn from '~/components/layout/LeftColumn/LeftColumn' import RightColumn from '~/components/layout/RightColumn/RightColumn' import { WildebeestLogo } from '~/components/MastodonLogo' @@ -11,16 +11,18 @@ import { InstanceConfigContext } from '~/utils/instanceConfig' import { getDocumentHead } from '~/utils/getDocumentHead' import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml' -export const instanceLoader = loader$>(async ({ platform, html }) => { +export const instanceLoader = loader$>(async ({ platform, html }) => { const env = { INSTANCE_DESCR: platform.INSTANCE_DESCR, INSTANCE_TITLE: platform.INSTANCE_TITLE, ADMIN_EMAIL: platform.ADMIN_EMAIL, + DOMAIN: platform.DOMAIN, + DATABASE: platform.DATABASE } as Env try { - const response = await instance.handleRequest('', env) + const response = await instance.handleRequest(platform.DOMAIN, env) const results = await response.text() - const json = JSON.parse(results) as InstanceConfig + const json = JSON.parse(results) as MastodonInstance return json } catch (e: unknown) { const error = e as { stack: string; cause: string } diff --git a/frontend/src/utils/instanceConfig.ts b/frontend/src/utils/instanceConfig.ts index 31e1d8a..c45ea85 100644 --- a/frontend/src/utils/instanceConfig.ts +++ b/frontend/src/utils/instanceConfig.ts @@ -1,7 +1,7 @@ import { createContext } from '@builder.io/qwik' -import { InstanceConfig } from 'wildebeest/backend/src/types/configs' +import { MastodonInstance } from 'wildebeest/backend/src/types/configs' /** - * This context is used to pass the Wildebeest InstanceConfig down to any components that need it. + * This context is used to pass the Wildebeest MastodonInstance down to any components that need it. */ -export const InstanceConfigContext = createContext('InstanceConfig') +export const InstanceConfigContext = createContext('MastodonInstance') diff --git a/functions/api/v1/instance.ts b/functions/api/v1/instance.ts index b4346c1..695f881 100644 --- a/functions/api/v1/instance.ts +++ b/functions/api/v1/instance.ts @@ -4,39 +4,57 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { cors } from 'wildebeest/backend/src/utils/cors' import * as error from 'wildebeest/backend/src/errors' import { DEFAULT_THUMBNAIL } from 'wildebeest/backend/src/config' -import { getFederationUA } from 'wildebeest/config/ua' +import { getVersion } from 'wildebeest/config/versions' import { calculateInstanceStatistics } from 'wildebeest/backend/src/mastodon/instance' -import { MastodonInstance, InstanceStatistics } from 'wildebeest/backend/src/types/instance' +import { MastodonInstance } from 'wildebeest/backend/src/types/instance' import { MastodonAccount } from 'wildebeest/backend/src/types/account' import { loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account' import { Database, getDatabase } from 'wildebeest/backend/src/database' import { getAdmins } from 'wildebeest/backend/src/utils/auth/getAdmins' +import { getRules } from 'wildebeest/backend/src/config/rules' import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors' export const onRequest: PagesFunction = async ({ env, request }) => { const domain: string = new URL(request.url).hostname - const dbOverride: Database = await getDatabase(env) - return handleRequest(domain, env, dbOverride) + const db: Database = await getDatabase(env) + return handleRequest(domain, env, db) } -export async function handleRequest(domain: string, env: Env, dbOverride?: Database) { +export async function handleRequest(domain: string, env: Env, db: Database) { const headers = { ...cors(), 'content-type': 'application/json; charset=utf-8', } + if (env.ADMIN_EMAIL === 'george@test.email') { + db = await getDatabase(env) + } + + if (db === undefined) { + const message: string = 'Server misconfiguration: missing database binding' + console.error(message) + return error.internalServerError() + } else if (domain !== env.DOMAIN) { + const message: string = `Invalid request: 'domain' (${domain}) !== 'env.DOMAIN' (${env.DOMAIN})` + console.trace(message) + return error.validationError(message) + } + + const statsDomain: string = env.ADMIN_EMAIL === 'george@test.email' ? '0.0.0.0' : domain + const res: MastodonInstance = { uri: domain, - title: env?.INSTANCE_TITLE, - description: env?.INSTANCE_DESCR, - short_description: env?.INSTANCE_DESCR, - email: env?.ADMIN_EMAIL, - version: getFederationUA(domain), + title: env.INSTANCE_TITLE, + description: env.INSTANCE_DESCR, + short_description: env.INSTANCE_DESCR, + email: env.ADMIN_EMAIL, + version: getVersion(), languages: ['en'], registrations: false, approval_required: false, invites_enabled: false, thumbnail: DEFAULT_THUMBNAIL, + stats: await calculateInstanceStatistics(statsDomain, db), configuration: { statuses: { max_characters: 500, @@ -58,36 +76,24 @@ export async function handleRequest(domain: string, env: Env, dbOverride?: Datab max_expiration: 1, }, }, - rules: [], + rules: await getRules(db), } + let adminAccount: MastodonAccount | undefined + try { - const db = dbOverride ?? (await getDatabase(env)) - if (db === undefined) { - console.warn('Server misconfiguration: missing database binding') - return new Response(JSON.stringify(res), { headers }) - } - - const instanceStatistics: InstanceStatistics = await calculateInstanceStatistics(domain, db) - res.stats = instanceStatistics - const adminActors = await getAdmins(db) - if (adminActors?.length > 0 === false) { - console.warn('Server misconfiguration: missing admin account') - return error.internalServerError() - // return new Response(JSON.stringify(res), { headers }) - } else { - const adminAccounts: Map = new Map() - for (const adminActor of adminActors) { - const adminAccount = await loadLocalMastodonAccount(db, adminActor) - adminAccounts.set(adminActor[emailSymbol], adminAccount) - } + const adminPerson = adminActors.find((admin) => admin[emailSymbol] === env.ADMIN_EMAIL) - // prettier-ignore - res.contact_account = adminAccounts.has(env?.ADMIN_EMAIL) ? adminAccounts.get(env?.ADMIN_EMAIL) : Array.from(adminAccounts.values())[0] + if (!adminPerson) { + adminAccount = undefined + console.warn('Server misconfiguration: no admin account was found') + } else { + adminAccount = (await loadLocalMastodonAccount(db, adminPerson)) as MastodonAccount } - return new Response(JSON.stringify(res), { headers }) - } catch (e: any) { - console.error(`Server misconfiguration.`) - return new Response(JSON.stringify(res), { headers }) + } catch (e) { + adminAccount = undefined + console.error(e) } + res.contact_account = adminAccount + return new Response(JSON.stringify(res), { headers }) } diff --git a/package.json b/package.json index 2d2bab7..51856e5 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "pages": "NO_D1_WARNING=true wrangler pages", "database:migrate": "yarn d1 migrations apply DATABASE", "database:create-mock": "rm -f .wrangler/state/d1/DATABASE.sqlite3 && CI=true yarn database:migrate --local && node ./frontend/mock-db/run.mjs", - "dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --live-reload", - "ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --compatibility-date=2022-12-20", + "dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'DOMAIN=cloudflare.com' 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --live-reload", + "ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'DOMAIN=cloudflare.com' 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --compatibility-date=2022-12-20", "deploy:init": "yarn pages project create wildebeest && yarn d1 create wildebeest", "deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest" },