Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 55 additions & 24 deletions backend/src/accounts/getAccount.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
// 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'
import { parseHandle, type Handle } from 'wildebeest/backend/src/utils/parse'
import { isHandle, urlToHandle } from 'wildebeest/backend/src/utils/handle'
import { isNumeric } from 'wildebeest/backend/src/utils/id'
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
import { MastodonAccount } from '../types'
import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain'

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)) {
// Retrieve the statuses from a local user
return getLocalAccount(domain, db, handle)
} else if (handle.domain !== null) {
// Retrieve the statuses of a remote actor
const acct = `${handle.localPart}@${handle.domain}`
return getRemoteAccount(handle, acct, db)
import { MastodonAccount } from 'wildebeest/backend/src/types'
// import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain'
import { findMastodonAccountIDByEmailQuery } from 'wildebeest/backend/src/mastodon/sql/account'

export async function getAccount(
localDomain: string,
mastodonAcctOrAPObjectId: string,
db: Database
): Promise<MastodonAccount | null> {
if (isHandle(mastodonAcctOrAPObjectId)) {
const handle: Handle = parseHandle(mastodonAcctOrAPObjectId)
return await _getAccount(handle, localDomain, db)
} else if (isNumeric(mastodonAcctOrAPObjectId)) {
console.error(`NOT IMPLEMENTED - MASTODON ID: ${mastodonAcctOrAPObjectId}`)
return null
} else {
try {
const apObjectId: URL = new URL(mastodonAcctOrAPObjectId)
const handle: Handle = parseHandle(urlToHandle(apObjectId))
return await _getAccount(handle, localDomain, db)
} catch {
console.error(`Unrecognized account identified: ${mastodonAcctOrAPObjectId}`)
return null
}
}
}

async function _getAccount(handle: Handle, localDomain: string, db: Database): Promise<MastodonAccount | null> {
if (handle.domain !== null && handle.domain !== localDomain) {
return await getRemoteAccount(handle, localDomain, db)
} else if (handle.domain === null || handle.domain === localDomain) {
handle.domain = localDomain
return await getLocalAccount(handle, localDomain, db)
} else {
console.error(
`Unable to find account 'handle.localPart' = ${handle.localPart} and 'handle.domain' = ${handle.domain}`
)
return null
}
}

async function getRemoteAccount(handle: Handle, acct: string, db: Database): Promise<MastodonAccount | null> {
async function getRemoteAccount(handle: Handle, localDomain: string, db: Database): Promise<MastodonAccount | null> {
const acct = `${handle.localPart}@${handle.domain}`
// 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.
Expand All @@ -36,13 +60,20 @@ async function getRemoteAccount(handle: Handle, acct: string, db: Database): Pro
return await loadExternalMastodonAccount(acct, actor, true)
}

async function getLocalAccount(domain: string, db: Database, handle: Handle): Promise<MastodonAccount | null> {
const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart)
async function getLocalAccount(handle: Handle, localDomain: string, db: Database): Promise<MastodonAccount | null> {
// const handle = parseHandle(mastodonAcctOrAPObjectId)
// const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart)

const actor = await getActorById(db, actorId)
if (actor === null) {
return null
}
// const actor = await getActorById(db, actorId)
// if (actor === null) {
// return null
// }

return await loadLocalMastodonAccount(handle, localDomain, db)
}

export async function getAccountByEmail(domain: string, email: string, db: Database): Promise<MastodonAccount | null> {
const row: any = await db.prepare(findMastodonAccountIDByEmailQuery).bind(email).first()

return await loadLocalMastodonAccount(db, actor)
return await getAccount(domain, row?.id, db)
}
84 changes: 57 additions & 27 deletions backend/src/activitypub/actors/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defaultImages } from 'wildebeest/config/accounts'
import { generateUserKey } from 'wildebeest/backend/src/utils/key-ops'
import { type APObject, sanitizeContent, getTextContent } from '../objects'
import { type APObject, sanitizeContent, getTextContent } from 'wildebeest/backend/src/activitypub/objects'
import { addPeer } from 'wildebeest/backend/src/activitypub/peers'
import { type Database } from 'wildebeest/backend/src/database'
import { Buffer } from 'buffer'
import { createMastodonId } from 'wildebeest/backend/src/utils/id'

const PERSON = 'Person'
const isTesting = typeof jest !== 'undefined'
Expand Down Expand Up @@ -34,6 +35,10 @@ export interface Person extends Actor {
}
}

export interface WBActor extends Actor {
mastodon_id?: string
}

