Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: /api/v1/instance #363

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
414e003
Update main.tf
DataDrivenMD Feb 9, 2023
f3a5574
yolo
DataDrivenMD Feb 9, 2023
c443959
Applying user handle patch
DataDrivenMD Feb 10, 2023
b6b93b7
Allow TF to overwrite wildebeest CNAME
DataDrivenMD Feb 10, 2023
6d0a2da
Merge remote-tracking branch 'upstream/main'
DataDrivenMD Feb 10, 2023
9b90066
Merge branch 'cloudflare:main' into main
DataDrivenMD Feb 10, 2023
e07b1ed
Merge pull request #1 from Distal-Labs/fix-missing-apps-verify_creden…
DataDrivenMD Feb 11, 2023
3409a3e
Define MastodonInstance types
DataDrivenMD Feb 11, 2023
629ce0c
Return Mastodon-compliant instance info
DataDrivenMD Feb 11, 2023
14c3289
Merge remote-tracking branch 'upstream/main' into fix-incompatible-in…
DataDrivenMD Feb 11, 2023
ec9aba6
Merge pull request #2 from Distal-Labs/fix-incompatible-instance-endp…
DataDrivenMD Feb 11, 2023
b83e79a
Merge branch 'fix-missing-apps-verify_credentials-endpoint'
DataDrivenMD Feb 11, 2023
749429c
Merge branch 'cloudflare:main' into main
DataDrivenMD Feb 13, 2023
d71599c
Change Default Images to Official Mastodon Avatar
DataDrivenMD Feb 13, 2023
60802a4
Merge pull request #3 from Distal-Labs/fix-use-official-default-avatar
DataDrivenMD Feb 13, 2023
bfbbd05
Merge remote-tracking branch 'upstream/main'
DataDrivenMD Feb 24, 2023
ddf04ef
Merge remote-tracking branch 'upstream/main'
DataDrivenMD Feb 25, 2023
ef45841
Merge branch 'cloudflare:main' into main
DataDrivenMD Mar 1, 2023
2887e3a
Merge branch 'cloudflare:main' into main
DataDrivenMD Mar 1, 2023
8fb9b9b
Tiny refactor of existing Mastodon instance tests
DataDrivenMD Mar 2, 2023
2e30352
/api/v1/instance endpoint + getAdmins() test
DataDrivenMD Mar 2, 2023
0f2d29b
Adding test for instance v1 statistics
DataDrivenMD Mar 2, 2023
199b267
Merge remote-tracking branch 'upstream/main'
DataDrivenMD Mar 2, 2023
9a94e00
Linting
DataDrivenMD Mar 2, 2023
4ef9d98
More linting
DataDrivenMD Mar 2, 2023
0776114
Merge branch 'main' into api/v1/instance
DataDrivenMD Mar 2, 2023
1a38165
Revert "Merge branch 'main' into api/v1/instance"
DataDrivenMD Mar 2, 2023
52a4eab
Merge branch 'cloudflare:main' into api/v1/instance
DataDrivenMD Mar 3, 2023
af4f8a8
Merge branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 6, 2023
baa28d5
Merge remote-tracking branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 6, 2023
ea5f3fa
Pass e2e tests
DataDrivenMD Mar 7, 2023
3283c55
Making requested changes
DataDrivenMD Mar 7, 2023
4a698bd
Merge remote-tracking branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 7, 2023
79c3b61
Reverting breaking change to `getVersion()`
DataDrivenMD Mar 8, 2023
35ed2d1
Prettier
DataDrivenMD Mar 8, 2023
a8eb077
Requested changes + bind DB for e2e tests
DataDrivenMD Mar 9, 2023
1685c6a
Merge remote-tracking branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 9, 2023
9b9863f
Merge remote-tracking branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 9, 2023
4095db5
Merge remote-tracking branch 'upstream/main' into api/v1/instance
DataDrivenMD Mar 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/src/mastodon/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
}
28 changes: 28 additions & 0 deletions backend/src/mastodon/sql/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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
;
`
}
67 changes: 67 additions & 0 deletions backend/src/types/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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
}
77 changes: 0 additions & 77 deletions backend/test/mastodon.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
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'
Expand All @@ -14,80 +11,6 @@ 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()
Expand Down
158 changes: 144 additions & 14 deletions backend/test/mastodon/instance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,152 @@
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 } from '../utils'
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 = '[email protected]'
const domain = 'cloudflare.com'

describe('Mastodon APIs', () => {
describe('instance', () => {
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('/v1', () => {
describe('/instance', () => {
const env = {
INSTANCE_TITLE: 'a',
ADMIN_EMAIL: admin_email,
INSTANCE_DESCR: 'c',
} as Env

test('return the correct instance admin', 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.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, '[email protected]')
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'))
}
})
})
})
})
2 changes: 1 addition & 1 deletion config/ua.ts
Original file line number Diff line number Diff line change
@@ -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}; +${domain})`
return `Wildebeest/${WILDEBEEST_VERSION} (Mastodon/${MASTODON_API_VERSION} compatible; +https://${domain})`
}
4 changes: 0 additions & 4 deletions config/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,3 @@ 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})`
}
Loading