From 1d571d686ed8fee01610147f13ac4bbddfa45432 Mon Sep 17 00:00:00 2001 From: "Jorge Caballero (DataDrivenMD)" <116459476+DataDrivenMD@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:07:09 -0800 Subject: [PATCH] Revert "Merge branch 'api/v1/instance' into fix-spread-and-rest-based-binding" This reverts commit 925eef2d5c02ab271b65054f1471309d2b530997, reversing changes made to 85b0ac44a7a8079661943620985a76ec399a5a2c. --- backend/src/mastodon/instance.ts | 15 --- backend/src/mastodon/sql/instance.ts | 28 ----- backend/src/types/instance.ts | 67 ----------- backend/test/mastodon.spec.ts | 77 +++++++++++++ backend/test/mastodon/instance.spec.ts | 154 ++----------------------- config/ua.ts | 2 +- config/versions.ts | 4 + functions/api/v1/instance.ts | 100 ++++------------ functions/api/v2/instance.ts | 4 +- 9 files changed, 118 insertions(+), 333 deletions(-) delete mode 100644 backend/src/mastodon/instance.ts delete mode 100644 backend/src/mastodon/sql/instance.ts delete mode 100644 backend/src/types/instance.ts diff --git a/backend/src/mastodon/instance.ts b/backend/src/mastodon/instance.ts deleted file mode 100644 index 78c1ded..0000000 --- a/backend/src/mastodon/instance.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 { - const row: any = await db - .prepare(sqlMastoV1InstanceStats(origin)) - .first<{ user_count: number; status_count: number; domain_count: number }>() - - return { - user_count: row?.user_count ?? 0, - status_count: row?.status_count ?? 0, - domain_count: row?.domain_count ?? 1, - } as InstanceStatistics -} diff --git a/backend/src/mastodon/sql/instance.ts b/backend/src/mastodon/sql/instance.ts deleted file mode 100644 index 64dd24e..0000000 --- a/backend/src/mastodon/sql/instance.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Prepared statements for Mastodon Instance API endpoints -/** Returns a SQL statement that can be used to calculate the instance-level - * statistics required by the Mastodon `GET /api/v1/instance` endpoint. The - * string returned by this method should be passed as a prepared statement - * to a `Database` object that references a Wildebeest database instance in order - * to retrieve actual results. For example: - * - * - * ``` - * const sqlQuery: string = sqlMastoV1InstanceStats('https://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://) - * @return a string value representing a SQL statement that can be used to - * calculate instance-level aggregate statistics - */ -export const sqlMastoV1InstanceStats = (domain: string): string => { - return ` - SELECT - (SELECT count(1) FROM actors WHERE type IN ('Person', 'Service') AND id LIKE '%${domain}/ap/users/%') AS user_count, - (SELECT count(1) FROM objects WHERE local = 1 AND type = 'Note') AS status_count, - (SELECT count(1) FROM peers) + 1 AS domain_count - ; -` -} diff --git a/backend/src/types/instance.ts b/backend/src/types/instance.ts deleted file mode 100644 index d2bac34..0000000 --- a/backend/src/types/instance.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { MastodonAccount } from './account' - -// https://docs.joinmastodon.org/entities/Instance/ -// https://github.com/mastodon/mastodon-ios/blob/develop/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon%2BEntity%2BInstance.swift -// https://github.com/mastodon/mastodon-android/blob/master/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java -export interface MastodonInstance { - uri: string - title: string - description: string - short_description: string - email: string - version?: string - languages?: Array - registrations?: boolean - approval_required?: boolean - invites_enabled?: boolean - urls?: InstanceURL - statistics?: InstanceStatistics - stats?: InstanceStatistics - thumbnail?: string - contact_account?: MastodonAccount - rules?: Array - configuration?: InstanceConfiguration -} - -export interface InstanceURL { - streaming_api: string -} - -export type InstanceStatistics = { - user_count: number - status_count: number - domain_count: number -} - -export type InstanceRule = { - id: string - text: string -} - -export type InstanceConfiguration = { - statuses?: StatusesConfiguration - media_attachments?: MediaAttachmentsConfiguration - polls?: PollsConfiguration -} - -export type StatusesConfiguration = { - max_characters: number - max_media_attachments: number - characters_reserved_per_url: number -} - -export type MediaAttachmentsConfiguration = { - supported_mime_types: Array - image_size_limit: number - image_matrix_limit: number - video_size_limit: number - video_frame_rate_limit: number - video_matrix_limit: number -} - -export type PollsConfiguration = { - max_options: number - max_characters_per_option: number - min_expiration: number - max_expiration: number -} diff --git a/backend/test/mastodon.spec.ts b/backend/test/mastodon.spec.ts index 4b28670..c5ca0ea 100644 --- a/backend/test/mastodon.spec.ts +++ b/backend/test/mastodon.spec.ts @@ -1,4 +1,7 @@ import { strict as assert } from 'node:assert/strict' +import type { Env } from 'wildebeest/backend/src/types/env' +import * as v1_instance from 'wildebeest/functions/api/v1/instance' +import * as v2_instance from 'wildebeest/functions/api/v2/instance' import * as custom_emojis from 'wildebeest/functions/api/v1/custom_emojis' import * as mutes from 'wildebeest/functions/api/v1/mutes' import * as blocks from 'wildebeest/functions/api/v1/blocks' @@ -11,6 +14,80 @@ const userKEK = 'test_kek23' const domain = 'cloudflare.com' describe('Mastodon APIs', () => { + describe('instance', () => { + type Data = { + rules: unknown[] + uri: string + title: string + email: string + description: string + version: string + domain: string + contact: { email: string } + } + + test('return the instance infos v1', async () => { + const env = { + INSTANCE_TITLE: 'a', + ADMIN_EMAIL: 'b', + INSTANCE_DESCR: 'c', + } as Env + + const res = await v1_instance.handleRequest(domain, env) + assert.equal(res.status, 200) + assertCORS(res) + assertJSON(res) + + { + const data = await res.json() + assert.equal(data.rules.length, 0) + assert.equal(data.uri, domain) + assert.equal(data.title, 'a') + assert.equal(data.email, 'b') + assert.equal(data.description, 'c') + assert(data.version.includes('Wildebeest')) + } + }) + + test('adds a short_description if missing v1', async () => { + const env = { + INSTANCE_DESCR: 'c', + } as Env + + const res = await v1_instance.handleRequest(domain, env) + assert.equal(res.status, 200) + + { + const data = await res.json() + assert.equal(data.short_description, 'c') + } + }) + + test('return the instance infos v2', async () => { + const db = await makeDB() + + const env = { + INSTANCE_TITLE: 'a', + ADMIN_EMAIL: 'b', + INSTANCE_DESCR: 'c', + } as Env + const res = await v2_instance.handleRequest(domain, db, env) + assert.equal(res.status, 200) + assertCORS(res) + assertJSON(res) + + { + const data = await res.json() + assert.equal(data.rules.length, 0) + assert.equal(data.domain, domain) + assert.equal(data.title, 'a') + assert.equal(data.contact.email, 'b') + assert.equal(data.description, 'c') + assert(data.version.includes('Wildebeest')) + } + }) + }) + describe('custom emojis', () => { test('returns an empty array', async () => { const res = await custom_emojis.onRequest() diff --git a/backend/test/mastodon/instance.spec.ts b/backend/test/mastodon/instance.spec.ts index c97b165..be13158 100644 --- a/backend/test/mastodon/instance.spec.ts +++ b/backend/test/mastodon/instance.spec.ts @@ -1,152 +1,22 @@ import { addPeer } from 'wildebeest/backend/src/activitypub/peers' import { strict as assert } from 'node:assert/strict' -import type { Env } from 'wildebeest/backend/src/types/env' -import * as v1_instance from 'wildebeest/functions/api/v1/instance' -import * as v2_instance from 'wildebeest/functions/api/v2/instance' import * as peers from 'wildebeest/functions/api/v1/instance/peers' -import { makeDB, assertCORS, assertJSON } from 'wildebeest/backend/test/utils' -import { createPerson } from 'wildebeest/backend/src/activitypub/actors' -import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note' -import { MastodonInstance } from 'wildebeest/backend/src/types/instance' - -const adminKEK = 'admin' -const userKEK = 'test_kek2' -const admin_email = 'admin@cloudflare.com' -const domain = 'cloudflare.com' +import { makeDB } from '../utils' describe('Mastodon APIs', () => { - describe('/v1', () => { - describe('/instance', () => { - const env = { - INSTANCE_TITLE: 'a', - ADMIN_EMAIL: admin_email, - INSTANCE_DESCR: 'c', - } as Env + describe('instance', () => { + test('returns peers', async () => { + const db = await makeDB() + await addPeer(db, 'a') + await addPeer(db, 'b') - test('return the correct instance admin', async () => { - const db = await makeDB() - await createPerson(domain, db, adminKEK, admin_email, {}, true) + const res = await peers.handleRequest(db) + assert.equal(res.status, 200) - const res = await v1_instance.handleRequest(domain, env, db) - assert.equal(res.status, 200) - assertCORS(res) - assertJSON(res) - - { - const data = await res.json() - assert.equal(data.email, admin_email) - assert.equal(data?.contact_account?.acct, adminKEK) - } - }) - - test('return the correct instance statistics', async () => { - const db = await makeDB() - const person = await createPerson(domain, db, adminKEK, admin_email, {}, true) - await createPerson(domain, db, userKEK, 'sven@cloudflare.com') - await addPeer(db, 'a') - await addPeer(db, 'b') - await createPublicNote(domain, db, 'my first status', person) - - const res = await v1_instance.handleRequest(domain, env, db) - assert.equal(res.status, 200) - assertCORS(res) - assertJSON(res) - - { - const data = await res.json() - assert.equal(data.stats?.user_count, 2) - assert.equal(data.stats?.status_count, 1) - assert.equal(data.stats?.domain_count, 3) - } - }) - - test('return the instance info', async () => { - const db = await makeDB() - await createPerson(domain, db, adminKEK, admin_email, {}, true) - - const res = await v1_instance.handleRequest(domain, env, db) - assert.equal(res.status, 200) - assertCORS(res) - assertJSON(res) - - { - const data = await res.json() - assert.equal(data.rules?.length, 0) - assert.equal(data.uri, domain) - assert.equal(data.title, 'a') - assert.equal(data.email, admin_email) - assert.equal(data.description, 'c') - assert(data.version?.includes('Wildebeest')) - } - }) - - test('adds a short_description if missing v1', async () => { - const db = await makeDB() - await createPerson(domain, db, adminKEK, admin_email, {}, true) - - const res = await v1_instance.handleRequest(domain, env, db) - assert.equal(res.status, 200) - - { - const data = await res.json() - assert.equal(data.short_description, 'c') - } - }) - - describe('/peers', () => { - test('returns peers', async () => { - const db = await makeDB() - await addPeer(db, 'a') - await addPeer(db, 'b') - - const res = await peers.handleRequest(db) - assert.equal(res.status, 200) - - const data = await res.json>() - assert.equal(data.length, 2) - assert.equal(data[0], 'a') - assert.equal(data[1], 'b') - }) - }) - }) - }) - describe('/v2', () => { - describe('/instance', () => { - type Data = { - rules: unknown[] - uri: string - title: string - email: string - description: string - version: string - domain: string - contact: { email: string } - } - - test('return the instance infos v2', async () => { - const db = await makeDB() - await createPerson(domain, db, adminKEK, admin_email, {}, true) - - const env = { - INSTANCE_TITLE: 'a', - ADMIN_EMAIL: 'b', - INSTANCE_DESCR: 'c', - } as Env - const res = await v2_instance.handleRequest(domain, db, env) - assert.equal(res.status, 200) - assertCORS(res) - assertJSON(res) - - { - const data = await res.json() - assert.equal(data.rules.length, 0) - assert.equal(data.domain, domain) - assert.equal(data.title, 'a') - assert.equal(data.contact.email, 'b') - assert.equal(data.description, 'c') - assert(data.version.includes('Wildebeest')) - } - }) + const data = await res.json>() + assert.equal(data.length, 2) + assert.equal(data[0], 'a') + assert.equal(data[1], 'b') }) }) }) diff --git a/config/ua.ts b/config/ua.ts index a70b92f..b1201b1 100644 --- a/config/ua.ts +++ b/config/ua.ts @@ -1,5 +1,5 @@ import { WILDEBEEST_VERSION, MASTODON_API_VERSION } from 'wildebeest/config/versions' export function getFederationUA(domain: string): string { - return `Wildebeest/${WILDEBEEST_VERSION} (Mastodon/${MASTODON_API_VERSION} compatible; +https://${domain})` + return `Wildebeest/${WILDEBEEST_VERSION} (Mastodon/${MASTODON_API_VERSION}; +${domain})` } diff --git a/config/versions.ts b/config/versions.ts index 66cb01f..737dd2e 100644 --- a/config/versions.ts +++ b/config/versions.ts @@ -4,3 +4,7 @@ import * as packagejson from '../package.json' export const MASTODON_API_VERSION = '4.0.2' export const WILDEBEEST_VERSION = packagejson.version + +export function getVersion(): string { + return `${MASTODON_API_VERSION} (compatible; Wildebeest ${WILDEBEEST_VERSION})` +} diff --git a/functions/api/v1/instance.ts b/functions/api/v1/instance.ts index b4346c1..b04dd6c 100644 --- a/functions/api/v1/instance.ts +++ b/functions/api/v1/instance.ts @@ -1,93 +1,37 @@ -// https://docs.joinmastodon.org/entities/Instance/ -// https://docs.joinmastodon.org/methods/instance/ 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 { calculateInstanceStatistics } from 'wildebeest/backend/src/mastodon/instance' -import { MastodonInstance, InstanceStatistics } 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 { emailSymbol } from 'wildebeest/backend/src/activitypub/actors' +import { getVersion } from 'wildebeest/config/versions' 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 domain = new URL(request.url).hostname + return handleRequest(domain, env) } -export async function handleRequest(domain: string, env: Env, dbOverride?: Database) { +export async function handleRequest(domain: string, env: Env) { + // prettier-ignore const headers = { - ...cors(), 'content-type': 'application/json; charset=utf-8', + ...cors() } - 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), - languages: ['en'], - registrations: false, - approval_required: false, - invites_enabled: false, - thumbnail: DEFAULT_THUMBNAIL, - configuration: { - statuses: { - max_characters: 500, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, - media_attachments: { - supported_mime_types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, - polls: { - max_options: 0, - max_characters_per_option: 1, - min_expiration: 1, - max_expiration: 1, - }, - }, - rules: [], - } - 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 res: any = {} - const instanceStatistics: InstanceStatistics = await calculateInstanceStatistics(domain, db) - res.stats = instanceStatistics + res.thumbnail = DEFAULT_THUMBNAIL - 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) - } + // Registration is disabled because unsupported by Wildebeest. Users + // should go through the login flow and authenticate with Access. + // The documentation is incorrect and registrations is a boolean. + res.registrations = false - // prettier-ignore - res.contact_account = adminAccounts.has(env?.ADMIN_EMAIL) ? adminAccounts.get(env?.ADMIN_EMAIL) : Array.from(adminAccounts.values())[0] - } - return new Response(JSON.stringify(res), { headers }) - } catch (e: any) { - console.error(`Server misconfiguration.`) - return new Response(JSON.stringify(res), { headers }) - } + res.version = getVersion() + res.rules = [] + res.uri = domain + res.title = env.INSTANCE_TITLE + res.email = env.ADMIN_EMAIL + res.description = env.INSTANCE_DESCR + + res.short_description = res.description + + return new Response(JSON.stringify(res), { headers }) } diff --git a/functions/api/v2/instance.ts b/functions/api/v2/instance.ts index 9e313a7..80439d8 100644 --- a/functions/api/v2/instance.ts +++ b/functions/api/v2/instance.ts @@ -2,7 +2,7 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { cors } from 'wildebeest/backend/src/utils/cors' import { DEFAULT_THUMBNAIL } from 'wildebeest/backend/src/config' import type { InstanceConfigV2 } from 'wildebeest/backend/src/types/configs' -import { getFederationUA } from 'wildebeest/config/ua' +import { getVersion } from 'wildebeest/config/versions' import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ env, request }) => { @@ -20,7 +20,7 @@ export async function handleRequest(domain: string, db: Database, env: Env) { const res: InstanceConfigV2 = { domain, title: env.INSTANCE_TITLE, - version: getFederationUA(domain), + version: getVersion(), source_url: 'https://github.com/cloudflare/wildebeest', description: env.INSTANCE_DESCR, thumbnail: {