kopia lustrzana https://github.com/cloudflare/wildebeest
Merge remote-tracking branch 'upstream/main' into fix-missing-apps-verify_credentials-endpoint
commit
980a779bc6
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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!
|
|
@ -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<MastodonAccount | null> {
|
||||
export async function getAccount(domain: string, accountId: string, db: Database): Promise<MastodonAccount | null> {
|
||||
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<MastodonAccount | null> {
|
||||
async function getRemoteAccount(handle: Handle, acct: string, db: D1Database): Promise<MastodonAccount | null> {
|
||||
// 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<MastodonA
|
|||
return await loadExternalMastodonAccount(acct, actor, true)
|
||||
}
|
||||
|
||||
async function getLocalAccount(domain: string, db: D1Database, handle: Handle): Promise<MastodonAccount | null> {
|
||||
async function getLocalAccount(domain: string, db: Database, handle: Handle): Promise<MastodonAccount | null> {
|
||||
const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart)
|
||||
|
||||
const actor = await getActorById(db, actorId)
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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<number> {
|
||||
const collection = await getMetadata(actor.following)
|
||||
|
@ -25,7 +26,7 @@ export async function getFollowing(actor: Actor): Promise<OrderedCollection<stri
|
|||
return collection
|
||||
}
|
||||
|
||||
export async function loadActors(db: D1Database, collection: OrderedCollection<string>): Promise<Array<Actor>> {
|
||||
export async function loadActors(db: Database, collection: OrderedCollection<string>): Promise<Array<Actor>> {
|
||||
const promises = collection.items.map((item) => {
|
||||
const actorId = new URL(item)
|
||||
return actors.getAndCache(actorId, db)
|
||||
|
|
|
@ -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(?, ?, ?)')
|
||||
|
|
|
@ -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<Actor> {
|
|||
|
||||
const data = await res.json<any>()
|
||||
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<Actor> {
|
||||
export async function getAndCache(url: URL, db: Database): Promise<Actor> {
|
||||
{
|
||||
const actor = await getActorById(db, url)
|
||||
if (actor !== null) {
|
||||
|
@ -111,7 +121,7 @@ export async function getAndCache(url: URL, db: D1Database): Promise<Actor> {
|
|||
return actor
|
||||
}
|
||||
|
||||
export async function getPersonByEmail(db: D1Database, email: string): Promise<Person | null> {
|
||||
export async function getPersonByEmail(db: Database, email: string): Promise<Person | null> {
|
||||
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<Actor | null> {
|
||||
export async function getActorById(db: Database, id: URL): Promise<Actor | null> {
|
||||
const stmt = db.prepare('SELECT * FROM actors WHERE id=?').bind(id.toString())
|
||||
const { results } = await stmt.all()
|
||||
if (!results || results.length === 0) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Image> {
|
||||
export async function createImage(domain: string, db: Database, actor: Actor, properties: any): Promise<Image> {
|
||||
const actorId = new URL(actor.id)
|
||||
return (await objects.createObject(domain, db, IMAGE, properties, actorId, true)) as Image
|
||||
}
|
||||
|
|
|
@ -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<Type extends APObject>(
|
||||
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<CacheObjectRes> {
|
||||
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<boolean> {
|
||||
export async function updateObject(db: Database, properties: any, id: URL): Promise<boolean> {
|
||||
// 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<APObject | null> {
|
||||
return getObjectBy(db, 'id', id.toString())
|
||||
export async function getObjectById(db: Database, id: string | URL): Promise<APObject | null> {
|
||||
return getObjectBy(db, ObjectByKey.id, id.toString())
|
||||
}
|
||||
|
||||
export async function getObjectByOriginalId(db: D1Database, id: string | URL): Promise<APObject | null> {
|
||||
return getObjectBy(db, 'original_object_id', id.toString())
|
||||
export async function getObjectByOriginalId(db: Database, id: string | URL): Promise<APObject | null> {
|
||||
return getObjectBy(db, ObjectByKey.originalObjectId, id.toString())
|
||||
}
|
||||
|
||||
export async function getObjectByMastodonId(db: D1Database, id: UUID): Promise<APObject | null> {
|
||||
return getObjectBy(db, 'mastodon_id', id)
|
||||
export async function getObjectByMastodonId(db: Database, id: UUID): Promise<APObject | null> {
|
||||
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<T extends APObject>(db: D1Database, note: T) {
|
||||
export async function deleteObject<T extends APObject>(db: Database, note: T) {
|
||||
const nodeId = note.id.toString()
|
||||
const batch = [
|
||||
db.prepare('DELETE FROM outbox_objects WHERE object_id=?').bind(nodeId),
|
||||
|
|
|
@ -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<objects.APObject> = [],
|
||||
|
@ -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<Actor>,
|
||||
attachment: Array<objects.APObject> = [],
|
||||
extraProperties: any = {}
|
||||
): Promise<Note> {
|
||||
|
@ -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
|
||||
|
|
|
@ -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<Array<String>> {
|
||||
export async function getPeers(db: Database): Promise<Array<String>> {
|
||||
const query = `SELECT domain FROM peers `
|
||||
const statement = db.prepare(query)
|
||||
|
||||
return getResultsField(statement, 'domain')
|
||||
}
|
||||
|
||||
export async function addPeer(db: D1Database, domain: string): Promise<void> {
|
||||
export async function addPeer(db: Database, domain: string): Promise<void> {
|
||||
const query = `
|
||||
INSERT OR IGNORE INTO peers (domain)
|
||||
VALUES (?)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import d1 from './d1'
|
||||
|
||||
export interface Result<T = unknown> {
|
||||
results?: T[]
|
||||
success: boolean
|
||||
error?: string
|
||||
meta: any
|
||||
}
|
||||
|
||||
export interface Database {
|
||||
prepare(query: string): PreparedStatement
|
||||
dump(): Promise<ArrayBuffer>
|
||||
batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]>
|
||||
exec<T = unknown>(query: string): Promise<Result<T>>
|
||||
}
|
||||
|
||||
export interface PreparedStatement {
|
||||
bind(...values: any[]): PreparedStatement
|
||||
first<T = unknown>(colName?: string): Promise<T>
|
||||
run<T = unknown>(): Promise<Result<T>>
|
||||
all<T = unknown>(): Promise<Result<T>>
|
||||
raw<T = unknown>(): Promise<T[]>
|
||||
}
|
||||
|
||||
export function getDatabase(env: Env): Database {
|
||||
return d1(env)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<MastodonAccount> {
|
||||
export async function loadLocalMastodonAccount(db: Database, res: Actor): Promise<MastodonAccount> {
|
||||
const query = `
|
||||
SELECT
|
||||
(SELECT count(*)
|
||||
|
@ -85,7 +86,7 @@ SELECT
|
|||
return account
|
||||
}
|
||||
|
||||
export async function getSigningKey(instanceKey: string, db: D1Database, actor: Actor): Promise<CryptoKey> {
|
||||
export async function getSigningKey(instanceKey: string, db: Database, actor: Actor): Promise<CryptoKey> {
|
||||
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))
|
||||
|
|
|
@ -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<Client | null> {
|
||||
export async function getClientById(db: Database, id: string): Promise<Client | null> {
|
||||
const stmt = db.prepare('SELECT * FROM clients WHERE id=?').bind(id)
|
||||
const { results } = await stmt.all()
|
||||
if (!results || results.length === 0) {
|
||||
|
|
|
@ -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<string>): Promise<void> {
|
||||
export async function moveFollowers(db: Database, actor: Actor, followers: Array<string>): Promise<void> {
|
||||
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<string>): Promise<void> {
|
||||
export async function moveFollowing(db: Database, actor: Actor, followingActors: Array<string>): Promise<void> {
|
||||
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<string> {
|
||||
export async function addFollowing(db: Database, actor: Actor, target: Actor, targetAcct: string): Promise<string> {
|
||||
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<Array<string>> {
|
||||
export function getFollowingAcct(db: Database, actor: Actor): Promise<Array<string>> {
|
||||
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<Array<st
|
|||
return getResultsField(statement, 'target_actor_acct')
|
||||
}
|
||||
|
||||
export function getFollowingRequestedAcct(db: D1Database, actor: Actor): Promise<Array<string>> {
|
||||
export function getFollowingRequestedAcct(db: Database, actor: Actor): Promise<Array<string>> {
|
||||
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<Array<string>> {
|
||||
export function getFollowingId(db: Database, actor: Actor): Promise<Array<string>> {
|
||||
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<Array<stri
|
|||
return getResultsField(statement, 'target_actor_id')
|
||||
}
|
||||
|
||||
export function getFollowers(db: D1Database, actor: Actor): Promise<Array<string>> {
|
||||
export function getFollowers(db: Database, actor: Actor): Promise<Array<string>> {
|
||||
const query = `
|
||||
SELECT actor_id FROM actor_following WHERE target_actor_id=? AND state=?
|
||||
`
|
||||
|
|
|
@ -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<Hashtag> {
|
|||
return [...matches].map((match) => match[1])
|
||||
}
|
||||
|
||||
export async function insertHashtags(db: D1Database, note: Note, values: Array<Hashtag>): Promise<void> {
|
||||
export async function insertHashtags(db: Database, note: Note, values: Array<Hashtag>): Promise<void> {
|
||||
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<H
|
|||
await db.batch(queries)
|
||||
}
|
||||
|
||||
export async function getTag(db: D1Database, domain: string, tag: string): Promise<Tag | null> {
|
||||
export async function getTag(db: Database, domain: string, tag: string): Promise<Tag | null> {
|
||||
const query = `
|
||||
SELECT * FROM note_hashtags WHERE value=?
|
||||
`
|
||||
|
|
|
@ -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<void> {
|
||||
export async function insertKey(db: Database, key: string, obj: APObject): Promise<void> {
|
||||
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<APObject | null> {
|
||||
export async function hasKey(db: Database, key: string): Promise<APObject | null> {
|
||||
const query = `
|
||||
SELECT objects.*
|
||||
FROM idempotency_keys
|
||||
|
|
|
@ -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<Array<string>> {
|
||||
export function getLikes(db: Database, obj: APObject): Promise<Array<string>> {
|
||||
const query = `
|
||||
SELECT actor_id FROM actor_favourites WHERE object_id=?
|
||||
`
|
||||
|
|
|
@ -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<string> {
|
||||
export async function insertFollowNotification(db: Database, actor: Actor, fromActor: Actor): Promise<string> {
|
||||
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<Array<Notification>> {
|
||||
export async function getNotifications(db: Database, actor: Actor, domain: string): Promise<Array<Notification>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<Array<string>> {
|
||||
export function getReblogs(db: Database, obj: APObject): Promise<Array<string>> {
|
||||
const query = `
|
||||
SELECT actor_id FROM actor_reblogs WHERE object_id=?
|
||||
`
|
||||
|
@ -40,7 +41,7 @@ export function getReblogs(db: D1Database, obj: APObject): Promise<Array<string>
|
|||
return getResultsField(statement, 'actor_id')
|
||||
}
|
||||
|
||||
export async function hasReblog(db: D1Database, actor: Actor, obj: APObject): Promise<boolean> {
|
||||
export async function hasReblog(db: Database, actor: Actor, obj: APObject): Promise<boolean> {
|
||||
const query = `
|
||||
SELECT count(*) as count FROM actor_reblogs WHERE object_id=?1 AND actor_id=?2
|
||||
`
|
||||
|
|
|
@ -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<Array<MastodonStatus>> {
|
||||
export async function getReplies(domain: string, db: Database, obj: APObject): Promise<Array<MastodonStatus>> {
|
||||
const QUERY = `
|
||||
SELECT objects.*,
|
||||
actors.id as actor_id,
|
||||
|
|
|
@ -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<Array<Actor>> {
|
||||
export async function getMentions(input: string, instanceDomain: string, db: Database): Promise<Array<Actor>> {
|
||||
const mentions: Array<Actor> = []
|
||||
|
||||
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<MastodonStatus | null> {
|
||||
|
@ -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<MastodonStatus | null> {
|
||||
export async function toMastodonStatusFromRow(domain: string, db: Database, row: any): Promise<MastodonStatus | null> {
|
||||
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<MastodonStatus | null> {
|
||||
export async function getMastodonStatusById(db: Database, id: UUID, domain: string): Promise<MastodonStatus | null> {
|
||||
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[],
|
||||
|
|
|
@ -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<Subscription | null> {
|
||||
export async function getSubscription(db: Database, actor: Actor, client: Client): Promise<Subscription | null> {
|
||||
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<Array<Subscription>> {
|
||||
export async function getSubscriptionForAllClients(db: Database, actor: Actor): Promise<Array<Subscription>> {
|
||||
const query = `
|
||||
SELECT * FROM subscriptions WHERE actor_id=? ORDER BY cdate DESC LIMIT 5
|
||||
`
|
||||
|
|
|
@ -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<Array<MastodonStatus>> {
|
||||
export async function getHomeTimeline(domain: string, db: Database, actor: Actor): Promise<Array<MastodonStatus>> {
|
||||
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
|
||||
|
|
|
@ -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<boolean> {
|
||||
async function loadContextData(db: Database, clientId: string, email: string, ctx: any): Promise<boolean> {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM actors
|
||||
|
@ -96,7 +97,7 @@ export async function main(context: EventContext<Env, any, any>) {
|
|||
// 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')
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MessageBody>
|
||||
|
|
|
@ -11,12 +11,12 @@ const headers = {
|
|||
accept: 'application/jrd+json',
|
||||
}
|
||||
|
||||
export async function queryAcct(domain: string, acct: string): Promise<Actor | null> {
|
||||
export async function queryAcct(domain: string, db: D1Database, acct: string): Promise<Actor | null> {
|
||||
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<URL | null> {
|
||||
|
|
|
@ -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<any>()
|
||||
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<any>()
|
||||
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. <script>alert(1)</script>",
|
||||
name: 'hi<br />hey',
|
||||
preferredUsername: 'sven <script>alert(1)</script>',
|
||||
})
|
||||
)
|
||||
}
|
||||
throw new Error(`unexpected request to "${input}"`)
|
||||
}
|
||||
|
||||
const actor = await actors.get('https://example.com/actor')
|
||||
assert.equal(actor.summary, "it's me, Mario. <p>alert(1)</p>")
|
||||
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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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('<p>@sven</p>', domain)
|
||||
const mentions = await getMentions('<p>@sven</p>', domain, db)
|
||||
assert.equal(mentions.length, 1)
|
||||
assert.equal(mentions[0].id.toString(), 'https://' + domain + '/users/sven')
|
||||
}
|
||||
|
||||
{
|
||||
const mentions = await getMentions('<p>@unknown</p>', domain)
|
||||
const mentions = await getMentions('<p>@unknown</p>', 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<D1Database> {
|
||||
const db = new Database(':memory:')
|
||||
export async function makeDB(): Promise<Database> {
|
||||
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<D1Database> {
|
|||
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<Client> {
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('Wildebeest', () => {
|
|||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'https://social.com/someone',
|
||||
type: 'Person',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<MessageBody>, 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
|
||||
|
|
|
@ -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<Person> {
|
||||
const person = await getPersonByEmail(db, username)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { init } from './init'
|
||||
import { type Database } from 'wildebeest/backend/src/database'
|
||||
|
||||
interface Env {
|
||||
DATABASE: D1Database
|
||||
DATABASE: Database
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -13,18 +13,13 @@ export const AccountCard = component$<{
|
|||
const accountUrl = useAccountUrl(account)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={accountUrl}
|
||||
class="inline-grid grid-cols-[repeat(2,_max-content)] grid-rows-[1fr,1fr] items-center no-underline"
|
||||
>
|
||||
<div class="row-span-2">
|
||||
<Link href={accountUrl} class="inline-flex items-center no-underline flex-wrap gap-2">
|
||||
<div class="flex-grow flex-shrink-0 flex justify-center">
|
||||
<Avatar primary={account} secondary={secondaryAvatar ?? null} />
|
||||
</div>
|
||||
<div data-testid="account-display-name" class="ml-2 col-start-2 row-start-1">
|
||||
{getDisplayNameElement(account)}
|
||||
</div>
|
||||
<div class="ml-2 text-wildebeest-400 col-start-2 row-start-2">
|
||||
@{subText === 'username' ? account.username : account.acct}
|
||||
<div>
|
||||
<div data-testid="account-display-name">{getDisplayNameElement(account)}</div>
|
||||
<div class="text-wildebeest-400">@{subText === 'username' ? account.username : account.acct}</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
|
|
|
@ -32,9 +32,9 @@ export default component$((props: Props) => {
|
|||
return (
|
||||
<article class="p-4 border-t border-wildebeest-700 break-words">
|
||||
<RebloggerLink account={reblogger}></RebloggerLink>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="flex justify-between mb-3 flex-wrap">
|
||||
<AccountCard account={status.account} subText={props.accountSubText} secondaryAvatar={reblogger} />
|
||||
<Link class="no-underline" href={statusUrl}>
|
||||
<Link class="no-underline ml-auto" href={statusUrl}>
|
||||
<div class="text-wildebeest-500 flex items-baseline">
|
||||
<i style={{ height: '0.75rem', width: '0.75rem' }} class="fa fa-xs fa-globe w-3 h-3" />
|
||||
<span class="ml-2 text-sm hover:underline min-w-max">{formatTimeAgo(new Date(status.created_at))}</span>
|
||||
|
|
|
@ -38,7 +38,7 @@ export default component$(() => {
|
|||
// const aboutLink = { iconName: 'fa-ellipsis', linkText: 'About', linkTarget: '/about', linkActiveRegex: /^\/about/ }
|
||||
|
||||
return (
|
||||
<div class="bg-wildebeest-600 xl:bg-transparent flex flex-col justify-between right-column-wrapper text-wildebeest-200 flex-1">
|
||||
<div class="bg-wildebeest-600 xl:bg-transparent flex flex-col justify-between right-column-wrapper text-wildebeest-200 flex-1 z-10">
|
||||
<div class="sticky top-[3.9rem] xl:top-0">
|
||||
<div class="xl:p-4">
|
||||
<Link class="no-underline hidden xl:flex items-center" aria-label="Wildebeest Home" href={'/'}>
|
||||
|
|
|
@ -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' }) +
|
||||
'<p>#テスト投稿\n長いURLを投稿してみる\nついでに改行も複数いれてみる\n\n\n良いプログラマになるには | プログラマが知るべき97のこと\n<a href="https://xn--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/">xn--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/</a></p>',
|
||||
}),
|
||||
]
|
||||
|
||||
export const statuses: MastodonStatus[] = mastodonRawStatuses.map((rawStatus) => ({
|
||||
|
|
|
@ -14,7 +14,9 @@ export const clientLoader = loader$<Promise<Client>, { 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'))
|
||||
}
|
||||
|
||||
|
|
|
@ -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'))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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<InstanceConfig>,
|
||||
|
@ -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$(() => {
|
|||
<WildebeestLogo size="small" />
|
||||
</Link>
|
||||
</header>
|
||||
<main class="flex-1 flex justify-center top-[3.9rem]">
|
||||
<main class="flex-1 flex justify-center top-[3.9rem] max-w-screen">
|
||||
<div class="w-fit md:w-72 hidden xl:block mx-2.5">
|
||||
<div class="sticky top-2.5">
|
||||
<LeftColumn />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full xl:max-w-xl bg-wildebeest-600 xl:bg-transparent flex flex-col break-all">
|
||||
<div class="w-0 xl:max-w-xl bg-wildebeest-600 xl:bg-transparent flex flex-col flex-1">
|
||||
<div class="bg-wildebeest-600 rounded flex flex-1 flex-col">
|
||||
<Slot />
|
||||
</div>
|
||||
|
|
|
@ -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$<Promise<MastodonStatus[]>, { DATABASE: D1Database; domain: string }>(
|
||||
async ({ platform, html }) => {
|
||||
|
@ -14,8 +15,10 @@ export const statusesLoader = loader$<Promise<MastodonStatus[]>, { 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'))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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$<Promise<MastodonStatus[]>, { DATABASE: D1Database; domain: string }>(
|
||||
async ({ platform, html }) => {
|
||||
|
@ -14,8 +15,10 @@ export const statusesLoader = loader$<Promise<MastodonStatus[]>, { 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'))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<Env, any> = 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<Response> {
|
||||
export async function handleRequest(request: Request, db: Database): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
const domain = url.hostname
|
||||
const resource = url.searchParams.get('resource')
|
||||
|
|
|
@ -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<Env, any> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const obj = await objects.getObjectById(db, objects.uri(domain, id))
|
||||
if (obj === null) {
|
||||
return new Response('', { status: 404 })
|
||||
|
|
|
@ -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<Env, any> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null && handle.domain !== domain) {
|
||||
|
|
|
@ -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<Env, any> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any> = 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<InboxMessageBody>,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string, userKEK: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const handle = parseHandle(id)
|
||||
|
||||
if (handle.domain !== null) {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, id: string, db: Database): Promise<Response> {
|
||||
const account = await getAccount(domain, id, db)
|
||||
|
||||
if (account) {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {
|
||||
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<Response> {
|
||||
async function getRemoteFollowers(request: Request, handle: Handle, db: Database): Promise<Response> {
|
||||
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<Response> {
|
||||
async function getLocalFollowers(request: Request, handle: Handle, db: Database): Promise<Response> {
|
||||
const domain = new URL(request.url).hostname
|
||||
const actorId = actorURL(domain, handle.localPart)
|
||||
const actor = await actors.getAndCache(actorId, db)
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {
|
||||
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<Response> {
|
||||
async function getRemoteFollowing(request: Request, handle: Handle, db: Database): Promise<Response> {
|
||||
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<Response> {
|
||||
async function getLocalFollowing(request: Request, handle: Handle, db: Database): Promise<Response> {
|
||||
const domain = new URL(request.url).hostname
|
||||
const actorId = actorURL(domain, handle.localPart)
|
||||
const actor = await actors.getAndCache(actorId, db)
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {
|
||||
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<Response> {
|
||||
async function getRemoteStatuses(request: Request, handle: Handle, db: Database): Promise<Response> {
|
||||
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
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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 })
|
||||
}
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(req: Request, db: Database, connectedActor: Person): Promise<Response> {
|
||||
const url = new URL(req.url)
|
||||
|
||||
let ids = []
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Env, any, ContextData> = async ({ request,
|
|||
}
|
||||
|
||||
export async function handleRequest(
|
||||
db: D1Database,
|
||||
db: Database,
|
||||
request: Request,
|
||||
connectedActor: Actor,
|
||||
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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()
|
||||
}
|
||||
|
|
|
@ -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<Env, any> = async ({ env }) => {
|
||||
return handleRequest(env.DATABASE)
|
||||
return handleRequest(getDatabase(env))
|
||||
}
|
||||
|
||||
export async function handleRequest(db: D1Database): Promise<Response> {
|
||||
export async function handleRequest(db: Database): Promise<Response> {
|
||||
const headers = {
|
||||
...cors(),
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
const query = `
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<DeliverMessageBody>,
|
||||
|
@ -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<Document> = []
|
||||
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)
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Env, any, ContextData> = 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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {
|
||||
const obj = await getObjectByMastodonId(db, id)
|
||||
if (obj === null) {
|
||||
return new Response('', { status: 404 })
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequestGet(db: Database, domain: string, value: string): Promise<Response> {
|
||||
const tag = await getTag(db, domain, value)
|
||||
if (tag === null) {
|
||||
return errors.tagNotFound(value)
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequest(db: Database, request: Request, domain: string, tag: string): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
if (url.searchParams.has('max_id')) {
|
||||
return new Response(JSON.stringify([]), { headers })
|
||||
|
|
|
@ -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<Env, any> = 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',
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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,
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequestPut(db: Database, id: UUID, request: Request): Promise<Response> {
|
||||
// Update the image properties
|
||||
{
|
||||
const image = (await getObjectByMastodonId(db, id)) as Image
|
||||
|
|
|
@ -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<Env, any> = async ({ request, env }) => {
|
||||
return handleRequest(env.DATABASE, request)
|
||||
return handleRequest(getDatabase(env), request)
|
||||
}
|
||||
|
||||
export async function handleRequest(db: D1Database, request: Request): Promise<Response> {
|
||||
export async function handleRequest(db: Database, request: Request): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
|
||||
if (!url.searchParams.has('q')) {
|
||||
|
@ -49,7 +50,7 @@ export async function handleRequest(db: D1Database, request: Request): Promise<R
|
|||
|
||||
if (useWebFinger && query.domain !== null) {
|
||||
const acct = `${query.localPart}@${query.domain}`
|
||||
const res = await queryAcct(query.domain, acct)
|
||||
const res = await queryAcct(query.domain, db, acct)
|
||||
if (res !== null) {
|
||||
out.accounts.push(await loadExternalMastodonAccount(acct, res))
|
||||
}
|
||||
|
|
|
@ -5,16 +5,17 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
|||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||
import { queryAcct } from 'wildebeest/backend/src/webfinger'
|
||||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = 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<Response> {
|
||||
export async function handleRequestPost(db: Database, request: Request, connectedActor: Actor): Promise<Response> {
|
||||
const body = await request.json<AddAliasRequest>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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
|
||||
|
|
|
@ -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<Env, any, ContextData> = 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
|
||||
|
|
|
@ -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<Env, any> = async ({ request, env }) => {
|
||||
return handleRequest(env.DATABASE, request)
|
||||
return handleRequest(getDatabase(env), request)
|
||||
}
|
||||
|
||||
export async function handleRequest(db: D1Database, request: Request): Promise<Response> {
|
||||
export async function handleRequest(db: Database, request: Request): Promise<Response> {
|
||||
const headers = {
|
||||
...cors(),
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
|
|
|
@ -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. */
|
||||
// {
|
||||
|
|
Ładowanie…
Reference in New Issue