kopia lustrzana https://github.com/cloudflare/wildebeest
Revert "Merge branch 'api/v1/instance' into fix-spread-and-rest-based-binding"
This reverts commitpull/366/head925eef2d5c
, reversing changes made to85b0ac44a7
.
rodzic
925eef2d5c
commit
1d571d686e
|
@ -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<InstanceStatistics> {
|
||||
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
|
||||
}
|
|
@ -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
|
||||
;
|
||||
`
|
||||
}
|
|
@ -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<string>
|
||||
registrations?: boolean
|
||||
approval_required?: boolean
|
||||
invites_enabled?: boolean
|
||||
urls?: InstanceURL
|
||||
statistics?: InstanceStatistics
|
||||
stats?: InstanceStatistics
|
||||
thumbnail?: string
|
||||
contact_account?: MastodonAccount
|
||||
rules?: Array<InstanceRule>
|
||||
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<string>
|
||||
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
|
||||
}
|
|
@ -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<Data>()
|
||||
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<any>()
|
||||
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<Data>()
|
||||
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()
|
||||
|
|
|
@ -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<MastodonInstance>()
|
||||
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<MastodonInstance>()
|
||||
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<MastodonInstance>()
|
||||
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<any>()
|
||||
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<Array<string>>()
|
||||
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<Data>()
|
||||
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<Array<string>>()
|
||||
assert.equal(data.length, 2)
|
||||
assert.equal(data[0], 'a')
|
||||
assert.equal(data[1], 'b')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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})`
|
||||
}
|
||||
|
|
|
@ -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})`
|
||||
}
|
||||
|
|
|
@ -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<Env, any> = 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<string, MastodonAccount> = 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 })
|
||||
}
|
||||
|
|
|
@ -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<Env, any> = 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: {
|
||||
|
|
Ładowanie…
Reference in New Issue