export async function get(url: string | URL): Promise<Actor> {
const headers = {
accept: 'application/activity+json',
Expand Down Expand Up @@ -99,7 +104,10 @@ export async function getAndCache(url: URL, db: Database): Promise<Actor> {
throw new Error('missing fields on Actor')
}

const properties = actor
const properties = actor as WBActor
if (properties.mastodon_id === undefined) {
properties.mastodon_id = createMastodonId(actor.id.toString())
}

const sql = `
INSERT INTO actors (id, type, properties)
Expand Down Expand Up @@ -129,10 +137,11 @@ export async function getPersonByEmail(db: Database, email: string): Promise<Per
return null
}
const row: any = results[0]
return personFromRow(row)
return await personFromRow(row, db)
}

type PersonProperties = {
mastodon_id?: string
name?: string
summary?: string
icon?: { url: string }
Expand Down Expand Up @@ -181,6 +190,11 @@ export async function createPerson(

const id = actorURL(domain, properties.preferredUsername).toString()

if (properties.mastodon_id === undefined) {
const mastodon_id: string = createMastodonId(actorURL.toString())
properties.mastodon_id = mastodon_id
}

if (properties.inbox === undefined) {
properties.inbox = id + '/inbox'
}
Expand Down Expand Up @@ -208,7 +222,7 @@ export async function createPerson(
.bind(id, PERSON, email, userKeyPair.pubKey, privkey, salt, JSON.stringify(properties), admin ? 1 : null)
.first()

return personFromRow(row)
return await personFromRow(row, db)
}

export async function updateActorProperty(db: Database, actorId: URL, key: string, value: string) {
Expand Down Expand Up @@ -238,11 +252,47 @@ export async function getActorById(db: Database, id: URL): Promise<Actor | null>
return null
}
const row: any = results[0]
return personFromRow(row)
return await personFromRow(row, db)
}

export async function getPersonByMastodonId(mastodon_id: string, db: Database): Promise<Person | null> {
const stmt = db.prepare("SELECT * FROM actors WHERE properties ->> '$.mastodon_id' =?").bind(mastodon_id)
const { results } = await stmt.all()
if (!results || results.length === 0) {
return null
}
const row: any = results[0]
return await personFromRow(row, db)
}

export async function getPersonByMastodonAcct(acct: string, db: Database): Promise<Person | null> {
const stmt = db.prepare("SELECT * FROM actors WHERE properties ->> '$.preferredUsername' =?").bind(acct)
const { results } = await stmt.all()
if (!results || results.length === 0) {
return null
}
const row: any = results[0]
return await personFromRow(row, db)
}

export function personFromRow(row: any): Person {
const properties = JSON.parse(row.properties) as PersonProperties
export async function personFromRow(row: any, db: Database): Promise<Person> {
const properties: PersonProperties = JSON.parse(row.properties) as PersonProperties
// Old actors weren't created with `mastodon_id` add it now if missing
if (properties.mastodon_id === undefined) {
console.warn(`${properties.preferredUsername ?? 'unknown'} is missing 'mastodon_id'; adding now.`)
const mastodon_id: string = createMastodonId(row.id)
const { success, error } = await db
.prepare(`UPDATE actors SET properties = json_set(properties, '$.mastodon_id', ?) WHERE id=?`)
.bind(mastodon_id)
.run()
if (!success) {
throw new Error(`Unable to set 'mastodon_id' due to SQL error: ${error}`)
} else {
console.info(`Successfully added 'mastodon_id'`)
properties.mastodon_id = mastodon_id
}
}

const icon = properties.icon ?? {
type: 'Image',
mediaType: 'image/jpeg',
Expand Down Expand Up @@ -275,26 +325,6 @@ export function personFromRow(row: any): Person {
domain = new URL(row.original_actor_id).hostname
}

// Old local actors weren't created with inbox/outbox/etc properties, so add
// them if missing.
{
if (properties.inbox === undefined) {
properties.inbox = id + '/inbox'
}

if (properties.outbox === undefined) {
properties.outbox = id + '/outbox'
}

if (properties.following === undefined) {
properties.following = id + '/following'
}

if (properties.followers === undefined) {
properties.followers = id + '/followers'
}
}

return {
// Hidden values
[emailSymbol]: row.email,
Expand Down
15 changes: 15 additions & 0 deletions backend/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ export function internalServerError(): Response {
return generateErrorResponse('Internal Server Error', 500)
}

export function malformedMastodonAccountRequest(id: string): Response {
console.warn(`Mastodon account ID is not a numeric value: ${id}`)
return resourceNotFound('account', id)
}

export function malformedMastodonAccountLookup(acct: string): Response {
console.warn(`Lookup value is not a username or Webfinger address: '${acct}'`)
return resourceNotFound('account', acct)
}

export function mastodonAccountNotFound(acct: string): Response {
console.warn(`Mastodon account not found: '${acct}'`)
return resourceNotFound('account', acct)
}

export function statusNotFound(id: string): Response {
return resourceNotFound('status', id)
}
Expand Down
Loading