diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95260db..24d4cb7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -97,8 +97,12 @@ jobs: - name: retrieve D1 database uses: cloudflare/wrangler-action@2.0.0 with: - command: d1 list | grep "wildebeest-${{ env.NAME_SUFFIX }}\s" | awk '{print "d1_id="$2}' >> $GITHUB_ENV + command: d1 list --json | jq -r '.[] | select(.name == "wildebeest-${{ env.NAME_SUFFIX }}") | .uuid' | awk '{print "d1_id="$1}' >> $GITHUB_ENV apiToken: ${{ secrets.CF_API_TOKEN }} + preCommands: | + echo "*** pre commands ***" + apt-get update && apt-get -y install jq + echo "******" env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0735c89 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Reporting Security Vulnerabilities + +Please see [this page](https://www.cloudflare.com/.well-known/security.txt) for information on how to report a vulnerability to Cloudflare. Thanks! diff --git a/backend/src/accounts/getAccount.ts b/backend/src/accounts/getAccount.ts index 5f3f630..304ba8a 100644 --- a/backend/src/accounts/getAccount.ts +++ b/backend/src/accounts/getAccount.ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/accounts/#get +import { type Database } from 'wildebeest/backend/src/database' import { actorURL, getActorById } from 'wildebeest/backend/src/activitypub/actors' import { parseHandle } from 'wildebeest/backend/src/utils/parse' import type { Handle } from 'wildebeest/backend/src/utils/parse' @@ -8,7 +9,7 @@ import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebees import { MastodonAccount } from '../types' import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain' -export async function getAccount(domain: string, accountId: string, db: D1Database): Promise { +export async function getAccount(domain: string, accountId: string, db: Database): Promise { const handle = parseHandle(accountId) if (handle.domain === null || (handle.domain !== null && handle.domain === domain)) { @@ -17,17 +18,17 @@ export async function getAccount(domain: string, accountId: string, db: D1Databa } else if (handle.domain !== null) { // Retrieve the statuses of a remote actor const acct = `${handle.localPart}@${handle.domain}` - return getRemoteAccount(handle, acct) + return getRemoteAccount(handle, acct, db) } else { return null } } -async function getRemoteAccount(handle: Handle, acct: string): Promise { +async function getRemoteAccount(handle: Handle, acct: string, db: D1Database): Promise { // TODO: using webfinger isn't the optimal implementation. We could cache // the object in D1 and directly query the remote API, indicated by the actor's // url field. For now, let's keep it simple. - const actor = await queryAcct(handle.domain!, acct) + const actor = await queryAcct(handle.domain!, db, acct) if (actor === null) { return null } @@ -35,7 +36,7 @@ async function getRemoteAccount(handle: Handle, acct: string): Promise { +async function getLocalAccount(domain: string, db: Database, handle: Handle): Promise { const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart) const actor = await getActorById(db, actorId) diff --git a/backend/src/activitypub/activities/handle.ts b/backend/src/activitypub/activities/handle.ts index 2bcd807..c2841fd 100644 --- a/backend/src/activitypub/activities/handle.ts +++ b/backend/src/activitypub/activities/handle.ts @@ -27,6 +27,7 @@ import type { Activity } from 'wildebeest/backend/src/activitypub/activities' import { originalActorIdSymbol, deleteObject } from 'wildebeest/backend/src/activitypub/objects' import { hasReblog } from 'wildebeest/backend/src/mastodon/reblog' import { getMetadata, loadItems } from 'wildebeest/backend/src/activitypub/objects/collection' +import { type Database } from 'wildebeest/backend/src/database' function extractID(domain: string, s: string | URL): string { return s.toString().replace(`https://${domain}/ap/users/`, '') @@ -87,7 +88,7 @@ export function makeGetActorAsId(activity: Activity) { export async function handle( domain: string, activity: Activity, - db: D1Database, + db: Database, userKEK: string, adminEmail: string, vapidKeys: JWK @@ -115,7 +116,7 @@ export async function handle( } // check current object - const object = await objects.getObjectBy(db, 'original_object_id', objectId.toString()) + const object = await objects.getObjectBy(db, objects.ObjectByKey.originalObjectId, objectId.toString()) if (object === null) { throw new Error(`object ${objectId} does not exist`) } @@ -423,7 +424,7 @@ export async function handle( async function cacheObject( domain: string, obj: APObject, - db: D1Database, + db: Database, originalActorId: URL, originalObjectId: URL ): Promise<{ created: boolean; object: APObject } | null> { diff --git a/backend/src/activitypub/actors/follow.ts b/backend/src/activitypub/actors/follow.ts index 29ab0c3..c704d7f 100644 --- a/backend/src/activitypub/actors/follow.ts +++ b/backend/src/activitypub/actors/follow.ts @@ -2,6 +2,7 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import * as actors from 'wildebeest/backend/src/activitypub/actors' import type { OrderedCollection } from 'wildebeest/backend/src/activitypub/objects/collection' import { getMetadata, loadItems } from 'wildebeest/backend/src/activitypub/objects/collection' +import { type Database } from 'wildebeest/backend/src/database' export async function countFollowing(actor: Actor): Promise { const collection = await getMetadata(actor.following) @@ -25,7 +26,7 @@ export async function getFollowing(actor: Actor): Promise): Promise> { +export async function loadActors(db: Database, collection: OrderedCollection): Promise> { const promises = collection.items.map((item) => { const actorId = new URL(item) return actors.getAndCache(actorId, db) diff --git a/backend/src/activitypub/actors/inbox.ts b/backend/src/activitypub/actors/inbox.ts index 7ffd64d..98e1247 100644 --- a/backend/src/activitypub/actors/inbox.ts +++ b/backend/src/activitypub/actors/inbox.ts @@ -1,7 +1,8 @@ import type { APObject } from 'wildebeest/backend/src/activitypub/objects' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { type Database } from 'wildebeest/backend/src/database' -export async function addObjectInInbox(db: D1Database, actor: Actor, obj: APObject) { +export async function addObjectInInbox(db: Database, actor: Actor, obj: APObject) { const id = crypto.randomUUID() const out = await db .prepare('INSERT INTO inbox_objects(id, actor_id, object_id) VALUES(?, ?, ?)') diff --git a/backend/src/activitypub/actors/index.ts b/backend/src/activitypub/actors/index.ts index e88e11d..7156827 100644 --- a/backend/src/activitypub/actors/index.ts +++ b/backend/src/activitypub/actors/index.ts @@ -2,6 +2,7 @@ import { defaultImages } from 'wildebeest/config/accounts' import { generateUserKey } from 'wildebeest/backend/src/utils/key-ops' import { type APObject, sanitizeContent, getTextContent } from '../objects' import { addPeer } from 'wildebeest/backend/src/activitypub/peers' +import { type Database } from 'wildebeest/backend/src/database' const PERSON = 'Person' const isTesting = typeof jest !== 'undefined' @@ -43,39 +44,48 @@ export async function get(url: string | URL): Promise { const data = await res.json() const actor: Actor = { ...data } - actor.id = new URL(data.id) + actor.id = new URL(actor.id) - if (data.content) { - actor.content = await sanitizeContent(data.content) + if (actor.summary) { + actor.summary = await sanitizeContent(actor.summary) + if (actor.summary.length > 500) { + actor.summary = actor.summary.substring(0, 500) + } } - if (data.name) { - actor.name = await getTextContent(data.name) + if (actor.name) { + actor.name = await getTextContent(actor.name) + if (actor.name.length > 30) { + actor.name = actor.name.substring(0, 30) + } } - if (data.preferredUsername) { - actor.preferredUsername = await getTextContent(data.preferredUsername) + if (actor.preferredUsername) { + actor.preferredUsername = await getTextContent(actor.preferredUsername) + if (actor.preferredUsername.length > 30) { + actor.preferredUsername = actor.preferredUsername.substring(0, 30) + } } // This is mostly for testing where for convenience not all values // are provided. // TODO: eventually clean that to better match production. - if (data.inbox !== undefined) { - actor.inbox = new URL(data.inbox) + if (actor.inbox !== undefined) { + actor.inbox = new URL(actor.inbox) } - if (data.following !== undefined) { - actor.following = new URL(data.following) + if (actor.following !== undefined) { + actor.following = new URL(actor.following) } - if (data.followers !== undefined) { - actor.followers = new URL(data.followers) + if (actor.followers !== undefined) { + actor.followers = new URL(actor.followers) } - if (data.outbox !== undefined) { - actor.outbox = new URL(data.outbox) + if (actor.outbox !== undefined) { + actor.outbox = new URL(actor.outbox) } return actor } // Get and cache the Actor locally -export async function getAndCache(url: URL, db: D1Database): Promise { +export async function getAndCache(url: URL, db: Database): Promise { { const actor = await getActorById(db, url) if (actor !== null) { @@ -111,7 +121,7 @@ export async function getAndCache(url: URL, db: D1Database): Promise { return actor } -export async function getPersonByEmail(db: D1Database, email: string): Promise { +export async function getPersonByEmail(db: Database, email: string): Promise { const stmt = db.prepare('SELECT * FROM actors WHERE email=? AND type=?').bind(email, PERSON) const { results } = await stmt.all() if (!results || results.length === 0) { @@ -137,7 +147,7 @@ type PersonProperties = { // Create a local user export async function createPerson( domain: string, - db: D1Database, + db: Database, userKEK: string, email: string, properties: PersonProperties = {} @@ -199,7 +209,7 @@ export async function createPerson( return personFromRow(row) } -export async function updateActorProperty(db: D1Database, actorId: URL, key: string, value: string) { +export async function updateActorProperty(db: Database, actorId: URL, key: string, value: string) { const { success, error } = await db .prepare(`UPDATE actors SET properties=json_set(properties, '$.${key}', ?) WHERE id=?`) .bind(value, actorId.toString()) @@ -209,7 +219,7 @@ export async function updateActorProperty(db: D1Database, actorId: URL, key: str } } -export async function setActorAlias(db: D1Database, actorId: URL, alias: URL) { +export async function setActorAlias(db: Database, actorId: URL, alias: URL) { const { success, error } = await db .prepare(`UPDATE actors SET properties=json_set(properties, '$.alsoKnownAs', json_array(?)) WHERE id=?`) .bind(alias.toString(), actorId.toString()) @@ -219,7 +229,7 @@ export async function setActorAlias(db: D1Database, actorId: URL, alias: URL) { } } -export async function getActorById(db: D1Database, id: URL): Promise { +export async function getActorById(db: Database, id: URL): Promise { const stmt = db.prepare('SELECT * FROM actors WHERE id=?').bind(id.toString()) const { results } = await stmt.all() if (!results || results.length === 0) { diff --git a/backend/src/activitypub/actors/outbox.ts b/backend/src/activitypub/actors/outbox.ts index b8af9c7..0c50aae 100644 --- a/backend/src/activitypub/actors/outbox.ts +++ b/backend/src/activitypub/actors/outbox.ts @@ -4,9 +4,10 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import type { OrderedCollection } from 'wildebeest/backend/src/activitypub/objects/collection' import { getMetadata, loadItems } from 'wildebeest/backend/src/activitypub/objects/collection' import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities' +import { type Database } from 'wildebeest/backend/src/database' export async function addObjectInOutbox( - db: D1Database, + db: Database, actor: Actor, obj: APObject, published_date?: string, diff --git a/backend/src/activitypub/deliver.ts b/backend/src/activitypub/deliver.ts index 45f273c..1d0c183 100644 --- a/backend/src/activitypub/deliver.ts +++ b/backend/src/activitypub/deliver.ts @@ -8,6 +8,7 @@ import { generateDigestHeader } from 'wildebeest/backend/src/utils/http-signing- import { signRequest } from 'wildebeest/backend/src/utils/http-signing' import { getFollowers } from 'wildebeest/backend/src/mastodon/follow' import { getFederationUA } from 'wildebeest/config/ua' +import { type Database } from 'wildebeest/backend/src/database' const MAX_BATCH_SIZE = 100 @@ -46,7 +47,7 @@ export async function deliverToActor( // to a collection (followers) and the worker creates the indivual messages. More // reliable and scalable. export async function deliverFollowers( - db: D1Database, + db: Database, userKEK: string, from: Actor, activity: Activity, diff --git a/backend/src/activitypub/objects/image.ts b/backend/src/activitypub/objects/image.ts index 0fb1df1..17566a4 100644 --- a/backend/src/activitypub/objects/image.ts +++ b/backend/src/activitypub/objects/image.ts @@ -1,5 +1,6 @@ import * as objects from '.' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { type Database } from 'wildebeest/backend/src/database' export const IMAGE = 'Image' @@ -8,7 +9,7 @@ export interface Image extends objects.Document { description?: string } -export async function createImage(domain: string, db: D1Database, actor: Actor, properties: any): Promise { +export async function createImage(domain: string, db: Database, actor: Actor, properties: any): Promise { const actorId = new URL(actor.id) return (await objects.createObject(domain, db, IMAGE, properties, actorId, true)) as Image } diff --git a/backend/src/activitypub/objects/index.ts b/backend/src/activitypub/objects/index.ts index 6c6ac87..9a1be6c 100644 --- a/backend/src/activitypub/objects/index.ts +++ b/backend/src/activitypub/objects/index.ts @@ -1,5 +1,6 @@ import type { UUID } from 'wildebeest/backend/src/types' import { addPeer } from 'wildebeest/backend/src/activitypub/peers' +import { type Database } from 'wildebeest/backend/src/database' export const originalActorIdSymbol = Symbol() export const originalObjectIdSymbol = Symbol() @@ -39,7 +40,7 @@ export function uri(domain: string, id: string): URL { export async function createObject( domain: string, - db: D1Database, + db: Database, type: string, properties: any, originalActorId: URL, @@ -86,7 +87,7 @@ type CacheObjectRes = { export async function cacheObject( domain: string, - db: D1Database, + db: Database, properties: unknown, originalActorId: URL, originalObjectId: URL, @@ -94,7 +95,7 @@ export async function cacheObject( ): Promise { const sanitizedProperties = await sanitizeObjectProperties(properties) - const cachedObject = await getObjectBy(db, 'original_object_id', originalObjectId.toString()) + const cachedObject = await getObjectBy(db, ObjectByKey.originalObjectId, originalObjectId.toString()) if (cachedObject !== null) { return { created: false, @@ -144,7 +145,7 @@ export async function cacheObject( } } -export async function updateObject(db: D1Database, properties: any, id: URL): Promise { +export async function updateObject(db: Database, properties: any, id: URL): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars const res: any = await db .prepare('UPDATE objects SET properties = ? WHERE id = ?') @@ -156,7 +157,7 @@ export async function updateObject(db: D1Database, properties: any, id: URL): Pr return true } -export async function updateObjectProperty(db: D1Database, obj: APObject, key: string, value: string) { +export async function updateObjectProperty(db: Database, obj: APObject, key: string, value: string) { const { success, error } = await db .prepare(`UPDATE objects SET properties=json_set(properties, '$.${key}', ?) WHERE id=?`) .bind(value, obj.id.toString()) @@ -166,24 +167,35 @@ export async function updateObjectProperty(db: D1Database, obj: APObject, key: s } } -export async function getObjectById(db: D1Database, id: string | URL): Promise { - return getObjectBy(db, 'id', id.toString()) +export async function getObjectById(db: Database, id: string | URL): Promise { + return getObjectBy(db, ObjectByKey.id, id.toString()) } -export async function getObjectByOriginalId(db: D1Database, id: string | URL): Promise { - return getObjectBy(db, 'original_object_id', id.toString()) +export async function getObjectByOriginalId(db: Database, id: string | URL): Promise { + return getObjectBy(db, ObjectByKey.originalObjectId, id.toString()) } -export async function getObjectByMastodonId(db: D1Database, id: UUID): Promise { - return getObjectBy(db, 'mastodon_id', id) +export async function getObjectByMastodonId(db: Database, id: UUID): Promise { + return getObjectBy(db, ObjectByKey.mastodonId, id) } -export async function getObjectBy(db: D1Database, key: string, value: string) { +export enum ObjectByKey { + id = 'id', + originalObjectId = 'original_object_id', + mastodonId = 'mastodon_id', +} + +const allowedObjectByKeysSet = new Set(Object.values(ObjectByKey)) + +export async function getObjectBy(db: Database, key: ObjectByKey, value: string) { + if (!allowedObjectByKeysSet.has(key)) { + throw new Error('getObjectBy run with invalid key: ' + key) + } const query = ` -SELECT * -FROM objects -WHERE objects.${key}=? - ` + SELECT * + FROM objects + WHERE objects.${key}=? + ` const { results, success, error } = await db.prepare(query).bind(value).all() if (!success) { throw new Error('SQL error: ' + error) @@ -289,7 +301,7 @@ function getTextContentRewriter() { // TODO: eventually use SQLite's `ON DELETE CASCADE` but requires writing the DB // schema directly into D1, which D1 disallows at the moment. // Some context at: https://stackoverflow.com/questions/13150075/add-on-delete-cascade-behavior-to-an-sqlite3-table-after-it-has-been-created -export async function deleteObject(db: D1Database, note: T) { +export async function deleteObject(db: Database, note: T) { const nodeId = note.id.toString() const batch = [ db.prepare('DELETE FROM outbox_objects WHERE object_id=?').bind(nodeId), diff --git a/backend/src/activitypub/objects/note.ts b/backend/src/activitypub/objects/note.ts index 0fc92b0..02ebdf6 100644 --- a/backend/src/activitypub/objects/note.ts +++ b/backend/src/activitypub/objects/note.ts @@ -4,6 +4,7 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import type { Link } from 'wildebeest/backend/src/activitypub/objects/link' import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities' import * as objects from '.' +import { type Database } from 'wildebeest/backend/src/database' const NOTE = 'Note' @@ -23,7 +24,7 @@ export interface Note extends objects.APObject { export async function createPublicNote( domain: string, - db: D1Database, + db: Database, content: string, actor: Actor, attachments: Array = [], @@ -51,12 +52,12 @@ export async function createPublicNote( return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note } -export async function createPrivateNote( +export async function createDirectNote( domain: string, - db: D1Database, + db: Database, content: string, actor: Actor, - targetActor: Actor, + targetActors: Array, attachment: Array = [], extraProperties: any = {} ): Promise { @@ -65,7 +66,7 @@ export async function createPrivateNote( const properties = { attributedTo: actorId, content, - to: [targetActor.id.toString()], + to: targetActors.map((a) => a.id.toString()), cc: [], // FIXME: stub values diff --git a/backend/src/activitypub/peers.ts b/backend/src/activitypub/peers.ts index 44c6433..3f05220 100644 --- a/backend/src/activitypub/peers.ts +++ b/backend/src/activitypub/peers.ts @@ -1,13 +1,14 @@ import { getResultsField } from 'wildebeest/backend/src/mastodon/utils' +import { type Database } from 'wildebeest/backend/src/database' -export async function getPeers(db: D1Database): Promise> { +export async function getPeers(db: Database): Promise> { const query = `SELECT domain FROM peers ` const statement = db.prepare(query) return getResultsField(statement, 'domain') } -export async function addPeer(db: D1Database, domain: string): Promise { +export async function addPeer(db: Database, domain: string): Promise { const query = ` INSERT OR IGNORE INTO peers (domain) VALUES (?) diff --git a/backend/src/database/d1.ts b/backend/src/database/d1.ts new file mode 100644 index 0000000..576bdce --- /dev/null +++ b/backend/src/database/d1.ts @@ -0,0 +1,6 @@ +import { type Database } from 'wildebeest/backend/src/database' +import type { Env } from 'wildebeest/backend/src/types/env' + +export default function make(env: Env): Database { + return env.DATABASE +} diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts new file mode 100644 index 0000000..ca8fbe4 --- /dev/null +++ b/backend/src/database/index.ts @@ -0,0 +1,28 @@ +import type { Env } from 'wildebeest/backend/src/types/env' +import d1 from './d1' + +export interface Result { + results?: T[] + success: boolean + error?: string + meta: any +} + +export interface Database { + prepare(query: string): PreparedStatement + dump(): Promise + batch(statements: PreparedStatement[]): Promise[]> + exec(query: string): Promise> +} + +export interface PreparedStatement { + bind(...values: any[]): PreparedStatement + first(colName?: string): Promise + run(): Promise> + all(): Promise> + raw(): Promise +} + +export function getDatabase(env: Env): Database { + return d1(env) +} diff --git a/backend/src/errors/index.ts b/backend/src/errors/index.ts index c875fd3..07fe9af 100644 --- a/backend/src/errors/index.ts +++ b/backend/src/errors/index.ts @@ -7,7 +7,7 @@ type ErrorResponse = { const headers = { ...cors(), - 'content-type': 'application/json', + 'content-type': 'application/json; charset=utf-8', } as const function generateErrorResponse(error: string, status: number, errorDescription?: string): Response { @@ -65,3 +65,7 @@ export function exceededLimit(detail: string): Response { export function resourceNotFound(name: string, id: string): Response { return generateErrorResponse('Resource not found', 404, `${name} "${id}" not found`) } + +export function validationError(detail: string): Response { + return generateErrorResponse('Validation failed', 422, detail) +} diff --git a/backend/src/mastodon/account.ts b/backend/src/mastodon/account.ts index a1961b3..7c81612 100644 --- a/backend/src/mastodon/account.ts +++ b/backend/src/mastodon/account.ts @@ -4,6 +4,7 @@ import { Actor } from '../activitypub/actors' import { defaultImages } from 'wildebeest/config/accounts' import * as apOutbox from 'wildebeest/backend/src/activitypub/actors/outbox' import * as apFollow from 'wildebeest/backend/src/activitypub/actors/follow' +import { type Database } from 'wildebeest/backend/src/database' function toMastodonAccount(acct: string, res: Actor): MastodonAccount { const avatar = res.icon?.url.toString() ?? defaultImages.avatar @@ -55,7 +56,7 @@ export async function loadExternalMastodonAccount( } // Load a local user and return it as a MastodonAccount -export async function loadLocalMastodonAccount(db: D1Database, res: Actor): Promise { +export async function loadLocalMastodonAccount(db: Database, res: Actor): Promise { const query = ` SELECT (SELECT count(*) @@ -85,7 +86,7 @@ SELECT return account } -export async function getSigningKey(instanceKey: string, db: D1Database, actor: Actor): Promise { +export async function getSigningKey(instanceKey: string, db: Database, actor: Actor): Promise { const stmt = db.prepare('SELECT privkey, privkey_salt FROM actors WHERE id=?').bind(actor.id.toString()) const { privkey, privkey_salt } = (await stmt.first()) as any return unwrapPrivateKey(instanceKey, new Uint8Array(privkey), new Uint8Array(privkey_salt)) diff --git a/backend/src/mastodon/client.ts b/backend/src/mastodon/client.ts index 180e4c5..2c2df3d 100644 --- a/backend/src/mastodon/client.ts +++ b/backend/src/mastodon/client.ts @@ -1,4 +1,5 @@ import { arrayBufferToBase64 } from 'wildebeest/backend/src/utils/key-ops' +import { type Database } from 'wildebeest/backend/src/database' export interface Client { id: string @@ -10,7 +11,7 @@ export interface Client { } export async function createClient( - db: D1Database, + db: Database, name: string, redirect_uris: string, website: string, @@ -42,7 +43,7 @@ export async function createClient( } } -export async function getClientById(db: D1Database, id: string): Promise { +export async function getClientById(db: Database, id: string): Promise { const stmt = db.prepare('SELECT * FROM clients WHERE id=?').bind(id) const { results } = await stmt.all() if (!results || results.length === 0) { diff --git a/backend/src/mastodon/follow.ts b/backend/src/mastodon/follow.ts index 16ef42e..49f0552 100644 --- a/backend/src/mastodon/follow.ts +++ b/backend/src/mastodon/follow.ts @@ -2,12 +2,13 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { urlToHandle } from 'wildebeest/backend/src/utils/handle' import { getResultsField } from './utils' +import { type Database } from 'wildebeest/backend/src/database' const STATE_PENDING = 'pending' const STATE_ACCEPTED = 'accepted' // During a migration we move the followers from the old Actor to the new -export async function moveFollowers(db: D1Database, actor: Actor, followers: Array): Promise { +export async function moveFollowers(db: Database, actor: Actor, followers: Array): Promise { const batch = [] const stmt = db.prepare(` INSERT OR IGNORE @@ -29,7 +30,7 @@ export async function moveFollowers(db: D1Database, actor: Actor, followers: Arr await db.batch(batch) } -export async function moveFollowing(db: D1Database, actor: Actor, followingActors: Array): Promise { +export async function moveFollowing(db: Database, actor: Actor, followingActors: Array): Promise { const batch = [] const stmt = db.prepare(` INSERT OR IGNORE @@ -52,7 +53,7 @@ export async function moveFollowing(db: D1Database, actor: Actor, followingActor } // Add a pending following -export async function addFollowing(db: D1Database, actor: Actor, target: Actor, targetAcct: string): Promise { +export async function addFollowing(db: Database, actor: Actor, target: Actor, targetAcct: string): Promise { const id = crypto.randomUUID() const query = ` @@ -71,7 +72,7 @@ export async function addFollowing(db: D1Database, actor: Actor, target: Actor, } // Accept the pending following request -export async function acceptFollowing(db: D1Database, actor: Actor, target: Actor) { +export async function acceptFollowing(db: Database, actor: Actor, target: Actor) { const query = ` UPDATE actor_following SET state=? WHERE actor_id=? AND target_actor_id=? AND state=? ` @@ -85,7 +86,7 @@ export async function acceptFollowing(db: D1Database, actor: Actor, target: Acto } } -export async function removeFollowing(db: D1Database, actor: Actor, target: Actor) { +export async function removeFollowing(db: Database, actor: Actor, target: Actor) { const query = ` DELETE FROM actor_following WHERE actor_id=? AND target_actor_id=? ` @@ -96,7 +97,7 @@ export async function removeFollowing(db: D1Database, actor: Actor, target: Acto } } -export function getFollowingAcct(db: D1Database, actor: Actor): Promise> { +export function getFollowingAcct(db: Database, actor: Actor): Promise> { const query = ` SELECT target_actor_acct FROM actor_following WHERE actor_id=? AND state=? ` @@ -105,7 +106,7 @@ export function getFollowingAcct(db: D1Database, actor: Actor): Promise> { +export function getFollowingRequestedAcct(db: Database, actor: Actor): Promise> { const query = ` SELECT target_actor_acct FROM actor_following WHERE actor_id=? AND state=? ` @@ -115,7 +116,7 @@ export function getFollowingRequestedAcct(db: D1Database, actor: Actor): Promise return getResultsField(statement, 'target_actor_acct') } -export function getFollowingId(db: D1Database, actor: Actor): Promise> { +export function getFollowingId(db: Database, actor: Actor): Promise> { const query = ` SELECT target_actor_id FROM actor_following WHERE actor_id=? AND state=? ` @@ -125,7 +126,7 @@ export function getFollowingId(db: D1Database, actor: Actor): Promise> { +export function getFollowers(db: Database, actor: Actor): Promise> { const query = ` SELECT actor_id FROM actor_following WHERE target_actor_id=? AND state=? ` diff --git a/backend/src/mastodon/hashtag.ts b/backend/src/mastodon/hashtag.ts index 5f8aff0..aace161 100644 --- a/backend/src/mastodon/hashtag.ts +++ b/backend/src/mastodon/hashtag.ts @@ -1,5 +1,6 @@ import type { Note } from 'wildebeest/backend/src/activitypub/objects/note' import type { Tag } from 'wildebeest/backend/src/types/tag' +import { type Database } from 'wildebeest/backend/src/database' export type Hashtag = string @@ -14,7 +15,7 @@ export function getHashtags(input: string): Array { return [...matches].map((match) => match[1]) } -export async function insertHashtags(db: D1Database, note: Note, values: Array): Promise { +export async function insertHashtags(db: Database, note: Note, values: Array): Promise { const queries = [] const stmt = db.prepare(` INSERT INTO note_hashtags (value, object_id) @@ -29,7 +30,7 @@ export async function insertHashtags(db: D1Database, note: Note, values: Array { +export async function getTag(db: Database, domain: string, tag: string): Promise { const query = ` SELECT * FROM note_hashtags WHERE value=? ` diff --git a/backend/src/mastodon/idempotency.ts b/backend/src/mastodon/idempotency.ts index efa59b7..f5c1bee 100644 --- a/backend/src/mastodon/idempotency.ts +++ b/backend/src/mastodon/idempotency.ts @@ -1,11 +1,12 @@ import type { APObject } from 'wildebeest/backend/src/activitypub/objects' +import { type Database } from 'wildebeest/backend/src/database' import { mastodonIdSymbol, originalActorIdSymbol, originalObjectIdSymbol, } from 'wildebeest/backend/src/activitypub/objects' -export async function insertKey(db: D1Database, key: string, obj: APObject): Promise { +export async function insertKey(db: Database, key: string, obj: APObject): Promise { const query = ` INSERT INTO idempotency_keys (key, object_id, expires_at) VALUES (?1, ?2, datetime('now', '+1 hour')) @@ -17,7 +18,7 @@ export async function insertKey(db: D1Database, key: string, obj: APObject): Pro } } -export async function hasKey(db: D1Database, key: string): Promise { +export async function hasKey(db: Database, key: string): Promise { const query = ` SELECT objects.* FROM idempotency_keys diff --git a/backend/src/mastodon/like.ts b/backend/src/mastodon/like.ts index eb68061..c4fbbf0 100644 --- a/backend/src/mastodon/like.ts +++ b/backend/src/mastodon/like.ts @@ -1,8 +1,9 @@ import type { APObject } from 'wildebeest/backend/src/activitypub/objects' +import { type Database } from 'wildebeest/backend/src/database' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import { getResultsField } from './utils' -export async function insertLike(db: D1Database, actor: Actor, obj: APObject) { +export async function insertLike(db: Database, actor: Actor, obj: APObject) { const id = crypto.randomUUID() const query = ` @@ -16,7 +17,7 @@ export async function insertLike(db: D1Database, actor: Actor, obj: APObject) { } } -export function getLikes(db: D1Database, obj: APObject): Promise> { +export function getLikes(db: Database, obj: APObject): Promise> { const query = ` SELECT actor_id FROM actor_favourites WHERE object_id=? ` diff --git a/backend/src/mastodon/notification.ts b/backend/src/mastodon/notification.ts index 5343775..8d9690a 100644 --- a/backend/src/mastodon/notification.ts +++ b/backend/src/mastodon/notification.ts @@ -1,4 +1,5 @@ import type { APObject } from 'wildebeest/backend/src/activitypub/objects' +import { type Database } from 'wildebeest/backend/src/database' import { defaultImages } from 'wildebeest/config/accounts' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import * as actors from 'wildebeest/backend/src/activitypub/actors' @@ -18,7 +19,7 @@ import { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/su import type { Cache } from 'wildebeest/backend/src/cache' export async function createNotification( - db: D1Database, + db: Database, type: NotificationType, actor: Actor, fromActor: Actor, @@ -36,7 +37,7 @@ export async function createNotification( return row.id } -export async function insertFollowNotification(db: D1Database, actor: Actor, fromActor: Actor): Promise { +export async function insertFollowNotification(db: Database, actor: Actor, fromActor: Actor): Promise { const type: NotificationType = 'follow' const query = ` @@ -49,7 +50,7 @@ export async function insertFollowNotification(db: D1Database, actor: Actor, fro } export async function sendFollowNotification( - db: D1Database, + db: Database, follower: Actor, actor: Actor, notificationId: string, @@ -81,7 +82,7 @@ export async function sendFollowNotification( } export async function sendLikeNotification( - db: D1Database, + db: Database, fromActor: Actor, actor: Actor, notificationId: string, @@ -113,7 +114,7 @@ export async function sendLikeNotification( } export async function sendMentionNotification( - db: D1Database, + db: Database, fromActor: Actor, actor: Actor, notificationId: string, @@ -145,7 +146,7 @@ export async function sendMentionNotification( } export async function sendReblogNotification( - db: D1Database, + db: Database, fromActor: Actor, actor: Actor, notificationId: string, @@ -176,7 +177,7 @@ export async function sendReblogNotification( return sendNotification(db, actor, message, vapidKeys) } -async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage, vapidKeys: JWK) { +async function sendNotification(db: Database, actor: Actor, message: WebPushMessage, vapidKeys: JWK) { const subscriptions = await getSubscriptionForAllClients(db, actor) const promises = subscriptions.map(async (subscription) => { @@ -195,7 +196,7 @@ async function sendNotification(db: D1Database, actor: Actor, message: WebPushMe await Promise.allSettled(promises) } -export async function getNotifications(db: D1Database, actor: Actor, domain: string): Promise> { +export async function getNotifications(db: Database, actor: Actor, domain: string): Promise> { const query = ` SELECT objects.*, @@ -278,7 +279,7 @@ export async function getNotifications(db: D1Database, actor: Actor, domain: str return out } -export async function pregenerateNotifications(db: D1Database, cache: Cache, actor: Actor, domain: string) { +export async function pregenerateNotifications(db: Database, cache: Cache, actor: Actor, domain: string) { const notifications = await getNotifications(db, actor, domain) await cache.put(actor.id + '/notifications', notifications) } diff --git a/backend/src/mastodon/reblog.ts b/backend/src/mastodon/reblog.ts index 2170ad5..9e4d5cb 100644 --- a/backend/src/mastodon/reblog.ts +++ b/backend/src/mastodon/reblog.ts @@ -1,6 +1,7 @@ // Also known as boost. import type { APObject } from 'wildebeest/backend/src/activitypub/objects' +import { type Database } from 'wildebeest/backend/src/database' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import { getResultsField } from './utils' import { addObjectInOutbox } from '../activitypub/actors/outbox' @@ -8,15 +9,15 @@ import { addObjectInOutbox } from '../activitypub/actors/outbox' /** * Creates a reblog and inserts it in the reblog author's outbox * - * @param db D1Database + * @param db Database * @param actor Reblogger * @param obj ActivityPub object to reblog */ -export async function createReblog(db: D1Database, actor: Actor, obj: APObject) { +export async function createReblog(db: Database, actor: Actor, obj: APObject) { await Promise.all([addObjectInOutbox(db, actor, obj), insertReblog(db, actor, obj)]) } -export async function insertReblog(db: D1Database, actor: Actor, obj: APObject) { +export async function insertReblog(db: Database, actor: Actor, obj: APObject) { const id = crypto.randomUUID() const query = ` @@ -30,7 +31,7 @@ export async function insertReblog(db: D1Database, actor: Actor, obj: APObject) } } -export function getReblogs(db: D1Database, obj: APObject): Promise> { +export function getReblogs(db: Database, obj: APObject): Promise> { const query = ` SELECT actor_id FROM actor_reblogs WHERE object_id=? ` @@ -40,7 +41,7 @@ export function getReblogs(db: D1Database, obj: APObject): Promise return getResultsField(statement, 'actor_id') } -export async function hasReblog(db: D1Database, actor: Actor, obj: APObject): Promise { +export async function hasReblog(db: Database, actor: Actor, obj: APObject): Promise { const query = ` SELECT count(*) as count FROM actor_reblogs WHERE object_id=?1 AND actor_id=?2 ` diff --git a/backend/src/mastodon/reply.ts b/backend/src/mastodon/reply.ts index 1be7886..6004823 100644 --- a/backend/src/mastodon/reply.ts +++ b/backend/src/mastodon/reply.ts @@ -1,9 +1,10 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { type Database } from 'wildebeest/backend/src/database' import { toMastodonStatusFromRow } from './status' import type { APObject } from 'wildebeest/backend/src/activitypub/objects' import type { MastodonStatus } from 'wildebeest/backend/src/types/status' -export async function insertReply(db: D1Database, actor: Actor, obj: APObject, inReplyToObj: APObject) { +export async function insertReply(db: Database, actor: Actor, obj: APObject, inReplyToObj: APObject) { const id = crypto.randomUUID() const query = ` INSERT INTO actor_replies (id, actor_id, object_id, in_reply_to_object_id) @@ -18,7 +19,7 @@ export async function insertReply(db: D1Database, actor: Actor, obj: APObject, i } } -export async function getReplies(domain: string, db: D1Database, obj: APObject): Promise> { +export async function getReplies(domain: string, db: Database, obj: APObject): Promise> { const QUERY = ` SELECT objects.*, actors.id as actor_id, diff --git a/backend/src/mastodon/status.ts b/backend/src/mastodon/status.ts index 3242e0c..ccbc532 100644 --- a/backend/src/mastodon/status.ts +++ b/backend/src/mastodon/status.ts @@ -17,8 +17,9 @@ import type { Person } from 'wildebeest/backend/src/activitypub/actors' import { addObjectInOutbox } from '../activitypub/actors/outbox' import type { APObject } from 'wildebeest/backend/src/activitypub/objects' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' +import { type Database } from 'wildebeest/backend/src/database' -export async function getMentions(input: string, instanceDomain: string): Promise> { +export async function getMentions(input: string, instanceDomain: string, db: Database): Promise> { const mentions: Array = [] for (let i = 0, len = input.length; i < len; i++) { @@ -33,7 +34,7 @@ export async function getMentions(input: string, instanceDomain: string): Promis const handle = parseHandle(buffer) const domain = handle.domain ? handle.domain : instanceDomain const acct = `${handle.localPart}@${domain}` - const targetActor = await queryAcct(domain!, acct) + const targetActor = await queryAcct(domain!, db, acct) if (targetActor === null) { console.warn(`actor ${acct} not found`) continue @@ -46,7 +47,7 @@ export async function getMentions(input: string, instanceDomain: string): Promis } export async function toMastodonStatusFromObject( - db: D1Database, + db: Database, obj: Note, domain: string ): Promise { @@ -99,11 +100,7 @@ export async function toMastodonStatusFromObject( // toMastodonStatusFromRow makes assumption about what field are available on // the `row` object. This function is only used for timelines, which is optimized // SQL. Otherwise don't use this function. -export async function toMastodonStatusFromRow( - domain: string, - db: D1Database, - row: any -): Promise { +export async function toMastodonStatusFromRow(domain: string, db: Database, row: any): Promise { if (row.publisher_actor_id === undefined) { console.warn('missing `row.publisher_actor_id`') return null @@ -180,7 +177,7 @@ export async function toMastodonStatusFromRow( return status } -export async function getMastodonStatusById(db: D1Database, id: UUID, domain: string): Promise { +export async function getMastodonStatusById(db: Database, id: UUID, domain: string): Promise { const obj = await getObjectByMastodonId(db, id) if (obj === null) { return null @@ -192,7 +189,7 @@ export async function getMastodonStatusById(db: D1Database, id: UUID, domain: st * Creates a status object in the given actor's outbox. * * @param domain the domain to use - * @param db D1Database + * @param db Database * @param actor Author of the reply * @param content content of the reply * @param attachments optional attachments for the status @@ -201,7 +198,7 @@ export async function getMastodonStatusById(db: D1Database, id: UUID, domain: st */ export async function createStatus( domain: string, - db: D1Database, + db: Database, actor: Person, content: string, attachments?: APObject[], diff --git a/backend/src/mastodon/subscription.ts b/backend/src/mastodon/subscription.ts index a7abefd..b9ec8b9 100644 --- a/backend/src/mastodon/subscription.ts +++ b/backend/src/mastodon/subscription.ts @@ -2,6 +2,7 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import { b64ToUrlEncoded, exportPublicKeyPair } from 'wildebeest/backend/src/webpush/util' import { Client } from './client' +import { type Database } from 'wildebeest/backend/src/database' export type PushSubscription = { endpoint: string @@ -51,7 +52,7 @@ export type Subscription = { } export async function createSubscription( - db: D1Database, + db: Database, actor: Actor, client: Client, req: CreateRequest @@ -85,7 +86,7 @@ export async function createSubscription( return subscriptionFromRow(row) } -export async function getSubscription(db: D1Database, actor: Actor, client: Client): Promise { +export async function getSubscription(db: Database, actor: Actor, client: Client): Promise { const query = ` SELECT * FROM subscriptions WHERE actor_id=? AND client_id=? ` @@ -103,7 +104,7 @@ export async function getSubscription(db: D1Database, actor: Actor, client: Clie return subscriptionFromRow(row) } -export async function getSubscriptionForAllClients(db: D1Database, actor: Actor): Promise> { +export async function getSubscriptionForAllClients(db: Database, actor: Actor): Promise> { const query = ` SELECT * FROM subscriptions WHERE actor_id=? ORDER BY cdate DESC LIMIT 5 ` diff --git a/backend/src/mastodon/timeline.ts b/backend/src/mastodon/timeline.ts index 26f228f..57c945b 100644 --- a/backend/src/mastodon/timeline.ts +++ b/backend/src/mastodon/timeline.ts @@ -3,13 +3,14 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors/' import { toMastodonStatusFromRow } from './status' import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities' import type { Cache } from 'wildebeest/backend/src/cache' +import { type Database } from 'wildebeest/backend/src/database' -export async function pregenerateTimelines(domain: string, db: D1Database, cache: Cache, actor: Actor) { +export async function pregenerateTimelines(domain: string, db: Database, cache: Cache, actor: Actor) { const timeline = await getHomeTimeline(domain, db, actor) await cache.put(actor.id + '/timeline/home', timeline) } -export async function getHomeTimeline(domain: string, db: D1Database, actor: Actor): Promise> { +export async function getHomeTimeline(domain: string, db: Database, actor: Actor): Promise> { const { results: following } = await db .prepare( ` @@ -110,7 +111,7 @@ function localPreferenceQuery(preference: LocalPreference): string { export async function getPublicTimeline( domain: string, - db: D1Database, + db: Database, localPreference: LocalPreference, offset: number = 0, hashtag?: string diff --git a/backend/src/middleware/main.ts b/backend/src/middleware/main.ts index b77cd74..dd10e80 100644 --- a/backend/src/middleware/main.ts +++ b/backend/src/middleware/main.ts @@ -3,8 +3,9 @@ import * as actors from 'wildebeest/backend/src/activitypub/actors' import type { Env } from 'wildebeest/backend/src/types/env' import * as errors from 'wildebeest/backend/src/errors' import { cors } from 'wildebeest/backend/src/utils/cors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' -async function loadContextData(db: D1Database, clientId: string, email: string, ctx: any): Promise { +async function loadContextData(db: Database, clientId: string, email: string, ctx: any): Promise { const query = ` SELECT * FROM actors @@ -96,7 +97,7 @@ export async function main(context: EventContext) { // configuration, which are used to verify the JWT. // TODO: since we don't load the instance configuration anymore, we // don't need to load the user before anymore. - if (!(await loadContextData(context.env.DATABASE, clientId, payload.email, context))) { + if (!(await loadContextData(getDatabase(context.env), clientId, payload.email, context))) { return errors.notAuthorized('failed to load context data') } diff --git a/backend/src/types/env.ts b/backend/src/types/env.ts index f913309..d849c5e 100644 --- a/backend/src/types/env.ts +++ b/backend/src/types/env.ts @@ -1,7 +1,8 @@ import type { Queue, MessageBody } from 'wildebeest/backend/src/types/queue' +import { type Database } from 'wildebeest/backend/src/database' export interface Env { - DATABASE: D1Database + DATABASE: Database // FIXME: shouldn't it be USER_KEY? userKEK: string QUEUE: Queue diff --git a/backend/src/webfinger/index.ts b/backend/src/webfinger/index.ts index 8f65d9e..e0f8502 100644 --- a/backend/src/webfinger/index.ts +++ b/backend/src/webfinger/index.ts @@ -11,12 +11,12 @@ const headers = { accept: 'application/jrd+json', } -export async function queryAcct(domain: string, acct: string): Promise { +export async function queryAcct(domain: string, db: D1Database, acct: string): Promise { const url = await queryAcctLink(domain, acct) if (url === null) { return null } - return actors.get(url) + return actors.getAndCache(url, db) } export async function queryAcctLink(domain: string, acct: string): Promise { diff --git a/backend/test/activitypub.spec.ts b/backend/test/activitypub.spec.ts index 6a46173..435c6d4 100644 --- a/backend/test/activitypub.spec.ts +++ b/backend/test/activitypub.spec.ts @@ -3,7 +3,7 @@ import { MessageType } from 'wildebeest/backend/src/types/queue' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import { createPerson } from 'wildebeest/backend/src/activitypub/actors' import * as actors from 'wildebeest/backend/src/activitypub/actors' -import { createPrivateNote, createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note' +import { createDirectNote, createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note' import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' import { strict as assert } from 'node:assert/strict' import { cacheObject } from 'wildebeest/backend/src/activitypub/objects/' @@ -22,43 +22,89 @@ const vapidKeys = {} as JWK const domain = 'cloudflare.com' describe('ActivityPub', () => { - test('fetch non-existant user by id', async () => { - const db = await makeDB() + describe('Actors', () => { + test('fetch non-existant user by id', async () => { + const db = await makeDB() - const res = await ap_users.handleRequest(domain, db, 'nonexisting') - assert.equal(res.status, 404) - }) + const res = await ap_users.handleRequest(domain, db, 'nonexisting') + assert.equal(res.status, 404) + }) - test('fetch user by id', async () => { - const db = await makeDB() - const properties = { - summary: 'test summary', - inbox: 'https://example.com/inbox', - outbox: 'https://example.com/outbox', - following: 'https://example.com/following', - followers: 'https://example.com/followers', - } - const pubKey = - '-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApnI8FHJQXqqAdM87YwVseRUqbNLiw8nQ0zHBUyLylzaORhI4LfW4ozguiw8cWYgMbCufXMoITVmdyeTMGbQ3Q1sfQEcEjOZZXEeCCocmnYjK6MFSspjFyNw6GP0a5A/tt1tAcSlgALv8sg1RqMhSE5Kv+6lSblAYXcIzff7T2jh9EASnimaoAAJMaRH37+HqSNrouCxEArcOFhmFETadXsv+bHZMozEFmwYSTugadr4WD3tZd+ONNeimX7XZ3+QinMzFGOW19ioVHyjt3yCDU1cPvZIDR17dyEjByNvx/4N4Zly7puwBn6Ixy/GkIh5BWtL5VOFDJm/S+zcf1G1WsOAXMwKL4Nc5UWKfTB7Wd6voId7vF7nI1QYcOnoyh0GqXWhTPMQrzie4nVnUrBedxW0s/0vRXeR63vTnh5JrTVu06JGiU2pq2kvwqoui5VU6rtdImITybJ8xRkAQ2jo4FbbkS6t49PORIuivxjS9wPl7vWYazZtDVa5g/5eL7PnxOG3HsdIJWbGEh1CsG83TU9burHIepxXuQ+JqaSiKdCVc8CUiO++acUqKp7lmbYR9E/wRmvxXDFkxCZzA0UL2mRoLLLOe4aHvRSTsqiHC5Wwxyew5bb+eseJz3wovid9ZSt/tfeMAkCDmaCxEK+LGEbJ9Ik8ihis8Esm21N0A54sCAwEAAQ==-----END PUBLIC KEY-----' - await db - .prepare('INSERT INTO actors (id, email, type, properties, pubkey) VALUES (?, ?, ?, ?, ?)') - .bind(`https://${domain}/ap/users/sven`, 'sven@cloudflare.com', 'Person', JSON.stringify(properties), pubKey) - .run() + test('fetch user by id', async () => { + const db = await makeDB() + const properties = { + summary: 'test summary', + inbox: 'https://example.com/inbox', + outbox: 'https://example.com/outbox', + following: 'https://example.com/following', + followers: 'https://example.com/followers', + } + const pubKey = + '-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApnI8FHJQXqqAdM87YwVseRUqbNLiw8nQ0zHBUyLylzaORhI4LfW4ozguiw8cWYgMbCufXMoITVmdyeTMGbQ3Q1sfQEcEjOZZXEeCCocmnYjK6MFSspjFyNw6GP0a5A/tt1tAcSlgALv8sg1RqMhSE5Kv+6lSblAYXcIzff7T2jh9EASnimaoAAJMaRH37+HqSNrouCxEArcOFhmFETadXsv+bHZMozEFmwYSTugadr4WD3tZd+ONNeimX7XZ3+QinMzFGOW19ioVHyjt3yCDU1cPvZIDR17dyEjByNvx/4N4Zly7puwBn6Ixy/GkIh5BWtL5VOFDJm/S+zcf1G1WsOAXMwKL4Nc5UWKfTB7Wd6voId7vF7nI1QYcOnoyh0GqXWhTPMQrzie4nVnUrBedxW0s/0vRXeR63vTnh5JrTVu06JGiU2pq2kvwqoui5VU6rtdImITybJ8xRkAQ2jo4FbbkS6t49PORIuivxjS9wPl7vWYazZtDVa5g/5eL7PnxOG3HsdIJWbGEh1CsG83TU9burHIepxXuQ+JqaSiKdCVc8CUiO++acUqKp7lmbYR9E/wRmvxXDFkxCZzA0UL2mRoLLLOe4aHvRSTsqiHC5Wwxyew5bb+eseJz3wovid9ZSt/tfeMAkCDmaCxEK+LGEbJ9Ik8ihis8Esm21N0A54sCAwEAAQ==-----END PUBLIC KEY-----' + await db + .prepare('INSERT INTO actors (id, email, type, properties, pubkey) VALUES (?, ?, ?, ?, ?)') + .bind(`https://${domain}/ap/users/sven`, 'sven@cloudflare.com', 'Person', JSON.stringify(properties), pubKey) + .run() - const res = await ap_users.handleRequest(domain, db, 'sven') - assert.equal(res.status, 200) + const res = await ap_users.handleRequest(domain, db, 'sven') + assert.equal(res.status, 200) - const data = await res.json() - assert.equal(data.summary, 'test summary') - assert(data.discoverable) - assert(data['@context']) - assert(isUrlValid(data.id)) - assert(isUrlValid(data.url)) - assert(isUrlValid(data.inbox)) - assert(isUrlValid(data.outbox)) - assert(isUrlValid(data.following)) - assert(isUrlValid(data.followers)) - assert.equal(data.publicKey.publicKeyPem, pubKey) + const data = await res.json() + assert.equal(data.summary, 'test summary') + assert(data.discoverable) + assert(data['@context']) + assert(isUrlValid(data.id)) + assert(isUrlValid(data.url)) + assert(isUrlValid(data.inbox)) + assert(isUrlValid(data.outbox)) + assert(isUrlValid(data.following)) + assert(isUrlValid(data.followers)) + assert.equal(data.publicKey.publicKeyPem, pubKey) + }) + + test('sanitize Actor properties', async () => { + globalThis.fetch = async (input: RequestInfo) => { + if (input === 'https://example.com/actor') { + return new Response( + JSON.stringify({ + id: 'https://example.com/actor', + type: 'Person', + summary: "it's me, Mario. ", + name: 'hi
hey', + preferredUsername: 'sven ', + }) + ) + } + throw new Error(`unexpected request to "${input}"`) + } + + const actor = await actors.get('https://example.com/actor') + assert.equal(actor.summary, "it's me, Mario.

alert(1)

") + assert.equal(actor.name, 'hi hey') + assert.equal(actor.preferredUsername, 'sven alert(1)') + }) + + test('Actor properties limits', async () => { + globalThis.fetch = async (input: RequestInfo) => { + if (input === 'https://example.com/actor') { + return new Response( + JSON.stringify({ + id: 'https://example.com/actor', + type: 'Person', + summary: 'a'.repeat(612), + name: 'b'.repeat(50), + preferredUsername: 'c'.repeat(50), + }) + ) + } + throw new Error(`unexpected request to "${input}"`) + } + + const actor = await actors.get('https://example.com/actor') + assert.equal(actor.summary, 'a'.repeat(500)) + assert.equal(actor.name, 'b'.repeat(30)) + assert.equal(actor.preferredUsername, 'c'.repeat(30)) + }) }) describe('Outbox', () => { @@ -100,7 +146,7 @@ describe('ActivityPub', () => { const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com') const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com') - const note = await createPrivateNote(domain, db, 'DM', actorA, actorB) + const note = await createDirectNote(domain, db, 'DM', actorA, [actorB]) await addObjectInOutbox(db, actorA, note, undefined, actorB.id.toString()) { @@ -125,7 +171,7 @@ describe('ActivityPub', () => { const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com') const actorB = await createPerson(domain, db, userKEK, 'target@cloudflare.com') - const note = await createPrivateNote(domain, db, 'DM', actorA, actorB) + const note = await createDirectNote(domain, db, 'DM', actorA, [actorB]) await addObjectInOutbox(db, actorA, note) const res = await ap_outbox_page.handleRequest(domain, db, 'target') diff --git a/backend/test/mastodon/accounts.spec.ts b/backend/test/mastodon/accounts.spec.ts index f29df60..6ab1640 100644 --- a/backend/test/mastodon/accounts.spec.ts +++ b/backend/test/mastodon/accounts.spec.ts @@ -976,24 +976,24 @@ describe('Mastodon APIs', () => { { rel: 'self', type: 'application/activity+json', - href: 'https://social.com/sven', + href: `https://${domain}/ap/users/actor`, }, ], }) ) } - if (request.url === 'https://social.com/sven') { + if (request.url === `https://${domain}/ap/users/actor`) { return new Response( JSON.stringify({ id: `https://${domain}/ap/users/actor`, type: 'Person', - inbox: 'https://example.com/inbox', + inbox: `https://${domain}/ap/users/actor/inbox`, }) ) } - if (request.url === 'https://example.com/inbox') { + if (request.url === `https://${domain}/ap/users/actor/inbox`) { assert.equal(request.method, 'POST') receivedActivity = await request.json() return new Response('') @@ -1040,7 +1040,7 @@ describe('Mastodon APIs', () => { const connectedActor = actor - const req = new Request('https://example.com', { method: 'POST' }) + const req = new Request('https://' + domain, { method: 'POST' }) const res = await accounts_unfollow.handleRequest(req, db, 'actor@' + domain, connectedActor, userKEK) assert.equal(res.status, 200) assertCORS(res) diff --git a/backend/test/mastodon/statuses.spec.ts b/backend/test/mastodon/statuses.spec.ts index 9025200..d81c05f 100644 --- a/backend/test/mastodon/statuses.spec.ts +++ b/backend/test/mastodon/statuses.spec.ts @@ -18,6 +18,7 @@ import { MessageType } from 'wildebeest/backend/src/types/queue' import { MastodonStatus } from 'wildebeest/backend/src/types' import { mastodonIdSymbol, getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects' import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' +import * as timelines from 'wildebeest/backend/src/mastodon/timeline' const userKEK = 'test_kek4' const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) @@ -202,6 +203,7 @@ describe('Mastodon APIs', () => { return new Response( JSON.stringify({ id: 'https://social.com/users/sven', + type: 'Person', inbox: 'https://social.com/sven/inbox', }) ) @@ -404,6 +406,7 @@ describe('Mastodon APIs', () => { }) test('get mentions from status', async () => { + const db = await makeDB() globalThis.fetch = async (input: RequestInfo) => { if (input.toString() === 'https://instance.horse/.well-known/webfinger?resource=acct%3Asven%40instance.horse') { return new Response( @@ -467,6 +470,7 @@ describe('Mastodon APIs', () => { return new Response( JSON.stringify({ id: 'https://instance.horse/users/sven', + type: 'Person', }) ) } @@ -474,6 +478,7 @@ describe('Mastodon APIs', () => { return new Response( JSON.stringify({ id: 'https://cloudflare.com/users/sven', + type: 'Person', }) ) } @@ -481,6 +486,7 @@ describe('Mastodon APIs', () => { return new Response( JSON.stringify({ id: 'https://cloudflare.com/users/a', + type: 'Person', }) ) } @@ -488,6 +494,7 @@ describe('Mastodon APIs', () => { return new Response( JSON.stringify({ id: 'https://cloudflare.com/users/b', + type: 'Person', }) ) } @@ -496,42 +503,42 @@ describe('Mastodon APIs', () => { } { - const mentions = await getMentions('test status', domain) + const mentions = await getMentions('test status', domain, db) assert.equal(mentions.length, 0) } { - const mentions = await getMentions('no-json@actor.com', domain) + const mentions = await getMentions('no-json@actor.com', domain, db) assert.equal(mentions.length, 0) } { - const mentions = await getMentions('@sven@instance.horse test status', domain) + const mentions = await getMentions('@sven@instance.horse test status', domain, db) assert.equal(mentions.length, 1) assert.equal(mentions[0].id.toString(), 'https://instance.horse/users/sven') } { - const mentions = await getMentions('@sven test status', domain) + const mentions = await getMentions('@sven test status', domain, db) assert.equal(mentions.length, 1) assert.equal(mentions[0].id.toString(), 'https://' + domain + '/users/sven') } { - const mentions = await getMentions('@a @b', domain) + const mentions = await getMentions('@a @b', domain, db) assert.equal(mentions.length, 2) assert.equal(mentions[0].id.toString(), 'https://' + domain + '/users/a') assert.equal(mentions[1].id.toString(), 'https://' + domain + '/users/b') } { - const mentions = await getMentions('

@sven

', domain) + const mentions = await getMentions('

@sven

', domain, db) assert.equal(mentions.length, 1) assert.equal(mentions[0].id.toString(), 'https://' + domain + '/users/sven') } { - const mentions = await getMentions('

@unknown

', domain) + const mentions = await getMentions('

@unknown

', domain, db) assert.equal(mentions.length, 0) } }) @@ -1010,5 +1017,150 @@ describe('Mastodon APIs', () => { assert.equal(results![0].object_id, note.id.toString()) assert.equal(results![1].object_id, note.id.toString()) }) + + test('reject statuses exceeding limits', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const body = { + status: 'a'.repeat(501), + visibility: 'public', + } + const req = new Request('https://example.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache) + assert.equal(res.status, 422) + assertJSON(res) + }) + + test('create status with direct visibility', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + const actor1 = await createPerson(domain, db, userKEK, 'actor1@cloudflare.com') + const actor2 = await createPerson(domain, db, userKEK, 'actor2@cloudflare.com') + + let deliveredActivity1: any = null + let deliveredActivity2: any = null + + globalThis.fetch = async (input: RequestInfo | Request) => { + if ( + input.toString() === 'https://cloudflare.com/.well-known/webfinger?resource=acct%3Aactor1%40cloudflare.com' + ) { + return new Response( + JSON.stringify({ + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: actor1.id, + }, + ], + }) + ) + } + if ( + input.toString() === 'https://cloudflare.com/.well-known/webfinger?resource=acct%3Aactor2%40cloudflare.com' + ) { + return new Response( + JSON.stringify({ + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: actor2.id, + }, + ], + }) + ) + } + + // @ts-ignore + if (input.url === actor1.inbox.toString()) { + deliveredActivity1 = await (input as Request).json() + return new Response() + } + // @ts-ignore + if (input.url === actor2.inbox.toString()) { + deliveredActivity2 = await (input as Request).json() + return new Response() + } + + throw new Error('unexpected request to ' + input) + } + + const body = { + status: '@actor1 @actor2 hey', + visibility: 'direct', + } + const req = new Request('https://' + domain, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache) + assert.equal(res.status, 200) + + assert(deliveredActivity1) + assert(deliveredActivity2) + delete deliveredActivity1.id + delete deliveredActivity2.id + + assert.deepEqual(deliveredActivity1, deliveredActivity2) + assert.equal(deliveredActivity1.to.length, 2) + assert.equal(deliveredActivity1.to[0], actor1.id.toString()) + assert.equal(deliveredActivity1.to[1], actor2.id.toString()) + assert.equal(deliveredActivity1.cc.length, 0) + + // ensure that the private note doesn't show up in public timeline + const timeline = await timelines.getPublicTimeline(domain, db, timelines.LocalPreference.NotSet) + assert.equal(timeline.length, 0) + }) + + test('create status with unlisted visibility', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const body = { + status: 'something nice', + visibility: 'unlisted', + } + const req = new Request('https://' + domain, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache) + assert.equal(res.status, 422) + assertJSON(res) + }) + + test('create status with private visibility', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const body = { + status: 'something nice', + visibility: 'private', + } + const req = new Request('https://' + domain, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache) + assert.equal(res.status, 422) + assertJSON(res) + }) }) }) diff --git a/backend/test/mastodon/timelines.spec.ts b/backend/test/mastodon/timelines.spec.ts index 56729db..fb789ea 100644 --- a/backend/test/mastodon/timelines.spec.ts +++ b/backend/test/mastodon/timelines.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert/strict' import { createReply } from 'wildebeest/backend/test/shared.utils' import { createImage } from 'wildebeest/backend/src/activitypub/objects/image' import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow' -import { createPublicNote, createPrivateNote } from 'wildebeest/backend/src/activitypub/objects/note' +import { createPublicNote, createDirectNote } from 'wildebeest/backend/src/activitypub/objects/note' import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' import { createPerson } from 'wildebeest/backend/src/activitypub/actors' import { makeDB, assertCORS, assertJSON, makeCache } from '../utils' @@ -67,7 +67,7 @@ describe('Mastodon APIs', () => { await acceptFollowing(db, actor3, actor2) // actor2 sends a DM to actor1 - const note = await createPrivateNote(domain, db, 'DM', actor2, actor1) + const note = await createDirectNote(domain, db, 'DM', actor2, [actor1]) await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString()) // actor3 shouldn't see the private note @@ -100,7 +100,7 @@ describe('Mastodon APIs', () => { const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com') // actor2 sends a DM to actor1 - const note = await createPrivateNote(domain, db, 'DM', actor2, actor1) + const note = await createDirectNote(domain, db, 'DM', actor2, [actor1]) await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString()) const data = await timelines.getPublicTimeline(domain, db, timelines.LocalPreference.NotSet) diff --git a/backend/test/shared.utils.ts b/backend/test/shared.utils.ts index e0e0768..eeec490 100644 --- a/backend/test/shared.utils.ts +++ b/backend/test/shared.utils.ts @@ -4,6 +4,7 @@ * building. */ +import { type Database } from 'wildebeest/backend/src/database' import type { Actor } from '../src/activitypub/actors' import { addObjectInOutbox } from '../src/activitypub/actors/outbox' import { type Note, createPublicNote } from '../src/activitypub/objects/note' @@ -13,14 +14,14 @@ import { insertReply } from '../src/mastodon/reply' * Creates a reply and inserts it in the reply author's outbox * * @param domain the domain to use - * @param db D1Database + * @param db Database * @param actor Author of the reply * @param originalNote The original note * @param replyContent content of the reply */ export async function createReply( domain: string, - db: D1Database, + db: Database, actor: Actor, originalNote: Note, replyContent: string diff --git a/backend/test/utils.ts b/backend/test/utils.ts index 864ceb2..f0b1471 100644 --- a/backend/test/utils.ts +++ b/backend/test/utils.ts @@ -7,7 +7,8 @@ import type { Client } from 'wildebeest/backend/src/mastodon/client' import { promises as fs } from 'fs' import * as path from 'path' import { BetaDatabase } from '@miniflare/d1' -import * as Database from 'better-sqlite3' +import * as SQLiteDatabase from 'better-sqlite3' +import { type Database } from 'wildebeest/backend/src/database' export function isUrlValid(s: string) { let url @@ -19,8 +20,8 @@ export function isUrlValid(s: string) { return url.protocol === 'https:' } -export async function makeDB(): Promise { - const db = new Database(':memory:') +export async function makeDB(): Promise { + const db = new SQLiteDatabase(':memory:') const db2 = new BetaDatabase(db)! // Manually run our migrations since @miniflare/d1 doesn't support it (yet). @@ -31,7 +32,7 @@ export async function makeDB(): Promise { db.exec(content) } - return db2 as unknown as D1Database + return db2 as unknown as Database } export function assertCORS(response: Response) { @@ -66,7 +67,7 @@ export async function streamToArrayBuffer(stream: ReadableStream) { } export async function createTestClient( - db: D1Database, + db: Database, redirectUri: string = 'https://localhost', scopes: string = 'read follow' ): Promise { diff --git a/backend/test/wildebeest/settings.spec.ts b/backend/test/wildebeest/settings.spec.ts index 3dc923a..fb14004 100644 --- a/backend/test/wildebeest/settings.spec.ts +++ b/backend/test/wildebeest/settings.spec.ts @@ -31,6 +31,7 @@ describe('Wildebeest', () => { return new Response( JSON.stringify({ id: 'https://social.com/someone', + type: 'Person', }) ) } diff --git a/consumer/src/deliver.ts b/consumer/src/deliver.ts index 3f1dcd2..0e4021b 100644 --- a/consumer/src/deliver.ts +++ b/consumer/src/deliver.ts @@ -1,4 +1,5 @@ import type { DeliverMessageBody } from 'wildebeest/backend/src/types/queue' +import { getDatabase } from 'wildebeest/backend/src/database' import { getSigningKey } from 'wildebeest/backend/src/mastodon/account' import * as actors from 'wildebeest/backend/src/activitypub/actors' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' @@ -7,12 +8,12 @@ import { deliverToActor } from 'wildebeest/backend/src/activitypub/deliver' export async function handleDeliverMessage(env: Env, actor: Actor, message: DeliverMessageBody) { const toActorId = new URL(message.toActorId) - const targetActor = await actors.getAndCache(toActorId, env.DATABASE) + const targetActor = await actors.getAndCache(toActorId, getDatabase(env as any)) if (targetActor === null) { console.warn(`actor ${toActorId} not found`) return } - const signingKey = await getSigningKey(message.userKEK, env.DATABASE, actor) + const signingKey = await getSigningKey(message.userKEK, getDatabase(env as any), actor) await deliverToActor(signingKey, actor, targetActor, message.activity, env.DOMAIN) } diff --git a/consumer/src/inbox.ts b/consumer/src/inbox.ts index 9c3ff81..8c54d5b 100644 --- a/consumer/src/inbox.ts +++ b/consumer/src/inbox.ts @@ -1,4 +1,5 @@ import type { InboxMessageBody } from 'wildebeest/backend/src/types/queue' +import { getDatabase } from 'wildebeest/backend/src/database' import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle' import * as notification from 'wildebeest/backend/src/mastodon/notification' import * as timeline from 'wildebeest/backend/src/mastodon/timeline' @@ -8,7 +9,7 @@ import type { Env } from './' export async function handleInboxMessage(env: Env, actor: Actor, message: InboxMessageBody) { const domain = env.DOMAIN - const db = env.DATABASE + const db = getDatabase(env as any) const adminEmail = env.ADMIN_EMAIL const cache = cacheFromEnv(env) const activity = message.activity diff --git a/consumer/src/index.ts b/consumer/src/index.ts index 0758d8a..0881094 100644 --- a/consumer/src/index.ts +++ b/consumer/src/index.ts @@ -1,4 +1,5 @@ import type { MessageBody, InboxMessageBody, DeliverMessageBody } from 'wildebeest/backend/src/types/queue' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { MessageType } from 'wildebeest/backend/src/types/queue' import { initSentryQueue } from './sentry' @@ -6,7 +7,7 @@ import { handleInboxMessage } from './inbox' import { handleDeliverMessage } from './deliver' export type Env = { - DATABASE: D1Database + DATABASE: Database DOMAIN: string ADMIN_EMAIL: string DO_CACHE: DurableObjectNamespace @@ -19,10 +20,11 @@ export type Env = { export default { async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext) { const sentry = initSentryQueue(env, ctx) + const db = getDatabase(env as any) try { for (const message of batch.messages) { - const actor = await actors.getActorById(env.DATABASE, new URL(message.body.actorId)) + const actor = await actors.getActorById(db, new URL(message.body.actorId)) if (actor === null) { console.warn(`actor ${message.body.actorId} is missing`) return diff --git a/frontend/mock-db/init.ts b/frontend/mock-db/init.ts index 1219e64..dc97a27 100644 --- a/frontend/mock-db/init.ts +++ b/frontend/mock-db/init.ts @@ -6,11 +6,12 @@ import { createReblog } from 'wildebeest/backend/src/mastodon/reblog' import { createReply as createReplyInBackend } from 'wildebeest/backend/test/shared.utils' import { createStatus } from 'wildebeest/backend/src/mastodon/status' import type { APObject } from 'wildebeest/backend/src/activitypub/objects' +import { type Database } from 'wildebeest/backend/src/database' /** * Run helper commands to initialize the database with actors, statuses, etc. */ -export async function init(domain: string, db: D1Database) { +export async function init(domain: string, db: Database) { const loadedStatuses: { status: MastodonStatus; note: Note }[] = [] for (const status of statuses) { const actor = await getOrCreatePerson(domain, db, status.account) @@ -47,7 +48,7 @@ export async function init(domain: string, db: D1Database) { */ async function createReply( domain: string, - db: D1Database, + db: Database, reply: MastodonStatus, loadedStatuses: { status: MastodonStatus; note: Note }[] ) { @@ -70,7 +71,7 @@ async function createReply( async function getOrCreatePerson( domain: string, - db: D1Database, + db: Database, { username, avatar, display_name }: Account ): Promise { const person = await getPersonByEmail(db, username) diff --git a/frontend/mock-db/worker.ts b/frontend/mock-db/worker.ts index d0439c1..e70b44b 100644 --- a/frontend/mock-db/worker.ts +++ b/frontend/mock-db/worker.ts @@ -1,7 +1,8 @@ import { init } from './init' +import { type Database } from 'wildebeest/backend/src/database' interface Env { - DATABASE: D1Database + DATABASE: Database } /** diff --git a/frontend/package.json b/frontend/package.json index a3c2808..dd75947 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "eslint": "8.30.0", "eslint-plugin-qwik": "0.16.1", "jest": "^29.3.1", + "lorem-ipsum": "^2.0.8", "node-fetch": "3.3.0", "postcss": "^8.4.16", "prettier": "2.8.1", diff --git a/frontend/src/components/AccountCard/AccountCard.tsx b/frontend/src/components/AccountCard/AccountCard.tsx index 3954dfa..c956e07 100644 --- a/frontend/src/components/AccountCard/AccountCard.tsx +++ b/frontend/src/components/AccountCard/AccountCard.tsx @@ -13,18 +13,13 @@ export const AccountCard = component$<{ const accountUrl = useAccountUrl(account) return ( - -
+ +
-
- {getDisplayNameElement(account)} -
-
- @{subText === 'username' ? account.username : account.acct} +
+
{getDisplayNameElement(account)}
+
@{subText === 'username' ? account.username : account.acct}
) diff --git a/frontend/src/components/Status/index.tsx b/frontend/src/components/Status/index.tsx index e0947c2..f8d5375 100644 --- a/frontend/src/components/Status/index.tsx +++ b/frontend/src/components/Status/index.tsx @@ -32,9 +32,9 @@ export default component$((props: Props) => { return (
-
+
- +
{formatTimeAgo(new Date(status.created_at))} diff --git a/frontend/src/components/layout/RightColumn/RightColumn.tsx b/frontend/src/components/layout/RightColumn/RightColumn.tsx index 81244ab..1443b2f 100644 --- a/frontend/src/components/layout/RightColumn/RightColumn.tsx +++ b/frontend/src/components/layout/RightColumn/RightColumn.tsx @@ -38,7 +38,7 @@ export default component$(() => { // const aboutLink = { iconName: 'fa-ellipsis', linkText: 'About', linkTarget: '/about', linkActiveRegex: /^\/about/ } return ( -
+
diff --git a/frontend/src/dummyData/statuses.ts b/frontend/src/dummyData/statuses.ts index b594fbe..5125d94 100644 --- a/frontend/src/dummyData/statuses.ts +++ b/frontend/src/dummyData/statuses.ts @@ -1,6 +1,7 @@ import type { MediaAttachment, MastodonStatus } from '~/types' import { generateDummyStatus } from './generateDummyStatus' import { ben, george, penny, rafael, zak } from './accounts' +import { loremIpsum } from 'lorem-ipsum' // Raw statuses which follow the precise structure found mastodon does const mastodonRawStatuses: MastodonStatus[] = [ @@ -38,6 +39,11 @@ const mastodonRawStatuses: MastodonStatus[] = [ .fill(null) .map((_, idx) => generateDummyMediaImage(`https:/loremflickr.com/640/480/abstract?lock=${100 + idx}`)), }), + generateDummyStatus({ + content: + loremIpsum({ count: 2, format: 'html', units: 'paragraphs' }) + + '

#テスト投稿\n長いURLを投稿してみる\nついでに改行も複数いれてみる\n\n\n良いプログラマになるには | プログラマが知るべき97のこと\nxn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/%E8%89%AF%E3%81%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%AB%E3%81%AF/

', + }), ] export const statuses: MastodonStatus[] = mastodonRawStatuses.map((rawStatus) => ({ diff --git a/frontend/src/routes/(admin)/oauth/authorize/index.tsx b/frontend/src/routes/(admin)/oauth/authorize/index.tsx index 011582f..7b6c3f8 100644 --- a/frontend/src/routes/(admin)/oauth/authorize/index.tsx +++ b/frontend/src/routes/(admin)/oauth/authorize/index.tsx @@ -14,7 +14,9 @@ export const clientLoader = loader$, { DATABASE: D1Database }>(a let client: Client | null = null try { client = await getClientById(platform.DATABASE, client_id) - } catch { + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) throw html(500, getErrorHtml('An error occurred while trying to fetch the client data, please try again later')) } if (client === null) { @@ -36,8 +38,9 @@ export const userLoader = loader$< // TODO: eventually, verify the JWT with Access, however this // is not critical. payload = access.getPayload(jwt.value) - } catch (err: unknown) { - console.warn((err as { stack: unknown }).stack) + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) throw html(500, getErrorHtml('Failed to validate Access JWT')) } diff --git a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx index dadb8b4..0b8307a 100644 --- a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx +++ b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx @@ -19,7 +19,9 @@ export const statusLoader = loader$< try { const statusResponse = await statusAPI.handleRequestGet(platform.DATABASE, params.statusId, domain, {} as Person) statusText = await statusResponse.text() - } catch { + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) throw html(500, getErrorHtml('An error occurred whilst retrieving the status data, please try again later')) } if (!statusText) { @@ -36,7 +38,9 @@ export const statusLoader = loader$< throw new Error(`No context present for status with ${params.statusId}`) } return { status, statusTextContent, context } - } catch { + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) throw html(500, getErrorHtml('No context for the status has been found, please try again later')) } }) diff --git a/frontend/src/routes/(frontend)/layout.tsx b/frontend/src/routes/(frontend)/layout.tsx index 0024254..6b29dd9 100644 --- a/frontend/src/routes/(frontend)/layout.tsx +++ b/frontend/src/routes/(frontend)/layout.tsx @@ -9,6 +9,7 @@ import { WildebeestLogo } from '~/components/MastodonLogo' import { getCommitHash } from '~/utils/getCommitHash' import { InstanceConfigContext } from '~/utils/instanceConfig' import { getDocumentHead } from '~/utils/getDocumentHead' +import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml' export const instanceLoader = loader$< Promise, @@ -24,8 +25,10 @@ export const instanceLoader = loader$< const results = await response.text() const json = JSON.parse(results) as InstanceConfig return json - } catch { - throw html(500, 'An error occurred whilst retrieving the instance details') + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) + throw html(500, getErrorHtml('An error occurred whilst retrieving the instance details')) } }) @@ -40,13 +43,13 @@ export default component$(() => { -
+
-
+
diff --git a/frontend/src/routes/(frontend)/public/index.tsx b/frontend/src/routes/(frontend)/public/index.tsx index f60c0a3..2fee643 100644 --- a/frontend/src/routes/(frontend)/public/index.tsx +++ b/frontend/src/routes/(frontend)/public/index.tsx @@ -5,6 +5,7 @@ import { DocumentHead, loader$ } from '@builder.io/qwik-city' import StickyHeader from '~/components/StickyHeader/StickyHeader' import { getDocumentHead } from '~/utils/getDocumentHead' import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel' +import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml' export const statusesLoader = loader$, { DATABASE: D1Database; domain: string }>( async ({ platform, html }) => { @@ -14,8 +15,10 @@ export const statusesLoader = loader$, { DATABASE: D1D const results = await response.text() // Manually parse the JSON to ensure that Qwik finds the resulting objects serializable. return JSON.parse(results) as MastodonStatus[] - } catch { - throw html(500, 'The public timeline is unavailable') + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) + throw html(500, getErrorHtml('The public timeline is unavailable')) } } ) diff --git a/frontend/src/routes/(frontend)/public/local/index.tsx b/frontend/src/routes/(frontend)/public/local/index.tsx index 828c1f3..d523ccd 100644 --- a/frontend/src/routes/(frontend)/public/local/index.tsx +++ b/frontend/src/routes/(frontend)/public/local/index.tsx @@ -5,6 +5,7 @@ import { DocumentHead, loader$ } from '@builder.io/qwik-city' import StickyHeader from '~/components/StickyHeader/StickyHeader' import { getDocumentHead } from '~/utils/getDocumentHead' import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel' +import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml' export const statusesLoader = loader$, { DATABASE: D1Database; domain: string }>( async ({ platform, html }) => { @@ -14,8 +15,10 @@ export const statusesLoader = loader$, { DATABASE: D1D const results = await response.text() // Manually parse the JSON to ensure that Qwik finds the resulting objects serializable. return JSON.parse(results) as MastodonStatus[] - } catch { - throw html(500, 'The local timeline is unavailable') + } catch (e: unknown) { + const error = e as { stack: string; cause: string } + console.warn(error.stack, error.cause) + throw html(500, getErrorHtml('The local timeline is unavailable')) } } ) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 264c677..cc93c0f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1441,6 +1441,11 @@ commander@^4.0.0: resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^9.3.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -2814,6 +2819,13 @@ longest-streak@^3.0.0: resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz" integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== +lorem-ipsum@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz#f969a089f2ac6f19cf01b854b61beabb0e6f3cbc" + integrity sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA== + dependencies: + commander "^9.3.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" diff --git a/functions/.well-known/webfinger.ts b/functions/.well-known/webfinger.ts index 922817c..9786aba 100644 --- a/functions/.well-known/webfinger.ts +++ b/functions/.well-known/webfinger.ts @@ -4,9 +4,10 @@ import { parseHandle } from '../../backend/src/utils/parse' import { getActorById, actorURL } from 'wildebeest/backend/src/activitypub/actors' import type { Env } from '../../backend/src/types/env' import type { WebFingerResponse } from '../../backend/src/webfinger' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ request, env }) => { - return handleRequest(request, env.DATABASE) + return handleRequest(request, getDatabase(env)) } const headers = { @@ -14,7 +15,7 @@ const headers = { 'cache-control': 'max-age=3600, public', } -export async function handleRequest(request: Request, db: D1Database): Promise { +export async function handleRequest(request: Request, db: Database): Promise { const url = new URL(request.url) const domain = url.hostname const resource = url.searchParams.get('resource') diff --git a/functions/ap/o/[id].ts b/functions/ap/o/[id].ts index 62dabfc..6ead150 100644 --- a/functions/ap/o/[id].ts +++ b/functions/ap/o/[id].ts @@ -1,10 +1,11 @@ import { cors } from 'wildebeest/backend/src/utils/cors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Env } from 'wildebeest/backend/src/types/env' import * as objects from 'wildebeest/backend/src/activitypub/objects' export const onRequest: PagesFunction = async ({ params, request, env }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { @@ -12,7 +13,7 @@ const headers = { 'content-type': 'application/activity+json; charset=utf-8', } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const obj = await objects.getObjectById(db, objects.uri(domain, id)) if (obj === null) { return new Response('', { status: 404 }) diff --git a/functions/ap/users/[id].ts b/functions/ap/users/[id].ts index 7eaf06f..3ef7862 100644 --- a/functions/ap/users/[id].ts +++ b/functions/ap/users/[id].ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' import type { Env } from 'wildebeest/backend/src/types/env' @@ -6,7 +7,7 @@ import * as actors from 'wildebeest/backend/src/activitypub/actors' export const onRequest: PagesFunction = async ({ params, request, env }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { @@ -15,7 +16,7 @@ const headers = { 'Cache-Control': 'max-age=180, public', } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null && handle.domain !== domain) { diff --git a/functions/ap/users/[id]/followers.ts b/functions/ap/users/[id]/followers.ts index a2e1f9d..931be69 100644 --- a/functions/ap/users/[id]/followers.ts +++ b/functions/ap/users/[id]/followers.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Env } from 'wildebeest/backend/src/types/env' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' @@ -10,10 +11,10 @@ const headers = { export const onRequest: PagesFunction = async ({ params, request, env }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/ap/users/[id]/followers/page.ts b/functions/ap/users/[id]/followers/page.ts index 770a981..499b1f3 100644 --- a/functions/ap/users/[id]/followers/page.ts +++ b/functions/ap/users/[id]/followers/page.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { getFollowers } from 'wildebeest/backend/src/mastodon/follow' import { getActorById } from 'wildebeest/backend/src/activitypub/actors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' @@ -7,14 +8,14 @@ import type { Env } from 'wildebeest/backend/src/types/env' export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { 'content-type': 'application/json; charset=utf-8', } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/ap/users/[id]/following.ts b/functions/ap/users/[id]/following.ts index c75a225..5e40c08 100644 --- a/functions/ap/users/[id]/following.ts +++ b/functions/ap/users/[id]/following.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Env } from 'wildebeest/backend/src/types/env' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' @@ -10,10 +11,10 @@ const headers = { export const onRequest: PagesFunction = async ({ params, request, env }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/ap/users/[id]/following/page.ts b/functions/ap/users/[id]/following/page.ts index 7b0f5ac..f79ee9c 100644 --- a/functions/ap/users/[id]/following/page.ts +++ b/functions/ap/users/[id]/following/page.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { getFollowingId } from 'wildebeest/backend/src/mastodon/follow' import { getActorById } from 'wildebeest/backend/src/activitypub/actors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' @@ -7,14 +8,14 @@ import type { Env } from 'wildebeest/backend/src/types/env' export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { 'content-type': 'application/json; charset=utf-8', } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/ap/users/[id]/inbox.ts b/functions/ap/users/[id]/inbox.ts index ff6bfa9..0913eae 100644 --- a/functions/ap/users/[id]/inbox.ts +++ b/functions/ap/users/[id]/inbox.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { getVAPIDKeys } from 'wildebeest/backend/src/config' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import * as actors from 'wildebeest/backend/src/activitypub/actors' @@ -38,12 +39,20 @@ export const onRequest: PagesFunction = async ({ params, request, env const activity: Activity = JSON.parse(body) const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string, activity, env.QUEUE, env.userKEK, getVAPIDKeys(env)) + return handleRequest( + domain, + getDatabase(env), + params.id as string, + activity, + env.QUEUE, + env.userKEK, + getVAPIDKeys(env) + ) } export async function handleRequest( domain: string, - db: D1Database, + db: Database, id: string, activity: Activity, queue: Queue, diff --git a/functions/ap/users/[id]/outbox.ts b/functions/ap/users/[id]/outbox.ts index c01d45e..8ba8d9a 100644 --- a/functions/ap/users/[id]/outbox.ts +++ b/functions/ap/users/[id]/outbox.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { getActorById } from 'wildebeest/backend/src/activitypub/actors' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' import type { ContextData } from 'wildebeest/backend/src/types/context' @@ -6,7 +7,7 @@ import type { Env } from 'wildebeest/backend/src/types/env' export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string, env.userKEK) + return handleRequest(domain, getDatabase(env), params.id as string, env.userKEK) } const headers = { @@ -14,7 +15,7 @@ const headers = { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use userKEK -export async function handleRequest(domain: string, db: D1Database, id: string, userKEK: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string, userKEK: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/ap/users/[id]/outbox/page.ts b/functions/ap/users/[id]/outbox/page.ts index 1dcf710..6b91c55 100644 --- a/functions/ap/users/[id]/outbox/page.ts +++ b/functions/ap/users/[id]/outbox/page.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import type { Activity } from 'wildebeest/backend/src/activitypub/activities' import { getActorById } from 'wildebeest/backend/src/activitypub/actors' @@ -11,7 +12,7 @@ import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities' export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { @@ -21,7 +22,7 @@ const headers = { const DEFAULT_LIMIT = 20 -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const handle = parseHandle(id) if (handle.domain !== null) { diff --git a/functions/api/v1/accounts/[id].ts b/functions/api/v1/accounts/[id].ts index d2eb7fb..e3861d1 100644 --- a/functions/api/v1/accounts/[id].ts +++ b/functions/api/v1/accounts/[id].ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/accounts/#get +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import type { ContextData } from 'wildebeest/backend/src/types/context' import type { Env } from 'wildebeest/backend/src/types/env' @@ -12,10 +13,10 @@ const headers = { export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, params.id as string, env.DATABASE) + return handleRequest(domain, params.id as string, getDatabase(env)) } -export async function handleRequest(domain: string, id: string, db: D1Database): Promise { +export async function handleRequest(domain: string, id: string, db: Database): Promise { const account = await getAccount(domain, id, db) if (account) { diff --git a/functions/api/v1/accounts/[id]/follow.ts b/functions/api/v1/accounts/[id]/follow.ts index 6491923..ad74505 100644 --- a/functions/api/v1/accounts/[id]/follow.ts +++ b/functions/api/v1/accounts/[id]/follow.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { deliverToActor } from 'wildebeest/backend/src/activitypub/deliver' @@ -12,12 +13,12 @@ import type { Relationship } from 'wildebeest/backend/src/types/account' import { addFollowing } from 'wildebeest/backend/src/mastodon/follow' export const onRequest: PagesFunction = async ({ request, env, params, data }) => { - return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor, env.userKEK) + return handleRequest(request, getDatabase(env), params.id as string, data.connectedActor, env.userKEK) } export async function handleRequest( request: Request, - db: D1Database, + db: Database, id: string, connectedActor: Person, userKEK: string diff --git a/functions/api/v1/accounts/[id]/followers.ts b/functions/api/v1/accounts/[id]/followers.ts index 2887a4e..8055b9c 100644 --- a/functions/api/v1/accounts/[id]/followers.ts +++ b/functions/api/v1/accounts/[id]/followers.ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/accounts/#followers +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Handle } from 'wildebeest/backend/src/utils/parse' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' import { cors } from 'wildebeest/backend/src/utils/cors' @@ -15,10 +16,10 @@ import { getFollowers, loadActors } from 'wildebeest/backend/src/activitypub/act import * as localFollow from 'wildebeest/backend/src/mastodon/follow' export const onRequest: PagesFunction = async ({ params, request, env }) => { - return handleRequest(request, env.DATABASE, params.id as string) + return handleRequest(request, getDatabase(env), params.id as string) } -export async function handleRequest(request: Request, db: D1Database, id: string): Promise { +export async function handleRequest(request: Request, db: Database, id: string): Promise { const handle = parseHandle(id) const domain = new URL(request.url).hostname @@ -33,7 +34,7 @@ export async function handleRequest(request: Request, db: D1Database, id: string } } -async function getRemoteFollowers(request: Request, handle: Handle, db: D1Database): Promise { +async function getRemoteFollowers(request: Request, handle: Handle, db: Database): Promise { const acct = `${handle.localPart}@${handle.domain}` const link = await webfinger.queryAcctLink(handle.domain!, acct) if (link === null) { @@ -57,7 +58,7 @@ async function getRemoteFollowers(request: Request, handle: Handle, db: D1Databa return new Response(JSON.stringify(out), { headers }) } -async function getLocalFollowers(request: Request, handle: Handle, db: D1Database): Promise { +async function getLocalFollowers(request: Request, handle: Handle, db: Database): Promise { const domain = new URL(request.url).hostname const actorId = actorURL(domain, handle.localPart) const actor = await actors.getAndCache(actorId, db) diff --git a/functions/api/v1/accounts/[id]/following.ts b/functions/api/v1/accounts/[id]/following.ts index ae53f85..040dcf4 100644 --- a/functions/api/v1/accounts/[id]/following.ts +++ b/functions/api/v1/accounts/[id]/following.ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/accounts/#following +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Handle } from 'wildebeest/backend/src/utils/parse' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' import { cors } from 'wildebeest/backend/src/utils/cors' @@ -15,10 +16,10 @@ import * as webfinger from 'wildebeest/backend/src/webfinger' import { getFollowing, loadActors } from 'wildebeest/backend/src/activitypub/actors/follow' export const onRequest: PagesFunction = async ({ params, request, env }) => { - return handleRequest(request, env.DATABASE, params.id as string) + return handleRequest(request, getDatabase(env), params.id as string) } -export async function handleRequest(request: Request, db: D1Database, id: string): Promise { +export async function handleRequest(request: Request, db: Database, id: string): Promise { const handle = parseHandle(id) const domain = new URL(request.url).hostname @@ -33,7 +34,7 @@ export async function handleRequest(request: Request, db: D1Database, id: string } } -async function getRemoteFollowing(request: Request, handle: Handle, db: D1Database): Promise { +async function getRemoteFollowing(request: Request, handle: Handle, db: Database): Promise { const acct = `${handle.localPart}@${handle.domain}` const link = await webfinger.queryAcctLink(handle.domain!, acct) if (link === null) { @@ -57,7 +58,7 @@ async function getRemoteFollowing(request: Request, handle: Handle, db: D1Databa return new Response(JSON.stringify(out), { headers }) } -async function getLocalFollowing(request: Request, handle: Handle, db: D1Database): Promise { +async function getLocalFollowing(request: Request, handle: Handle, db: Database): Promise { const domain = new URL(request.url).hostname const actorId = actorURL(domain, handle.localPart) const actor = await actors.getAndCache(actorId, db) diff --git a/functions/api/v1/accounts/[id]/statuses.ts b/functions/api/v1/accounts/[id]/statuses.ts index 17b38ca..a79fe5e 100644 --- a/functions/api/v1/accounts/[id]/statuses.ts +++ b/functions/api/v1/accounts/[id]/statuses.ts @@ -1,4 +1,5 @@ import type { Env } from 'wildebeest/backend/src/types/env' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities' import * as errors from 'wildebeest/backend/src/errors' import { cors } from 'wildebeest/backend/src/utils/cors' @@ -25,10 +26,10 @@ const headers = { } export const onRequest: PagesFunction = async ({ request, env, params }) => { - return handleRequest(request, env.DATABASE, params.id as string) + return handleRequest(request, getDatabase(env), params.id as string) } -export async function handleRequest(request: Request, db: D1Database, id: string): Promise { +export async function handleRequest(request: Request, db: Database, id: string): Promise { const handle = parseHandle(id) const url = new URL(request.url) const domain = url.hostname @@ -46,7 +47,7 @@ export async function handleRequest(request: Request, db: D1Database, id: string } } -async function getRemoteStatuses(request: Request, handle: Handle, db: D1Database): Promise { +async function getRemoteStatuses(request: Request, handle: Handle, db: Database): Promise { const url = new URL(request.url) const domain = url.hostname const isPinned = url.searchParams.get('pinned') === 'true' @@ -118,7 +119,7 @@ async function getRemoteStatuses(request: Request, handle: Handle, db: D1Databas export async function getLocalStatuses( request: Request, - db: D1Database, + db: Database, handle: Handle, offset: number, withReplies: boolean diff --git a/functions/api/v1/accounts/[id]/unfollow.ts b/functions/api/v1/accounts/[id]/unfollow.ts index 680cfa2..693022b 100644 --- a/functions/api/v1/accounts/[id]/unfollow.ts +++ b/functions/api/v1/accounts/[id]/unfollow.ts @@ -1,4 +1,5 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import { deliverToActor } from 'wildebeest/backend/src/activitypub/deliver' import { getSigningKey } from 'wildebeest/backend/src/mastodon/account' @@ -11,12 +12,12 @@ import type { Relationship } from 'wildebeest/backend/src/types/account' import { removeFollowing } from 'wildebeest/backend/src/mastodon/follow' export const onRequest: PagesFunction = async ({ request, env, params, data }) => { - return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor, env.userKEK) + return handleRequest(request, getDatabase(env), params.id as string, data.connectedActor, env.userKEK) } export async function handleRequest( request: Request, - db: D1Database, + db: Database, id: string, connectedActor: Person, userKEK: string @@ -34,7 +35,7 @@ export async function handleRequest( } const acct = `${handle.localPart}@${handle.domain}` - const targetActor = await webfinger.queryAcct(handle.domain!, acct) + const targetActor = await webfinger.queryAcct(handle.domain!, db, acct) if (targetActor === null) { return new Response('', { status: 404 }) } diff --git a/functions/api/v1/accounts/relationships.ts b/functions/api/v1/accounts/relationships.ts index cdfa8e8..0894f2d 100644 --- a/functions/api/v1/accounts/relationships.ts +++ b/functions/api/v1/accounts/relationships.ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/accounts/#relationships +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { cors } from 'wildebeest/backend/src/utils/cors' import type { Person } from 'wildebeest/backend/src/activitypub/actors' import type { Env } from 'wildebeest/backend/src/types/env' @@ -7,10 +8,10 @@ import type { ContextData } from 'wildebeest/backend/src/types/context' import { getFollowingAcct, getFollowingRequestedAcct } from 'wildebeest/backend/src/mastodon/follow' export const onRequest: PagesFunction = async ({ request, env, data }) => { - return handleRequest(request, env.DATABASE, data.connectedActor) + return handleRequest(request, getDatabase(env), data.connectedActor) } -export async function handleRequest(req: Request, db: D1Database, connectedActor: Person): Promise { +export async function handleRequest(req: Request, db: Database, connectedActor: Person): Promise { const url = new URL(req.url) let ids = [] diff --git a/functions/api/v1/accounts/update_credentials.ts b/functions/api/v1/accounts/update_credentials.ts index 550f57b..82991c3 100644 --- a/functions/api/v1/accounts/update_credentials.ts +++ b/functions/api/v1/accounts/update_credentials.ts @@ -1,6 +1,7 @@ // https://docs.joinmastodon.org/methods/accounts/#update_credentials import { cors } from 'wildebeest/backend/src/utils/cors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/queue' import * as errors from 'wildebeest/backend/src/errors' import * as activities from 'wildebeest/backend/src/activitypub/activities/update' @@ -21,7 +22,7 @@ const headers = { export const onRequest: PagesFunction = async ({ request, data, env }) => { return handleRequest( - env.DATABASE, + getDatabase(env), request, data.connectedActor, env.CF_ACCOUNT_ID, @@ -32,7 +33,7 @@ export const onRequest: PagesFunction = async ({ request, } export async function handleRequest( - db: D1Database, + db: Database, request: Request, connectedActor: Actor, diff --git a/functions/api/v1/accounts/verify_credentials.ts b/functions/api/v1/accounts/verify_credentials.ts index ce9d37d..0cd2ecb 100644 --- a/functions/api/v1/accounts/verify_credentials.ts +++ b/functions/api/v1/accounts/verify_credentials.ts @@ -6,12 +6,13 @@ import type { Env } from 'wildebeest/backend/src/types/env' import * as errors from 'wildebeest/backend/src/errors' import type { CredentialAccount } from 'wildebeest/backend/src/types/account' import type { ContextData } from 'wildebeest/backend/src/types/context' +import { getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ data, env }) => { if (!data.connectedActor) { return errors.notAuthorized('no connected user') } - const user = await loadLocalMastodonAccount(env.DATABASE, data.connectedActor) + const user = await loadLocalMastodonAccount(getDatabase(env), data.connectedActor) const res: CredentialAccount = { ...user, diff --git a/functions/api/v1/apps.ts b/functions/api/v1/apps.ts index 3ef273c..8d1bdbb 100644 --- a/functions/api/v1/apps.ts +++ b/functions/api/v1/apps.ts @@ -7,6 +7,7 @@ import { createClient } from 'wildebeest/backend/src/mastodon/client' import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription' import { getVAPIDKeys } from 'wildebeest/backend/src/config' import { readBody } from 'wildebeest/backend/src/utils/body' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' type AppsPost = { redirect_uris: string @@ -16,10 +17,10 @@ type AppsPost = { } export const onRequest: PagesFunction = async ({ request, env }) => { - return handleRequest(env.DATABASE, request, getVAPIDKeys(env)) + return handleRequest(getDatabase(env), request, getVAPIDKeys(env)) } -export async function handleRequest(db: D1Database, request: Request, vapidKeys: JWK) { +export async function handleRequest(db: Database, request: Request, vapidKeys: JWK) { if (request.method !== 'POST') { return errors.methodNotAllowed() } diff --git a/functions/api/v1/instance/peers.ts b/functions/api/v1/instance/peers.ts index dde8c11..e5535a6 100644 --- a/functions/api/v1/instance/peers.ts +++ b/functions/api/v1/instance/peers.ts @@ -1,12 +1,13 @@ import { cors } from 'wildebeest/backend/src/utils/cors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Env } from 'wildebeest/backend/src/types/env' import { getPeers } from 'wildebeest/backend/src/activitypub/peers' export const onRequest: PagesFunction = async ({ env }) => { - return handleRequest(env.DATABASE) + return handleRequest(getDatabase(env)) } -export async function handleRequest(db: D1Database): Promise { +export async function handleRequest(db: Database): Promise { const headers = { ...cors(), 'content-type': 'application/json; charset=utf-8', diff --git a/functions/api/v1/notifications/[id].ts b/functions/api/v1/notifications/[id].ts index 32af039..2a1fff4 100644 --- a/functions/api/v1/notifications/[id].ts +++ b/functions/api/v1/notifications/[id].ts @@ -1,5 +1,6 @@ // https://docs.joinmastodon.org/methods/notifications/#get-one +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import type { Notification, NotificationsQueryResult } from 'wildebeest/backend/src/types/notification' import { urlToHandle } from 'wildebeest/backend/src/utils/handle' import { getActorById } from 'wildebeest/backend/src/activitypub/actors' @@ -14,13 +15,13 @@ const headers = { export const onRequest: PagesFunction = async ({ data, request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, params.id as string, env.DATABASE, data.connectedActor) + return handleRequest(domain, params.id as string, getDatabase(env), data.connectedActor) } export async function handleRequest( domain: string, id: string, - db: D1Database, + db: Database, connectedActor: Person ): Promise { const query = ` diff --git a/functions/api/v1/push/subscription.ts b/functions/api/v1/push/subscription.ts index 8154c91..ad83cb0 100644 --- a/functions/api/v1/push/subscription.ts +++ b/functions/api/v1/push/subscription.ts @@ -9,13 +9,14 @@ import { ContextData } from 'wildebeest/backend/src/types/context' import type { Env } from 'wildebeest/backend/src/types/env' import * as errors from 'wildebeest/backend/src/errors' import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequestGet: PagesFunction = async ({ request, env, data }) => { - return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId, getVAPIDKeys(env)) + return handleGetRequest(getDatabase(env), request, data.connectedActor, data.clientId, getVAPIDKeys(env)) } export const onRequestPost: PagesFunction = async ({ request, env, data }) => { - return handlePostRequest(env.DATABASE, request, data.connectedActor, data.clientId, getVAPIDKeys(env)) + return handlePostRequest(getDatabase(env), request, data.connectedActor, data.clientId, getVAPIDKeys(env)) } const headers = { @@ -24,7 +25,7 @@ const headers = { } export async function handleGetRequest( - db: D1Database, + db: Database, request: Request, connectedActor: Actor, clientId: string, @@ -55,7 +56,7 @@ export async function handleGetRequest( } export async function handlePostRequest( - db: D1Database, + db: Database, request: Request, connectedActor: Actor, clientId: string, diff --git a/functions/api/v1/statuses.ts b/functions/api/v1/statuses.ts index e1912b4..3b7ba94 100644 --- a/functions/api/v1/statuses.ts +++ b/functions/api/v1/statuses.ts @@ -8,7 +8,7 @@ import * as timeline from 'wildebeest/backend/src/mastodon/timeline' import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/queue' import type { Document } from 'wildebeest/backend/src/activitypub/objects' import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects' -import { createStatus, getMentions } from 'wildebeest/backend/src/mastodon/status' +import { getMentions } from 'wildebeest/backend/src/mastodon/status' import { getHashtags, insertHashtags } from 'wildebeest/backend/src/mastodon/hashtag' import * as activities from 'wildebeest/backend/src/activitypub/activities/create' import type { Env } from 'wildebeest/backend/src/types/env' @@ -26,6 +26,9 @@ import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats' import * as idempotency from 'wildebeest/backend/src/mastodon/idempotency' import { newMention } from 'wildebeest/backend/src/activitypub/objects/mention' import { originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' +import { createPublicNote, createDirectNote } from 'wildebeest/backend/src/activitypub/objects/note' +import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' type StatusCreate = { status: string @@ -36,13 +39,13 @@ type StatusCreate = { } export const onRequest: PagesFunction = async ({ request, env, data }) => { - return handleRequest(request, env.DATABASE, data.connectedActor, env.userKEK, env.QUEUE, cacheFromEnv(env)) + return handleRequest(request, getDatabase(env), data.connectedActor, env.userKEK, env.QUEUE, cacheFromEnv(env)) } // FIXME: add tests for delivery to followers and mentions to a specific Actor. export async function handleRequest( request: Request, - db: D1Database, + db: Database, connectedActor: Person, userKEK: string, queue: Queue, @@ -74,6 +77,10 @@ export async function handleRequest( return new Response('', { status: 400 }) } + if (body.status.length > 500) { + return errors.validationError('text character limit of 500 exceeded') + } + const mediaAttachments: Array = [] if (body.media_ids && body.media_ids.length > 0) { if (body.media_ids.length > 4) { @@ -107,13 +114,22 @@ export async function handleRequest( const hashtags = getHashtags(body.status) - const mentions = await getMentions(body.status, domain) + const mentions = await getMentions(body.status, domain, db) if (mentions.length > 0) { extraProperties.tag = mentions.map(newMention) } const content = enrichStatus(body.status, mentions) - const note = await createStatus(domain, db, connectedActor, content, mediaAttachments, extraProperties) + + let note + + if (body.visibility === 'public') { + note = await createPublicNote(domain, db, content, connectedActor, mediaAttachments, extraProperties) + } else if (body.visibility === 'direct') { + note = await createDirectNote(domain, db, content, connectedActor, mentions, mediaAttachments, extraProperties) + } else { + return errors.validationError(`status with visibility: ${body.visibility}`) + } if (hashtags.length > 0) { await insertHashtags(db, note, hashtags) @@ -127,11 +143,27 @@ export async function handleRequest( const activity = activities.create(domain, connectedActor, note) await deliverFollowers(db, userKEK, connectedActor, activity, queue) + if (body.visibility === 'public') { + await addObjectInOutbox(db, connectedActor, note) + + // A public note is sent to the public group URL and cc'ed any mentioned + // actors. + for (let i = 0, len = mentions.length; i < len; i++) { + const targetActor = mentions[i] + note.cc.push(targetActor.id.toString()) + } + } else if (body.visibility === 'direct') { + // A direct note is sent to mentioned people only + for (let i = 0, len = mentions.length; i < len; i++) { + const targetActor = mentions[i] + await addObjectInOutbox(db, connectedActor, note, undefined, targetActor.id.toString()) + } + } + { // If the status is mentioning other persons, we need to delivery it to them. for (let i = 0, len = mentions.length; i < len; i++) { const targetActor = mentions[i] - note.cc.push(targetActor.id.toString()) const activity = activities.create(domain, connectedActor, note) const signingKey = await getSigningKey(userKEK, db, connectedActor) await deliverToActor(signingKey, connectedActor, targetActor, activity, domain) diff --git a/functions/api/v1/statuses/[id].ts b/functions/api/v1/statuses/[id].ts index 007aee1..c813bfa 100644 --- a/functions/api/v1/statuses/[id].ts +++ b/functions/api/v1/statuses/[id].ts @@ -16,16 +16,17 @@ import { deliverFollowers } from 'wildebeest/backend/src/activitypub/deliver' import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/queue' import * as timeline from 'wildebeest/backend/src/mastodon/timeline' import { cacheFromEnv } from 'wildebeest/backend/src/cache' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequestGet: PagesFunction = async ({ params, env, request, data }) => { const domain = new URL(request.url).hostname - return handleRequestGet(env.DATABASE, params.id as UUID, domain, data.connectedActor) + return handleRequestGet(getDatabase(env), params.id as UUID, domain, data.connectedActor) } export const onRequestDelete: PagesFunction = async ({ params, env, request, data }) => { const domain = new URL(request.url).hostname return handleRequestDelete( - env.DATABASE, + getDatabase(env), params.id as UUID, data.connectedActor, domain, @@ -36,7 +37,7 @@ export const onRequestDelete: PagesFunction = async ({ pa } export async function handleRequestGet( - db: D1Database, + db: Database, id: UUID, domain: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars -- To be used when we implement private statuses @@ -62,7 +63,7 @@ export async function handleRequestGet( } export async function handleRequestDelete( - db: D1Database, + db: Database, id: UUID, connectedActor: Person, domain: string, diff --git a/functions/api/v1/statuses/[id]/context.ts b/functions/api/v1/statuses/[id]/context.ts index 9cf8d66..710fda6 100644 --- a/functions/api/v1/statuses/[id]/context.ts +++ b/functions/api/v1/statuses/[id]/context.ts @@ -6,10 +6,11 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects' import { getReplies } from 'wildebeest/backend/src/mastodon/reply' import type { Context } from 'wildebeest/backend/src/types/status' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, params.id as string) + return handleRequest(domain, getDatabase(env), params.id as string) } const headers = { @@ -17,7 +18,7 @@ const headers = { 'content-type': 'application/json; charset=utf-8', } -export async function handleRequest(domain: string, db: D1Database, id: string): Promise { +export async function handleRequest(domain: string, db: Database, id: string): Promise { const obj = await getObjectByMastodonId(db, id) if (obj === null) { return new Response('', { status: 404 }) diff --git a/functions/api/v1/statuses/[id]/favourite.ts b/functions/api/v1/statuses/[id]/favourite.ts index 047f376..5ec5692 100644 --- a/functions/api/v1/statuses/[id]/favourite.ts +++ b/functions/api/v1/statuses/[id]/favourite.ts @@ -13,14 +13,15 @@ import type { Note } from 'wildebeest/backend/src/activitypub/objects/note' import type { ContextData } from 'wildebeest/backend/src/types/context' import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status' import { originalObjectIdSymbol, originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ env, data, params, request }) => { const domain = new URL(request.url).hostname - return handleRequest(env.DATABASE, params.id as string, data.connectedActor, env.userKEK, domain) + return handleRequest(getDatabase(env), params.id as string, data.connectedActor, env.userKEK, domain) } export async function handleRequest( - db: D1Database, + db: Database, id: string, connectedActor: Person, userKEK: string, diff --git a/functions/api/v1/statuses/[id]/reblog.ts b/functions/api/v1/statuses/[id]/reblog.ts index ffef22a..15296ce 100644 --- a/functions/api/v1/statuses/[id]/reblog.ts +++ b/functions/api/v1/statuses/[id]/reblog.ts @@ -13,14 +13,15 @@ import type { Note } from 'wildebeest/backend/src/activitypub/objects/note' import type { ContextData } from 'wildebeest/backend/src/types/context' import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status' import { originalActorIdSymbol, originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ env, data, params, request }) => { const domain = new URL(request.url).hostname - return handleRequest(env.DATABASE, params.id as string, data.connectedActor, env.userKEK, env.QUEUE, domain) + return handleRequest(getDatabase(env), params.id as string, data.connectedActor, env.userKEK, env.QUEUE, domain) } export async function handleRequest( - db: D1Database, + db: Database, id: string, connectedActor: Person, userKEK: string, diff --git a/functions/api/v1/tags/[tag].ts b/functions/api/v1/tags/[tag].ts index dd1b65d..6c2b9eb 100644 --- a/functions/api/v1/tags/[tag].ts +++ b/functions/api/v1/tags/[tag].ts @@ -5,6 +5,7 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { getTag } from 'wildebeest/backend/src/mastodon/hashtag' import * as errors from 'wildebeest/backend/src/errors' import { cors } from 'wildebeest/backend/src/utils/cors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' const headers = { ...cors(), @@ -13,10 +14,10 @@ const headers = { export const onRequestGet: PagesFunction = async ({ params, env, request }) => { const domain = new URL(request.url).hostname - return handleRequestGet(env.DATABASE, domain, params.tag as string) + return handleRequestGet(getDatabase(env), domain, params.tag as string) } -export async function handleRequestGet(db: D1Database, domain: string, value: string): Promise { +export async function handleRequestGet(db: Database, domain: string, value: string): Promise { const tag = await getTag(db, domain, value) if (tag === null) { return errors.tagNotFound(value) diff --git a/functions/api/v1/timelines/public.ts b/functions/api/v1/timelines/public.ts index 1f54682..ec946c1 100644 --- a/functions/api/v1/timelines/public.ts +++ b/functions/api/v1/timelines/public.ts @@ -2,6 +2,7 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { cors } from 'wildebeest/backend/src/utils/cors' import type { ContextData } from 'wildebeest/backend/src/types/context' import { getPublicTimeline, LocalPreference } from 'wildebeest/backend/src/mastodon/timeline' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' const headers = { ...cors(), @@ -15,12 +16,12 @@ export const onRequest: PagesFunction = async ({ request, const only_media = searchParams.get('only_media') === 'true' const offset = Number.parseInt(searchParams.get('offset') ?? '0') const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, { local, remote, only_media, offset }) + return handleRequest(domain, getDatabase(env), { local, remote, only_media, offset }) } export async function handleRequest( domain: string, - db: D1Database, + db: Database, // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use only_media { local = false, remote = false, only_media = false, offset = 0 } = {} ): Promise { diff --git a/functions/api/v1/timelines/tag/[tag].ts b/functions/api/v1/timelines/tag/[tag].ts index b8b237c..01a9654 100644 --- a/functions/api/v1/timelines/tag/[tag].ts +++ b/functions/api/v1/timelines/tag/[tag].ts @@ -2,6 +2,7 @@ import type { Env } from 'wildebeest/backend/src/types/env' import { cors } from 'wildebeest/backend/src/utils/cors' import type { ContextData } from 'wildebeest/backend/src/types/context' import * as timelines from 'wildebeest/backend/src/mastodon/timeline' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' const headers = { ...cors(), @@ -10,10 +11,10 @@ const headers = { export const onRequest: PagesFunction = async ({ request, env, params }) => { const domain = new URL(request.url).hostname - return handleRequest(env.DATABASE, request, domain, params.tag as string) + return handleRequest(getDatabase(env), request, domain, params.tag as string) } -export async function handleRequest(db: D1Database, request: Request, domain: string, tag: string): Promise { +export async function handleRequest(db: Database, request: Request, domain: string, tag: string): Promise { const url = new URL(request.url) if (url.searchParams.has('max_id')) { return new Response(JSON.stringify([]), { headers }) diff --git a/functions/api/v2/instance.ts b/functions/api/v2/instance.ts index 7e8b508..eea8c43 100644 --- a/functions/api/v2/instance.ts +++ b/functions/api/v2/instance.ts @@ -3,13 +3,14 @@ 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 { getVersion } from 'wildebeest/config/versions' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequest: PagesFunction = async ({ env, request }) => { const domain = new URL(request.url).hostname - return handleRequest(domain, env.DATABASE, env) + return handleRequest(domain, getDatabase(env), env) } -export async function handleRequest(domain: string, db: D1Database, env: Env) { +export async function handleRequest(domain: string, db: Database, env: Env) { const headers = { ...cors(), 'content-type': 'application/json; charset=utf-8', diff --git a/functions/api/v2/media.ts b/functions/api/v2/media.ts index 8d9f0bb..f19b3f5 100644 --- a/functions/api/v2/media.ts +++ b/functions/api/v2/media.ts @@ -6,14 +6,15 @@ import type { ContextData } from 'wildebeest/backend/src/types/context' import type { MediaAttachment } from 'wildebeest/backend/src/types/media' import type { Person } from 'wildebeest/backend/src/activitypub/actors' import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequestPost: PagesFunction = async ({ request, env, data }) => { - return handleRequestPost(request, env.DATABASE, data.connectedActor, env.CF_ACCOUNT_ID, env.CF_API_TOKEN) + return handleRequestPost(request, getDatabase(env), data.connectedActor, env.CF_ACCOUNT_ID, env.CF_API_TOKEN) } export async function handleRequestPost( request: Request, - db: D1Database, + db: Database, connectedActor: Person, accountId: string, diff --git a/functions/api/v2/media/[id].ts b/functions/api/v2/media/[id].ts index eeef301..2d1fa03 100644 --- a/functions/api/v2/media/[id].ts +++ b/functions/api/v2/media/[id].ts @@ -11,16 +11,17 @@ import type { Env } from 'wildebeest/backend/src/types/env' import type { ContextData } from 'wildebeest/backend/src/types/context' import * as errors from 'wildebeest/backend/src/errors' import { updateObjectProperty } from 'wildebeest/backend/src/activitypub/objects' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequestPut: PagesFunction = async ({ params, env, request }) => { - return handleRequestPut(env.DATABASE, params.id as UUID, request) + return handleRequestPut(getDatabase(env), params.id as UUID, request) } type UpdateMedia = { description?: string } -export async function handleRequestPut(db: D1Database, id: UUID, request: Request): Promise { +export async function handleRequestPut(db: Database, id: UUID, request: Request): Promise { // Update the image properties { const image = (await getObjectByMastodonId(db, id)) as Image diff --git a/functions/api/v2/search.ts b/functions/api/v2/search.ts index 5fd9081..019e54c 100644 --- a/functions/api/v2/search.ts +++ b/functions/api/v2/search.ts @@ -8,6 +8,7 @@ import { parseHandle } from 'wildebeest/backend/src/utils/parse' import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account' import { personFromRow } from 'wildebeest/backend/src/activitypub/actors' import type { Handle } from 'wildebeest/backend/src/utils/parse' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' const headers = { ...cors(), @@ -21,10 +22,10 @@ type SearchResult = { } export const onRequest: PagesFunction = async ({ request, env }) => { - return handleRequest(env.DATABASE, request) + return handleRequest(getDatabase(env), request) } -export async function handleRequest(db: D1Database, request: Request): Promise { +export async function handleRequest(db: Database, request: Request): Promise { const url = new URL(request.url) if (!url.searchParams.has('q')) { @@ -49,7 +50,7 @@ export async function handleRequest(db: D1Database, request: Request): Promise = async ({ env, request, data }) => { - return handleRequestPost(env.DATABASE, request, data.connectedActor) + return handleRequestPost(getDatabase(env), request, data.connectedActor) } type AddAliasRequest = { alias: string } -export async function handleRequestPost(db: D1Database, request: Request, connectedActor: Actor): Promise { +export async function handleRequestPost(db: Database, request: Request, connectedActor: Actor): Promise { const body = await request.json() const handle = parseHandle(body.alias) @@ -23,7 +24,7 @@ export async function handleRequestPost(db: D1Database, request: Request, connec console.warn("account migration within an instance isn't supported") return new Response('', { status: 400 }) } - const actor = await queryAcct(handle.domain, acct) + const actor = await queryAcct(handle.domain, db, acct) if (actor === null) { return errors.resourceNotFound('actor', acct) } diff --git a/functions/first-login.ts b/functions/first-login.ts index 61156fd..8d0c418 100644 --- a/functions/first-login.ts +++ b/functions/first-login.ts @@ -6,14 +6,15 @@ import { createPerson } from 'wildebeest/backend/src/activitypub/actors' import { parse } from 'cookie' import * as errors from 'wildebeest/backend/src/errors' import * as access from 'wildebeest/backend/src/access' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' export const onRequestPost: PagesFunction = async ({ request, env }) => { - return handlePostRequest(request, env.DATABASE, env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) + return handlePostRequest(request, getDatabase(env), env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } export async function handlePostRequest( request: Request, - db: D1Database, + db: Database, userKEK: string, accessDomain: string, accessAud: string diff --git a/functions/oauth/authorize.ts b/functions/oauth/authorize.ts index a4984b0..7bda1c2 100644 --- a/functions/oauth/authorize.ts +++ b/functions/oauth/authorize.ts @@ -7,16 +7,17 @@ import * as errors from 'wildebeest/backend/src/errors' import { getClientById } from 'wildebeest/backend/src/mastodon/client' import * as access from 'wildebeest/backend/src/access' import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' // Extract the JWT token sent by Access (running before us). const extractJWTFromRequest = (request: Request) => request.headers.get('Cf-Access-Jwt-Assertion') || '' export const onRequestPost: PagesFunction = async ({ request, env }) => { - return handleRequestPost(request, env.DATABASE, env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) + return handleRequestPost(request, getDatabase(env), env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } export async function buildRedirect( - db: D1Database, + db: Database, request: Request, isFirstLogin: boolean, jwt: string @@ -64,7 +65,7 @@ export async function buildRedirect( export async function handleRequestPost( request: Request, - db: D1Database, + db: Database, userKEK: string, accessDomain: string, accessAud: string diff --git a/functions/oauth/token.ts b/functions/oauth/token.ts index 87e8d54..e44e940 100644 --- a/functions/oauth/token.ts +++ b/functions/oauth/token.ts @@ -3,6 +3,7 @@ import { cors } from 'wildebeest/backend/src/utils/cors' import * as errors from 'wildebeest/backend/src/errors' import type { Env } from 'wildebeest/backend/src/types/env' +import { type Database, getDatabase } from 'wildebeest/backend/src/database' import { readBody } from 'wildebeest/backend/src/utils/body' import { getClientById } from 'wildebeest/backend/src/mastodon/client' @@ -11,10 +12,10 @@ type Body = { } export const onRequest: PagesFunction = async ({ request, env }) => { - return handleRequest(env.DATABASE, request) + return handleRequest(getDatabase(env), request) } -export async function handleRequest(db: D1Database, request: Request): Promise { +export async function handleRequest(db: Database, request: Request): Promise { const headers = { ...cors(), 'content-type': 'application/json; charset=utf-8', diff --git a/playwright.config.ts b/playwright.config.ts index 50bd2a0..aa43bc8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -66,18 +66,18 @@ const config: PlaywrightTestConfig = { }, /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, - // }, + { + name: 'Mobile Chrome', + use: { + ...devices['Pixel 5'], + }, + }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + }, + }, /* Test against branded browsers. */ // {