Merge branch 'cloudflare:main' into main

pull/429/head
Ben Royce 2023-03-25 22:40:04 -04:00 zatwierdzone przez GitHub
commit a851c74fb5
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
132 zmienionych plików z 2322 dodań i 521 usunięć

Wyświetl plik

@ -53,6 +53,9 @@ jobs:
- name: Check frontend linting
run: yarn lint:frontend
- name: Check frontend types
run: yarn --cwd types-check
test-ui:
runs-on: ubuntu-latest
steps:

Wyświetl plik

@ -273,6 +273,7 @@ jobs:
echo -e "DOMAIN=\"${{ vars.CF_DEPLOY_DOMAIN }}\"\n" >> consumer/wrangler.toml
echo -e "ADMIN_EMAIL=\"${{ vars.ADMIN_EMAIL }}\"\n" >> consumer/wrangler.toml
yarn
yarn --cwd consumer/
echo "******"
command: publish --config consumer/wrangler.toml

Wyświetl plik

@ -0,0 +1,20 @@
import { setActorAlias } from 'wildebeest/backend/src/activitypub/actors'
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 { type Database } from 'wildebeest/backend/src/database'
export async function addAlias(db: Database, alias: string, connectedActor: Actor) {
const handle = parseHandle(alias)
const acct = `${handle.localPart}@${handle.domain}`
if (handle.domain === null) {
throw new Error("account migration within an instance isn't supported")
}
const actor = await queryAcct(handle.domain, db, acct)
if (actor === null) {
throw new Error('actor not found')
}
await setActorAlias(db, connectedActor.id, actor.id)
}

Wyświetl plik

@ -24,7 +24,7 @@ export async function getAccount(domain: string, accountId: string, db: Database
}
}
async function getRemoteAccount(handle: Handle, acct: string, db: D1Database): Promise<MastodonAccount | null> {
async function getRemoteAccount(handle: Handle, acct: string, db: Database): 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.

Wyświetl plik

@ -3,10 +3,12 @@ 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'
import { Buffer } from 'buffer'
const PERSON = 'Person'
const isTesting = typeof jest !== 'undefined'
export const emailSymbol = Symbol()
export const isAdminSymbol = Symbol()
export function actorURL(domain: string, id: string): URL {
return new URL(`/ap/users/${id}`, 'https://' + domain)
@ -22,6 +24,7 @@ export interface Actor extends APObject {
alsoKnownAs?: string
[emailSymbol]: string
[isAdminSymbol]: boolean
}
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
@ -150,7 +153,8 @@ export async function createPerson(
db: Database,
userKEK: string,
email: string,
properties: PersonProperties = {}
properties: PersonProperties = {},
admin: boolean = false
): Promise<Person> {
const userKeyPair = await generateUserKey(userKEK)
@ -158,7 +162,7 @@ export async function createPerson(
// Since D1 and better-sqlite3 behaviors don't exactly match, presumable
// because Buffer support is different in Node/Worker. We have to transform
// the values depending on the platform.
if (isTesting) {
if (isTesting || db.client === 'neon') {
privkey = Buffer.from(userKeyPair.wrappedPrivKey)
salt = Buffer.from(userKeyPair.salt)
} else {
@ -198,12 +202,12 @@ export async function createPerson(
const row = await db
.prepare(
`
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties)
VALUES(?, ?, ?, ?, ?, ?, ?)
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties, is_admin)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *
`
)
.bind(id, PERSON, email, userKeyPair.pubKey, privkey, salt, JSON.stringify(properties))
.bind(id, PERSON, email, userKeyPair.pubKey, privkey, salt, JSON.stringify(properties), admin ? 1 : null)
.first()
return personFromRow(row)
@ -211,7 +215,7 @@ export async function createPerson(
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=?`)
.prepare(`UPDATE actors SET properties=${db.qb.jsonSet('properties', key, '?1')} WHERE id=?2`)
.bind(value, actorId.toString())
.run()
if (!success) {
@ -220,12 +224,24 @@ export async function updateActorProperty(db: Database, actorId: URL, key: strin
}
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())
.run()
if (!success) {
throw new Error('SQL error: ' + error)
if (db.client === 'neon') {
const { success, error } = await db
.prepare(`UPDATE actors SET properties=${db.qb.jsonSet('properties', 'alsoKnownAs,0', '?1')} WHERE id=?2`)
.bind('"' + alias.toString() + '"', actorId.toString())
.run()
if (!success) {
throw new Error('SQL error: ' + error)
}
} else {
const { success, error } = await db
.prepare(
`UPDATE actors SET properties=${db.qb.jsonSet('properties', 'alsoKnownAs', 'json_array(?1)')} WHERE id=?2`
)
.bind(alias.toString(), actorId.toString())
.run()
if (!success) {
throw new Error('SQL error: ' + error)
}
}
}
@ -240,7 +256,16 @@ export async function getActorById(db: Database, id: URL): Promise<Actor | null>
}
export function personFromRow(row: any): Person {
const properties = JSON.parse(row.properties) as PersonProperties
let properties
if (typeof row.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = row.properties as PersonProperties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(row.properties) as PersonProperties
}
const icon = properties.icon ?? {
type: 'Image',
mediaType: 'image/jpeg',
@ -296,6 +321,7 @@ export function personFromRow(row: any): Person {
return {
// Hidden values
[emailSymbol]: row.email,
[isAdminSymbol]: row.is_admin === 1,
...properties,
name,

Wyświetl plik

@ -128,7 +128,15 @@ export async function cacheObject(
}
{
const properties = JSON.parse(row.properties)
let properties
if (typeof row.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = row.properties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(row.properties)
}
const object = {
published: new Date(row.cdate).toISOString(),
...properties,
@ -159,7 +167,7 @@ export async function updateObject(db: Database, properties: any, id: URL): Prom
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=?`)
.prepare(`UPDATE objects SET properties=${db.qb.jsonSet('properties', key, '?1')} WHERE id=?2`)
.bind(value, obj.id.toString())
.run()
if (!success) {
@ -206,7 +214,15 @@ export async function getObjectBy(db: Database, key: ObjectByKey, value: string)
}
const result: any = results[0]
const properties = JSON.parse(result.properties)
let properties
if (typeof result.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = result.properties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(result.properties)
}
return {
published: new Date(result.cdate).toISOString(),
@ -270,7 +286,8 @@ function getContentRewriter() {
contentRewriter.on('*', {
element(el) {
if (!['p', 'span', 'br', 'a'].includes(el.tagName)) {
el.tagName = 'p'
const element = el as { tagName: string }
element.tagName = 'p'
}
if (el.hasAttribute('class')) {

Wyświetl plik

@ -9,10 +9,9 @@ export async function getPeers(db: Database): Promise<Array<String>> {
}
export async function addPeer(db: Database, domain: string): Promise<void> {
const query = `
INSERT OR IGNORE INTO peers (domain)
VALUES (?)
`
const query = db.qb.insertOrIgnore(`
INTO peers (domain) VALUES (?)
`)
const out = await db.prepare(query).bind(domain).run()
if (!out.success) {

Wyświetl plik

@ -0,0 +1,29 @@
import { type Database } from 'wildebeest/backend/src/database'
export async function getRules(db: Database): Promise<Array<{ id: string; text: string }>> {
const query = `SELECT * from server_rules;`
const result = await db.prepare(query).all<{ id: string; text: string }>()
if (!result.success) {
throw new Error('SQL error: ' + result.error)
}
return result.results ?? []
}
export async function upsertRule(db: Database, rule: { id?: number; text: string } | string) {
const id = typeof rule === 'string' ? null : rule.id ?? null
const text = typeof rule === 'string' ? rule : rule.text
return await db
.prepare(
`INSERT INTO server_rules (id, text)
VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET text=excluded.text;`
)
.bind(id, text)
.run()
}
export async function deleteRule(db: Database, ruleId: number) {
return await db.prepare('DELETE FROM server_rules WHERE id=?').bind(ruleId).run()
}

Wyświetl plik

@ -0,0 +1,49 @@
import { type Database } from 'wildebeest/backend/src/database'
import { type ServerSettingsData } from 'wildebeest/frontend/src/routes/(admin)/settings/(admin)/server-settings/layout'
export async function getSettings(db: Database): Promise<ServerSettingsData> {
const query = `SELECT * from server_settings`
const result = await db.prepare(query).all<{ setting_name: string; setting_value: string }>()
const data = (result.results ?? []).reduce(
(settings, { setting_name, setting_value }) => ({
...settings,
[setting_name]: setting_value,
}),
{} as Object
)
if (!result.success) {
throw new Error('SQL Error: ' + result.error)
}
return data as ServerSettingsData
}
export async function updateSettings(db: Database, data: Partial<ServerSettingsData>) {
const result = await upsertServerSettings(db, data)
if (result && !result.success) {
throw new Error('SQL Error: ' + result.error)
}
return new Response('', { status: 200 })
}
export async function upsertServerSettings(db: Database, settings: Partial<ServerSettingsData>) {
const settingsEntries = Object.entries(settings)
if (!settingsEntries.length) {
return null
}
const query = `
INSERT INTO server_settings (setting_name, setting_value)
VALUES ${settingsEntries.map(() => `(?, ?)`).join(', ')}
ON CONFLICT(setting_name) DO UPDATE SET setting_value=excluded.setting_value
`
return await db
.prepare(query)
.bind(...settingsEntries.flat())
.run()
}

Wyświetl plik

@ -1,6 +1,40 @@
import { type Database } from 'wildebeest/backend/src/database'
import { type Database, QueryBuilder } from 'wildebeest/backend/src/database'
import type { Env } from 'wildebeest/backend/src/types/env'
export default function make({ DATABASE }: Pick<Env, 'DATABASE'>): Database {
return DATABASE
const qb: QueryBuilder = {
jsonExtract(obj: string, prop: string): string {
return `json_extract(${obj}, '$.${prop}')`
},
jsonExtractIsNull(obj: string, prop: string): string {
return `${qb.jsonExtract(obj, prop)} IS NULL`
},
set(array: string): string {
return `(SELECT value FROM json_each(${array}))`
},
epoch(): string {
return '00-00-00 00:00:00'
},
insertOrIgnore(q: string): string {
return `INSERT OR IGNORE ${q}`
},
psqlOnly(): string {
return ''
},
jsonSet(obj: string, field: string, value: string): string {
return `json_set(${obj}, '$.${field}', ${value})`
},
}
export default function make({ DATABASE }: Pick<Env, 'DATABASE'>): Database {
const db = DATABASE as any
db.qb = qb
db.client = 'd1'
return db as Database
}

Wyświetl plik

@ -1,5 +1,6 @@
import type { Env } from 'wildebeest/backend/src/types/env'
import d1 from './d1'
import neon from './neon'
export interface Result<T = unknown> {
results?: T[]
@ -13,6 +14,8 @@ export interface Database {
dump(): Promise<ArrayBuffer>
batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]>
exec<T = unknown>(query: string): Promise<Result<T>>
qb: QueryBuilder
client: string
}
export interface PreparedStatement {
@ -23,6 +26,20 @@ export interface PreparedStatement {
raw<T = unknown>(): Promise<T[]>
}
export function getDatabase(env: Pick<Env, 'DATABASE'>): Database {
export interface QueryBuilder {
jsonExtract(obj: string, prop: string): string
jsonExtractIsNull(obj: string, prop: string): string
set(array: string): string
epoch(): string
insertOrIgnore(q: string): string
psqlOnly(raw: string): string
jsonSet(obj: string, field: string, value: string): string
}
export async function getDatabase(env: Pick<Env, 'DATABASE' | 'NEON_DATABASE_URL'>): Promise<Database> {
if (env.NEON_DATABASE_URL !== undefined) {
return neon(env)
}
return d1(env)
}

Wyświetl plik

@ -0,0 +1,195 @@
-- Migration number: 0000 2022-12-05T20:27:34.391Z
CREATE TABLE IF NOT EXISTS actors (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
email TEXT,
privkey bytea,
privkey_salt bytea,
pubkey TEXT,
cdate timestamp NOT NULL DEFAULT (now()),
properties jsonb NOT NULL DEFAULT '{}'::jsonb
);
CREATE INDEX IF NOT EXISTS actors_email ON actors(email);
CREATE TABLE IF NOT EXISTS actor_following (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
target_actor_id TEXT NOT NULL,
target_actor_acct TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE INDEX IF NOT EXISTS actor_following_actor_id ON actor_following(actor_id);
CREATE INDEX IF NOT EXISTS actor_following_target_actor_id ON actor_following(target_actor_id);
CREATE TABLE IF NOT EXISTS objects (
id TEXT PRIMARY KEY,
mastodon_id TEXT UNIQUE NOT NULL,
type TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now()),
original_actor_id TEXT,
original_object_id TEXT UNIQUE,
reply_to_object_id TEXT,
properties jsonb NOT NULL DEFAULT '{}'::jsonb,
local INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS inbox_objects (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE TABLE IF NOT EXISTS outbox_objects (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now()),
published_date timestamp NOT NULL DEFAULT (now())
);
CREATE TABLE IF NOT EXISTS actor_notifications (
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
actor_id TEXT NOT NULL,
from_actor_id TEXT NOT NULL,
object_id TEXT,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE INDEX IF NOT EXISTS actor_notifications_actor_id ON actor_notifications(actor_id);
CREATE TABLE IF NOT EXISTS actor_favourites (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE INDEX IF NOT EXISTS actor_favourites_actor_id ON actor_favourites(actor_id);
CREATE INDEX IF NOT EXISTS actor_favourites_object_id ON actor_favourites(object_id);
CREATE TABLE IF NOT EXISTS actor_reblogs (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE INDEX IF NOT EXISTS actor_reblogs_actor_id ON actor_reblogs(actor_id);
CREATE INDEX IF NOT EXISTS actor_reblogs_object_id ON actor_reblogs(object_id);
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
secret TEXT NOT NULL,
name TEXT NOT NULL,
redirect_uris TEXT NOT NULL,
website TEXT,
scopes TEXT,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE TABLE IF NOT EXISTS actor_replies (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
object_id TEXT NOT NULL,
in_reply_to_object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now())
);
CREATE INDEX IF NOT EXISTS actor_replies_in_reply_to_object_id ON actor_replies(in_reply_to_object_id);
-- Migration number: 0001 2023-01-16T13:09:04.033Z
CREATE UNIQUE INDEX unique_actor_following ON actor_following (actor_id, target_actor_id);
-- Migration number: 0002 2023-01-16T13:46:54.975Z
ALTER TABLE outbox_objects
ADD target TEXT NOT NULL DEFAULT 'https://www.w3.org/ns/activitystreams#Public';
-- Migration number: 0003 2023-02-02T15:03:27.478Z
CREATE TABLE IF NOT EXISTS peers (
domain TEXT UNIQUE NOT NULL
);
-- Migration number: 0004 2023-02-03T17:17:19.099Z
CREATE INDEX IF NOT EXISTS outbox_objects_actor_id ON outbox_objects(actor_id);
CREATE INDEX IF NOT EXISTS outbox_objects_target ON outbox_objects(target);
-- Migration number: 0005 2023-02-07T10:57:21.848Z
CREATE TABLE IF NOT EXISTS idempotency_keys (
key TEXT PRIMARY KEY,
object_id TEXT NOT NULL,
expires_at timestamp NOT NULL
);
-- Migration number: 0006 2023-02-13T11:18:03.485Z
CREATE TABLE IF NOT EXISTS note_hashtags (
value TEXT NOT NULL,
object_id TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now())
);
-- Migration number: 0007 2023-02-15T11:01:46.585Z
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
actor_id TEXT NOT NULL,
client_id TEXT NOT NULL,
endpoint TEXT NOT NULL,
key_p256dh TEXT NOT NULL,
key_auth TEXT NOT NULL,
alert_mention INTEGER NOT NULL,
alert_status INTEGER NOT NULL,
alert_reblog INTEGER NOT NULL,
alert_follow INTEGER NOT NULL,
alert_follow_request INTEGER NOT NULL,
alert_favourite INTEGER NOT NULL,
alert_poll INTEGER NOT NULL,
alert_update INTEGER NOT NULL,
alert_admin_sign_up INTEGER NOT NULL,
alert_admin_report INTEGER NOT NULL,
policy TEXT NOT NULL,
cdate timestamp NOT NULL DEFAULT (now()),
UNIQUE(actor_id, client_id)
);
-- Migration number: 0003 2023-02-24T15:03:27.478Z
CREATE TABLE IF NOT EXISTS server_settings (
setting_name TEXT UNIQUE NOT NULL,
setting_value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS server_rules (
id INTEGER PRIMARY KEY,
text TEXT NOT NULL
);
-- Migration number: 0009 2023-02-28T13:58:08.319Z
ALTER TABLE actors
ADD is_admin INTEGER;
UPDATE actors SET is_admin = 1
WHERE id =
(SELECT id
FROM actors
ORDER BY cdate ASC LIMIT 1 );
-- Migration number: 0010 2023-03-08T09:40:30.734Z
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO clients (id, secret, name, redirect_uris, scopes)
VALUES ('924801be-d211-495d-8cac-e73503413af8', encode(gen_random_bytes(42), 'hex'), 'Wildebeest User Interface', '/', 'all');

Wyświetl plik

@ -0,0 +1,132 @@
import * as neon from '@neondatabase/serverless'
import type { Database, Result, QueryBuilder } from 'wildebeest/backend/src/database'
import type { Env } from 'wildebeest/backend/src/types/env'
function sqliteToPsql(query: string): string {
let c = 0
return query.replace(/\?([0-9])?/g, (match: string, p1: string) => {
c += 1
return `$${p1 || c}`
})
}
const qb: QueryBuilder = {
jsonExtract(obj: string, prop: string): string {
return `jsonb_extract_path(${obj}, '${prop}')::text`
},
jsonExtractIsNull(obj: string, prop: string): string {
return `${qb.jsonExtract(obj, prop)} = 'null'`
},
set(array: string): string {
return `(SELECT value::text FROM json_array_elements_text(${array}))`
},
epoch(): string {
return 'epoch'
},
insertOrIgnore(q: string): string {
return `INSERT ${q} ON CONFLICT DO NOTHING`
},
psqlOnly(q: string): string {
return q
},
jsonSet(obj: string, field: string, value: string): string {
return `jsonb_set(${obj}, '{${field}}', ${value})`
},
}
export default async function make(env: Pick<Env, 'NEON_DATABASE_URL'>): Promise<Database> {
const client = new neon.Client(env.NEON_DATABASE_URL)
await client.connect()
return {
client: 'neon',
qb,
prepare(query: string) {
return new PreparedStatement(env, query, [], client)
},
dump() {
throw new Error('not implemented')
},
async batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]> {
const results = []
for (let i = 0, len = statements.length; i < len; i++) {
const query = sqliteToPsql(statements[i].query)
const result = await client.query(query, statements[i].values)
results.push({
results: result.rows as T[],
success: true,
meta: {},
})
}
return results
},
async exec<T = unknown>(query: string): Promise<Result<T>> {
throw new Error('not implemented')
console.log(query)
},
}
}
export class PreparedStatement {
private env: Pick<Env, 'NEON_DATABASE_URL'>
private client: neon.Client
public query: string
public values: any[]
constructor(env: Pick<Env, 'NEON_DATABASE_URL'>, query: string, values: any[], client: neon.Client) {
this.env = env
this.query = query
this.values = values
this.client = client
}
bind(...values: any[]): PreparedStatement {
return new PreparedStatement(this.env, this.query, [...this.values, ...values], this.client)
}
async first<T = unknown>(colName?: string): Promise<T> {
if (colName) {
throw new Error('not implemented')
}
const query = sqliteToPsql(this.query)
const results = await this.client.query(query, this.values)
if (results.rows.length !== 1) {
throw new Error(`expected a single row, returned ${results.rows.length} row(s)`)
}
return results.rows[0] as T
}
async run<T = unknown>(): Promise<Result<T>> {
return this.all()
}
async all<T = unknown>(): Promise<Result<T>> {
const query = sqliteToPsql(this.query)
const results = await this.client.query(query, this.values)
return {
results: results.rows as T[],
success: true,
meta: {},
}
}
async raw<T = unknown>(): Promise<T[]> {
throw new Error('not implemented')
}
}

Wyświetl plik

@ -89,5 +89,12 @@ SELECT
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))
if (privkey.buffer && privkey_salt.buffer) {
// neon.tech
return unwrapPrivateKey(instanceKey, new Uint8Array(privkey.buffer), new Uint8Array(privkey_salt.buffer))
} else {
// D1
return unwrapPrivateKey(instanceKey, new Uint8Array(privkey), new Uint8Array(privkey_salt))
}
}

Wyświetl plik

@ -6,16 +6,16 @@ export interface Client {
secret: string
name: string
redirect_uris: string
website: string
scopes: string
website?: string
}
export async function createClient(
db: Database,
name: string,
redirect_uris: string,
website: string,
scopes: string
scopes: string,
website?: string
): Promise<Client> {
const id = crypto.randomUUID()
@ -28,7 +28,10 @@ export async function createClient(
INSERT INTO clients (id, secret, name, redirect_uris, website, scopes)
VALUES (?, ?, ?, ?, ?, ?)
`
const { success, error } = await db.prepare(query).bind(id, secret, name, redirect_uris, website, scopes).run()
const { success, error } = await db
.prepare(query)
.bind(id, secret, name, redirect_uris, website === undefined ? null : website, scopes)
.run()
if (!success) {
throw new Error('SQL error: ' + error)
}

Wyświetl plik

@ -10,11 +10,12 @@ const STATE_ACCEPTED = 'accepted'
// During a migration we move the followers from the old Actor to the new
export async function moveFollowers(db: Database, actor: Actor, followers: Array<string>): Promise<void> {
const batch = []
const stmt = db.prepare(`
INSERT OR IGNORE
const stmt = db.prepare(
db.qb.insertOrIgnore(`
INTO actor_following (id, actor_id, target_actor_id, target_actor_acct, state)
VALUES (?1, ?2, ?3, ?4, 'accepted');
VALUES (?1, ?2, ?3, ?4, 'accepted')
`)
)
const actorId = actor.id.toString()
const actorAcc = urlToHandle(actor.id)
@ -32,11 +33,12 @@ export async function moveFollowers(db: Database, actor: Actor, followers: Array
export async function moveFollowing(db: Database, actor: Actor, followingActors: Array<string>): Promise<void> {
const batch = []
const stmt = db.prepare(`
INSERT OR IGNORE
const stmt = db.prepare(
db.qb.insertOrIgnore(`
INTO actor_following (id, actor_id, target_actor_id, target_actor_acct, state)
VALUES (?1, ?2, ?3, ?4, 'accepted');
VALUES (?1, ?2, ?3, ?4, 'accepted')
`)
)
const actorId = actor.id.toString()
@ -56,10 +58,10 @@ export async function moveFollowing(db: Database, actor: Actor, followingActors:
export async function addFollowing(db: Database, actor: Actor, target: Actor, targetAcct: string): Promise<string> {
const id = crypto.randomUUID()
const query = `
INSERT OR IGNORE INTO actor_following (id, actor_id, target_actor_id, state, target_actor_acct)
const query = db.qb.insertOrIgnore(`
INTO actor_following (id, actor_id, target_actor_id, state, target_actor_acct)
VALUES (?, ?, ?, ?, ?)
`
`)
const out = await db
.prepare(query)

Wyświetl plik

@ -36,7 +36,15 @@ export async function hasKey(db: Database, key: string): Promise<APObject | null
}
const result = results[0]
const properties = JSON.parse(result.properties)
let properties
if (typeof result.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = result.properties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(result.properties)
}
return {
published: new Date(result.cdate).toISOString(),

Wyświetl plik

@ -11,16 +11,26 @@ function tag(name: string, content: string, attrs: Record<string, string> = {}):
return `<${name}${htmlAttrs}>${content}</${name}>`
}
const linkRegex = /(^|\s|\b)(https?:\/\/[-\w@:%._+~#=]{2,256}\.[a-z]{2,6}\b(?:[-\w@:%_+.~#?&/=]*))(\b|\s|$)/g
const linkRegex = /(^|\s|\b)(https?:\/\/[-\w@:%._+~#=]{1,256}\.[a-z]{2,6}\b(?:[-\w@:%_+.~#?&/=]*))(\b|\s|$)/g
const mentionedEmailRegex = /(^|\s|\b|\W)@(\w+(?:[.-]?\w+)+@\w+(?:[.-]?\w+)+(?:\.\w{2,63})+)(\b|\s|$)/g
const tagRegex = /(^|\s|\b|\W)#(\w{2,63})(\b|\s|$)/g
/// Transform a text status into a HTML status; enriching it with links / mentions.
// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string, mentions: Array<Actor>): string {
const enrichedStatus = status
const anchorsPlaceholdersMap = new Map<string, string>()
const getLinkAnchorPlaceholder = (link: string) => {
const anchor = getLinkAnchor(link)
const placeholder = `%%%___-LINK-PLACEHOLDER-${crypto.randomUUID()}-__%%%`
anchorsPlaceholdersMap.set(placeholder, anchor)
return placeholder
}
let enrichedStatus = status
.replace(
linkRegex,
(_, matchPrefix: string, link: string, matchSuffix: string) =>
`${matchPrefix}${getLinkAnchor(link)}${matchSuffix}`
`${matchPrefix}${getLinkAnchorPlaceholder(link)}${matchSuffix}`
)
.replace(mentionedEmailRegex, (_, matchPrefix: string, email: string, matchSuffix: string) => {
// ensure that the match is part of the mentions array
@ -33,6 +43,15 @@ export function enrichStatus(status: string, mentions: Array<Actor>): string {
// otherwise the match isn't valid and we don't add HTML
return `${matchPrefix}${email}${matchSuffix}`
})
.replace(
tagRegex,
(_, matchPrefix: string, tag: string, matchSuffix: string) =>
`${matchPrefix}${/^\d+$/.test(tag) ? `#${tag}` : getTagAnchor(tag)}${matchSuffix}`
)
for (const [placeholder, anchor] of anchorsPlaceholdersMap.entries()) {
enrichedStatus = enrichedStatus.replace(placeholder, anchor)
}
return tag('p', enrichedStatus)
}
@ -60,3 +79,12 @@ function getLinkAnchor(link: string) {
return link
}
}
function getTagAnchor(hashTag: string) {
try {
return tag('a', `#${hashTag}`, { href: `/tags/${hashTag.replace(/^#/, '')}`, class: 'status-link hashtag' })
} catch (err: unknown) {
console.warn('failed to parse link', err)
return tag
}
}

Wyświetl plik

@ -225,7 +225,15 @@ export async function getNotifications(db: Database, actor: Actor, domain: strin
for (let i = 0, len = results.length; i < len; i++) {
const result = results[i]
const properties = JSON.parse(result.properties)
let properties
if (typeof result.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = result.properties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(result.properties)
}
const notifFromActorId = new URL(result.notif_from_actor_id)
const notifFromActor = await getActorById(db, notifFromActorId)

Wyświetl plik

@ -105,7 +105,15 @@ export async function toMastodonStatusFromRow(domain: string, db: Database, row:
console.warn('missing `row.publisher_actor_id`')
return null
}
const properties = JSON.parse(row.properties)
let properties
if (typeof row.properties === 'object') {
// neon uses JSONB for properties which is returned as a deserialized
// object.
properties = row.properties
} else {
// D1 uses a string for JSON properties
properties = JSON.parse(row.properties)
}
const actorId = new URL(row.publisher_actor_id)
const author = actors.personFromRow({

Wyświetl plik

@ -16,7 +16,7 @@ export async function getHomeTimeline(domain: string, db: Database, actor: Actor
`
SELECT
actor_following.target_actor_id as id,
json_extract(actors.properties, '$.followers') as actorFollowersURL
${db.qb.jsonExtract('actors.properties', 'followers')} as actorFollowersURL
FROM actor_following
INNER JOIN actors ON actors.id = actor_following.target_actor_id
WHERE actor_id=? AND state='accepted'
@ -60,10 +60,10 @@ INNER JOIN objects ON objects.id = outbox_objects.object_id
INNER JOIN actors ON actors.id = outbox_objects.actor_id
WHERE
objects.type = 'Note'
AND outbox_objects.actor_id IN (SELECT value FROM json_each(?2))
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN (SELECT value FROM json_each(?3)))
GROUP BY objects.id
AND outbox_objects.actor_id IN ${db.qb.set('?2')}
AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')}
AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN ${db.qb.set('?3')})
GROUP BY objects.id ${db.qb.psqlOnly(', actors.id, outbox_objects.actor_id, outbox_objects.published_date')}
ORDER by outbox_objects.published_date DESC
LIMIT ?4
`
@ -101,7 +101,7 @@ export enum LocalPreference {
function localPreferenceQuery(preference: LocalPreference): string {
switch (preference) {
case LocalPreference.NotSet:
return '1'
return 'true'
case LocalPreference.OnlyLocal:
return 'objects.local = 1'
case LocalPreference.OnlyRemote:
@ -136,10 +136,12 @@ INNER JOIN actors ON actors.id=outbox_objects.actor_id
LEFT JOIN note_hashtags ON objects.id=note_hashtags.object_id
WHERE objects.type='Note'
AND ${localPreferenceQuery(localPreference)}
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')}
AND outbox_objects.target = '${PUBLIC_GROUP}'
${hashtagFilter}
GROUP BY objects.id
GROUP BY objects.id ${db.qb.psqlOnly(
', actors.id, actors.cdate, actors.properties, outbox_objects.actor_id, outbox_objects.published_date'
)}
ORDER by outbox_objects.published_date DESC
LIMIT ?1 OFFSET ?2
`

Wyświetl plik

@ -97,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(getDatabase(context.env), clientId, payload.email, context))) {
if (!(await loadContextData(await getDatabase(context.env), clientId, payload.email, context))) {
return errors.notAuthorized('failed to load context data')
}

Wyświetl plik

@ -25,4 +25,6 @@ export interface Env {
SENTRY_DSN: string
SENTRY_ACCESS_CLIENT_ID: string
SENTRY_ACCESS_CLIENT_SECRET: string
NEON_DATABASE_URL?: string
}

Wyświetl plik

@ -0,0 +1,15 @@
import { type Database } from 'wildebeest/backend/src/database'
import { Person, personFromRow } from 'wildebeest/backend/src/activitypub/actors'
export async function getAdmins(db: Database): Promise<Person[]> {
let rows: unknown[] = []
try {
const stmt = db.prepare('SELECT * FROM actors WHERE is_admin=1')
const result = await stmt.all<unknown>()
rows = result.success ? (result.results as unknown[]) : []
} catch {
/* empty */
}
return rows.map(personFromRow)
}

Wyświetl plik

@ -0,0 +1,23 @@
import * as access from 'wildebeest/backend/src/access'
export function getJwtEmail(jwtCookie: string) {
let payload: access.JWTPayload
if (!jwtCookie) {
throw new Error('Missing Authorization')
}
try {
// TODO: eventually, verify the JWT with Access, however this
// is not critical.
payload = access.getPayload(jwtCookie)
} catch (e: unknown) {
const error = e as { stack: string; cause: string }
console.warn(error.stack, error.cause)
throw new Error('Failed to validate Access JWT')
}
if (!payload.email) {
throw new Error("The Access JWT doesn't contain an email")
}
return payload.email
}

Wyświetl plik

@ -0,0 +1,22 @@
import * as access from 'wildebeest/backend/src/access'
export async function isUserAuthenticated(request: Request, jwt: string, accessAuthDomain: string, accessAud: string) {
if (!jwt) return false
try {
const validate = access.generateValidator({
jwt,
domain: accessAuthDomain,
aud: accessAud,
})
await validate(new Request(request.url))
} catch {
return false
}
const identity = await access.getIdentity({ jwt, domain: accessAuthDomain })
if (identity) {
return true
}
return false
}

Wyświetl plik

@ -281,11 +281,12 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
if (h === 'request-line') {
if (!options.strict) {
const cf = (request as { cf?: IncomingRequestCfProperties }).cf
/*
* We allow headers from the older spec drafts if strict parsing isn't
* specified in options.
*/
parsed.signingString += request.method + ' ' + request.url + ' ' + request.cf?.httpProtocol
parsed.signingString += request.method + ' ' + request.url + ' ' + cf?.httpProtocol
} else {
/* Strict parsing doesn't allow older draft headers. */
throw new StrictParsingError('request-line is not a valid header ' + 'with strict parsing enabled.')

Wyświetl plik

@ -19,7 +19,8 @@ export function initSentry(request: Request, env: Env, context: any) {
request,
transportOptions: { headers },
})
const colo = request.cf && request.cf.colo ? request.cf.colo : 'UNKNOWN'
const cf = (request as { cf?: IncomingRequestCfProperties }).cf
const colo = cf?.colo ? cf.colo : 'UNKNOWN'
sentry.setTag('colo', colo)
// cf-connecting-ip should always be present, but if not we can fallback to XFF.

Wyświetl plik

@ -1,4 +1,5 @@
import * as actors from '../activitypub/actors'
import { type Database } from 'wildebeest/backend/src/database'
import type { Actor } from '../activitypub/actors'
export type WebFingerResponse = {
@ -11,7 +12,7 @@ const headers = {
accept: 'application/jrd+json',
}
export async function queryAcct(domain: string, db: D1Database, acct: string): Promise<Actor | null> {
export async function queryAcct(domain: string, db: Database, acct: string): Promise<Actor | null> {
const url = await queryAcctLink(domain, acct)
if (url === null) {
return null

Wyświetl plik

@ -26,12 +26,12 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
}
export function b64ToUrlEncoded(str: string): string {
return str.replaceAll(/\+/g, '-').replaceAll(/\//g, '_').replace(/=+/g, '')
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+/g, '')
}
export function urlEncodedToB64(str: string): string {
const padding = '='.repeat((4 - (str.length % 4)) % 4)
return str.replaceAll(/-/g, '+').replaceAll(/_/g, '/') + padding
return str.replace(/-/g, '+').replace(/_/g, '/') + padding
}
export function stringToU8Array(str: string): Uint8Array {

Wyświetl plik

@ -187,6 +187,9 @@ describe('Mastodon APIs', () => {
'http://www.cloudflare.co.uk?test=test@123',
'http://www.cloudflare.com/.com/?test=test@~123&a=b',
'https://developers.cloudflare.com/workers/runtime-apis/request/#background',
'https://a.test',
'https://a.test/test',
'https://a.test/test?test=test',
]
linksToTest.forEach((link) => {
const url = new URL(link)
@ -198,6 +201,51 @@ describe('Mastodon APIs', () => {
assert.equal(enrichStatus(`@!@£${link}!!!`, []), `<p>@!@£<a href="${link}">${urlDisplayText}</a>!!!</p>`)
})
})
test('convert tags to HTML', async () => {
const tagsToTest = [
{
tag: '#test',
expectedTagAnchor: '<a href="/tags/test" class="status-link hashtag">#test</a>',
},
{
tag: '#123_joke_123',
expectedTagAnchor: '<a href="/tags/123_joke_123" class="status-link hashtag">#123_joke_123</a>',
},
{
tag: '#_123',
expectedTagAnchor: '<a href="/tags/_123" class="status-link hashtag">#_123</a>',
},
{
tag: '#example:',
expectedTagAnchor: '<a href="/tags/example" class="status-link hashtag">#example</a>:',
},
{
tag: '#tagA#tagB',
expectedTagAnchor:
'<a href="/tags/tagA" class="status-link hashtag">#tagA</a><a href="/tags/tagB" class="status-link hashtag">#tagB</a>',
},
]
for (let i = 0, len = tagsToTest.length; i < len; i++) {
const { tag, expectedTagAnchor } = tagsToTest[i]
assert.equal(enrichStatus(`hey ${tag} hi`, []), `<p>hey ${expectedTagAnchor} hi</p>`)
assert.equal(enrichStatus(`${tag} hi`, []), `<p>${expectedTagAnchor} hi</p>`)
assert.equal(enrichStatus(`${tag}\n\thein`, []), `<p>${expectedTagAnchor}\n\thein</p>`)
assert.equal(enrichStatus(`hey ${tag}`, []), `<p>hey ${expectedTagAnchor}</p>`)
assert.equal(enrichStatus(`${tag}`, []), `<p>${expectedTagAnchor}</p>`)
assert.equal(enrichStatus(`@!@£${tag}!!!`, []), `<p>@!@£${expectedTagAnchor}!!!</p>`)
}
})
test('ignore invalid tags', () => {
assert.equal(enrichStatus('tags cannot be empty like: #', []), `<p>tags cannot be empty like: #</p>`)
assert.equal(
enrichStatus('tags cannot contain only numbers like: #123', []),
`<p>tags cannot contain only numbers like: #123</p>`
)
})
})
describe('Follow', () => {

Wyświetl plik

@ -36,6 +36,33 @@ describe('Mastodon APIs', () => {
assert.deepEqual(rest, {})
})
test('POST /apps registers client without website', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const request = new Request('https://example.com', {
method: 'POST',
body: '{"redirect_uris":"mastodon://example.com/oauth","client_name":"Example mastodon client","scopes":"read write follow push"}',
headers: {
'content-type': 'application/json',
},
})
const res = await apps.handleRequest(db, request, vapidKeys)
assert.equal(res.status, 200)
assertCORS(res)
assertJSON(res)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, redirect_uri, client_id, client_secret, vapid_key, id, ...rest } = await res.json<
Record<string, string>
>()
assert.equal(name, 'Example mastodon client')
assert.equal(redirect_uri, 'mastodon://example.com/oauth')
assert.equal(id, '20')
assert.deepEqual(rest, {})
})
test('POST /apps returns 422 for malformed requests', async () => {
// client_name and redirect_uris are required according to https://docs.joinmastodon.org/methods/apps/#form-data-parameters
const db = await makeDB()
@ -76,12 +103,14 @@ describe('Mastodon APIs', () => {
})
test('GET /apps is bad request', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const request = new Request('https://example.com')
const ctx: any = {
next: () => new Response(),
data: null,
env: {
DATABASE: db,
VAPID_JWK: JSON.stringify(vapidKeys),
},
request,

Wyświetl plik

@ -85,7 +85,7 @@ describe('Mastodon APIs', () => {
headers,
})
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
assert.equal(res.status, 403)
assert.equal(res.status, 422)
})
test('authorize redirects with code on success and show first login', async () => {

Wyświetl plik

@ -81,7 +81,7 @@ describe('Mastodon APIs', () => {
.prepare(
`
SELECT
json_extract(properties, '$.content') as content,
${db.qb.jsonExtract('properties', 'content')} as content,
original_actor_id,
original_object_id
FROM objects
@ -758,7 +758,7 @@ describe('Mastodon APIs', () => {
const row = await db
.prepare(
`
SELECT json_extract(properties, '$.inReplyTo') as inReplyTo
SELECT ${db.qb.jsonExtract('properties', 'inReplyTo')} as inReplyTo
FROM objects
WHERE mastodon_id=?
`

Wyświetl plik

@ -9,6 +9,7 @@ import * as path from 'path'
import { BetaDatabase } from '@miniflare/d1'
import * as SQLiteDatabase from 'better-sqlite3'
import { type Database } from 'wildebeest/backend/src/database'
import d1 from 'wildebeest/backend/src/database/d1'
export function isUrlValid(s: string) {
let url
@ -32,7 +33,8 @@ export async function makeDB(): Promise<Database> {
db.exec(content)
}
return db2 as unknown as Database
const env = { DATABASE: db2 } as any
return d1(env)
}
export function assertCORS(response: Response) {
@ -71,7 +73,7 @@ export async function createTestClient(
redirectUri: string = 'https://localhost',
scopes: string = 'read follow'
): Promise<Client> {
return createClient(db, 'test client', redirectUri, 'https://cloudflare.com', scopes)
return createClient(db, 'test client', redirectUri, scopes, 'https://cloudflare.com')
}
type TestQueue = Queue<any> & { messages: Array<any> }

Wyświetl plik

@ -1,7 +1,7 @@
import { makeDB } from '../utils'
import { strict as assert } from 'node:assert/strict'
import { createPerson, getActorById } from 'wildebeest/backend/src/activitypub/actors'
import * as account_alias from 'wildebeest/functions/api/wb/settings/account/alias'
import * as alias from 'wildebeest/backend/src/accounts/alias'
const domain = 'cloudflare.com'
const userKEK = 'test_kek22'
@ -39,14 +39,7 @@ describe('Wildebeest', () => {
throw new Error('unexpected request to ' + input)
}
const alias = 'test@example.com'
const req = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify({ alias }),
})
const res = await account_alias.handleRequestPost(db, req, actor)
assert.equal(res.status, 201)
await alias.addAlias(db, 'test@example.com', actor)
// Ensure the actor has the alias set
const newActor = await getActorById(db, actor.id)

Wyświetl plik

@ -8,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, getDatabase(env))
const targetActor = await actors.getAndCache(toActorId, await getDatabase(env))
if (targetActor === null) {
console.warn(`actor ${toActorId} not found`)
return
}
const signingKey = await getSigningKey(message.userKEK, getDatabase(env), actor)
const signingKey = await getSigningKey(message.userKEK, await getDatabase(env), actor)
await deliverToActor(signingKey, actor, targetActor, message.activity, env.DOMAIN)
}

Wyświetl plik

@ -9,7 +9,7 @@ import type { Env } from './'
export async function handleInboxMessage(env: Env, actor: Actor, message: InboxMessageBody) {
const domain = env.DOMAIN
const db = getDatabase(env)
const db = await getDatabase(env)
const adminEmail = env.ADMIN_EMAIL
const cache = cacheFromEnv(env)
const activity = message.activity

Wyświetl plik

@ -15,12 +15,14 @@ export type Env = {
SENTRY_DSN: string
SENTRY_ACCESS_CLIENT_ID: string
SENTRY_ACCESS_CLIENT_SECRET: string
NEON_DATABASE_URL?: string
}
export default {
async queue(batch: MessageBatch<MessageBody>, env: Env, ctx: ExecutionContext) {
const sentry = initSentryQueue(env, ctx)
const db = getDatabase(env)
const db = await getDatabase(env)
try {
for (const message of batch.messages) {

Wyświetl plik

@ -1,3 +1,4 @@
compatibility_date = "2023-01-09"
main = "./src/index.ts"
usage_model = "unbound"
node_compat = true

Wyświetl plik

@ -1,4 +1,4 @@
import { cloudflarePagesAdaptor } from '@builder.io/qwik-city/adaptors/cloudflare-pages/vite'
import { cloudflarePagesAdapter } from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'
import { extendConfig } from '@builder.io/qwik-city/vite'
import baseConfig from '../../vite.config'
@ -11,7 +11,7 @@ export default extendConfig(baseConfig, () => {
},
},
plugins: [
cloudflarePagesAdaptor({
cloudflarePagesAdapter({
// Do not SSG as the D1 database is not available at build time, I think.
// staticGenerate: true,
}),

Wyświetl plik

@ -7,6 +7,8 @@ import { createReply as createReplyInBackend } from 'wildebeest/backend/test/sha
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'
import { upsertRule } from 'wildebeest/backend/src/config/rules'
import { upsertServerSettings } from 'wildebeest/backend/src/config/server'
/**
* Run helper commands to initialize the database with actors, statuses, etc.
@ -41,6 +43,17 @@ export async function init(domain: string, db: Database) {
for (const reply of replies) {
await createReply(domain, db, reply, loadedStatuses)
}
await createServerData(db)
}
async function createServerData(db: Database) {
await upsertServerSettings(db, {
'extended description': 'this is a test wildebeest instance!',
})
await upsertRule(db, "don't be mean")
await upsertRule(db, "don't insult people")
await upsertRule(db, 'respect the rules')
}
/**
@ -74,12 +87,21 @@ async function getOrCreatePerson(
db: Database,
{ username, avatar, display_name }: Account
): Promise<Person> {
const person = await getPersonByEmail(db, username)
const isAdmin = username === 'george'
const email = `${username}@test.email`
const person = await getPersonByEmail(db, email)
if (person) return person
const newPerson = await createPerson(domain, db, 'test-kek', username, {
icon: { url: avatar },
name: display_name,
})
const newPerson = await createPerson(
domain,
db,
'test-kek',
email,
{
icon: { url: avatar },
name: display_name,
},
isAdmin
)
if (!newPerson) {
throw new Error('Could not create Actor ' + username)
}

Wyświetl plik

@ -7,14 +7,16 @@
},
"private": true,
"scripts": {
"pretypes-check": "yarn build",
"types-check": "tsc",
"lint": "eslint src mock-db adaptors",
"build": "vite build && vite build -c adaptors/cloudflare-pages/vite.config.ts",
"dev": "vite --mode ssr",
"watch": "concurrently \"vite build -w\" \"vite build -w -c adaptors/cloudflare-pages/vite.config.ts\""
"watch": "nodemon -w ./src --ext tsx,ts --exec npm run build"
},
"devDependencies": {
"@builder.io/qwik": "0.18.1",
"@builder.io/qwik-city": "0.2.1",
"@builder.io/qwik": "0.21.0",
"@builder.io/qwik-city": "0.4.0",
"@types/eslint": "8.4.10",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.16",
@ -22,12 +24,12 @@
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"autoprefixer": "10.4.11",
"concurrently": "^7.6.0",
"eslint": "8.30.0",
"eslint-plugin-qwik": "0.16.1",
"jest": "^29.3.1",
"lorem-ipsum": "^2.0.8",
"node-fetch": "3.3.0",
"nodemon": "^2.0.20",
"postcss": "^8.4.16",
"prettier": "2.8.1",
"sass": "^1.57.0",

Wyświetl plik

@ -0,0 +1,22 @@
import { component$ } from '@builder.io/qwik'
type Props = {
type: 'success' | 'failure'
message: string
}
export default component$(({ type, message }: Props) => {
const colorClasses = getColorClasses(type)
return <p class={`border mb-5 p-5 text-center rounded ${colorClasses}`}>{message}</p>
})
export function getColorClasses(type: Props['type']): string {
switch (type) {
case 'success':
return 'bg-green-800 border-green-700 text-green-100'
case 'failure':
return 'bg-red-800 border-red-700 text-red-100 text-green-100'
default:
return 'bg-green-800 border-green-700 text-green-100'
}
}

Wyświetl plik

@ -0,0 +1,23 @@
import { component$ } from '@builder.io/qwik'
import Spinner from '../Spinner'
type Props = {
loading: boolean
text: string
}
export const SubmitButton = component$<Props>(({ text, loading }) => {
return (
<button
type="submit"
class="w-full my-10 relative bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
>
{text}
{loading && (
<div class="absolute inset-0 bg-[#00000078] grid place-items-center cursor-progress">
<Spinner />
</div>
)}
</button>
)
})

Wyświetl plik

@ -0,0 +1,34 @@
import { component$, useSignal } from '@builder.io/qwik'
type Props = {
label: string
name?: string
description?: string
class?: string
invalid?: boolean
value?: string
required?: boolean
}
export const TextArea = component$<Props>(
({ class: className, label, name, description, invalid, value, required }) => {
const inputId = useSignal(`${label.replace(/\s+/g, '_')}___${crypto.randomUUID()}`).value
return (
<div class={`mb-6 ${className || ''}`}>
<label class="font-semibold block mb-1" for={inputId}>
{label}
{!!required && <span class="ml-1 text-red-500">*</span>}
</label>
{!!description && <div class="text-sm inline-block mb-2 text-wildebeest-400">{description}</div>}
<textarea
class={`bg-black text-white p-3 rounded outline-none border hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full ${
invalid ? 'border-red-500' : 'border-black'
}`}
id={inputId}
name={name}
value={value}
/>
</div>
)
}
)

Wyświetl plik

@ -0,0 +1,36 @@
import { component$, useSignal } from '@builder.io/qwik'
type Props = {
label: string
name?: string
description?: string
class?: string
invalid?: boolean
value?: string
required?: boolean
}
export const TextInput = component$<Props>(
({ class: className, label, name, description, invalid, value, required }) => {
const inputId = useSignal(`${label.replace(/\s+/g, '_')}___${crypto.randomUUID()}`).value
const includeDefaultMb = !/(^|\s)m[y,b]?-\S+(\s|$)/.test(className || '')
return (
<div class={`${className || ''} ${includeDefaultMb ? 'mb-6' : ''}`}>
<label class="font-semibold block mb-2" for={inputId}>
{label}
{!!required && <span class="ml-1 text-red-500">*</span>}
</label>
<input
class={`bg-black text-white p-3 mb-1 rounded outline-none border hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full ${
invalid ? 'border-red-500' : 'border-black'
}`}
type="text"
id={inputId}
name={name}
value={value}
/>
{!!description && <div class="text-sm text-wildebeest-400">{description}</div>}
</div>
)
}
)

Wyświetl plik

@ -0,0 +1,25 @@
import { component$ } from '@builder.io/qwik'
export default component$(() => {
return (
<div role="status">
<svg
aria-hidden="true"
class="w-6 h-6 mr-2 text-wildebeest-100 animate-spin fill-wildebeest-vibrant-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
)
})

Wyświetl plik

@ -1,30 +1,37 @@
import { $, component$, Slot } from '@builder.io/qwik'
import { useNavigate } from '@builder.io/qwik-city'
export default component$<{ withBackButton?: boolean }>(({ withBackButton }) => {
const nav = useNavigate()
export default component$<{ withBackButton?: boolean; backButtonPlacement?: 'start' | 'end' }>(
({ withBackButton, backButtonPlacement = 'start' }) => {
const nav = useNavigate()
const goBack = $(() => {
if (window.history.length > 1) {
window.history.back()
} else {
nav('/explore')
}
})
const goBack = $(() => {
if (window.history.length > 1) {
window.history.back()
} else {
nav('/explore')
}
})
return (
<header class="bg-wildebeest-900 sticky top-[3.9rem] xl:top-0 xl:pt-2.5 z-10">
<div class="flex bg-wildebeest-700 xl:rounded-t overflow-hidden">
{!!withBackButton && (
<div class="flex justify-between items-center bg-wildebeest-700">
<button class="text-semi no-underline text-wildebeest-vibrant-400 bg-transparent p-4" onClick$={goBack}>
<i class="fa fa-chevron-left mr-2 w-3 inline-block" />
<span class="hover:underline">Back</span>
</button>
</div>
)}
<Slot />
const backButton = !withBackButton ? (
// eslint-disable-next-line qwik/single-jsx-root
<></>
) : (
<div class="flex justify-between items-center bg-wildebeest-700">
<button class="text-semi no-underline text-wildebeest-vibrant-400 bg-transparent p-4" onClick$={goBack}>
<i class="fa fa-chevron-left mr-2 w-3 inline-block" />
<span class="hover:underline">Back</span>
</button>
</div>
</header>
)
})
)
return (
<header class="bg-wildebeest-900 sticky top-[3.9rem] xl:top-0 xl:pt-2.5 z-10">
<div class="flex bg-wildebeest-700 xl:rounded-t overflow-hidden">
{backButtonPlacement === 'start' && backButton}
<Slot />
{backButtonPlacement === 'end' && <div class="ml-auto">{backButton}</div>}
</div>
</header>
)
}
)

Wyświetl plik

@ -1,4 +1,5 @@
import { component$, useContext } from '@builder.io/qwik'
import { Link } from '@builder.io/qwik-city'
import { InstanceConfigContext } from '~/utils/instanceConfig'
import { useDomain } from '~/utils/useDomain'
@ -16,6 +17,12 @@ export default component$(() => {
<img class="w-full" src={config.thumbnail} alt="Society2 instance thumbnail" />
<p>{config.description}</p>
</div>
<Link
class="block text-wildebeest-500 border border-current my-4 p-2 text-center rounded-md no-underline"
href="/about"
>
Learn More
</Link>
</div>
)
})

Wyświetl plik

@ -1,6 +1,7 @@
import { component$ } from '@builder.io/qwik'
import { Link, useLocation } from '@builder.io/qwik-city'
import { WildebeestLogo } from '~/components/MastodonLogo'
import { authLoader } from '~/routes/layout'
type LinkConfig = {
iconName: string
@ -10,6 +11,7 @@ type LinkConfig = {
}
export default component$(() => {
const { isAuthorized, loginUrl } = authLoader().value
const location = useLocation()
const renderNavLink = ({ iconName, linkText, linkTarget, linkActiveRegex }: LinkConfig) => {
@ -35,7 +37,7 @@ export default component$(() => {
{ iconName: 'fa-globe', linkText: 'Federated', linkTarget: '/public', linkActiveRegex: /^\/public\/?$/ },
]
// const aboutLink = { iconName: 'fa-ellipsis', linkText: 'About', linkTarget: '/about', linkActiveRegex: /^\/about/ }
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 z-10">
@ -47,11 +49,25 @@ export default component$(() => {
</div>
<hr class="hidden xl:block border-t border-wildebeest-700 my-3" />
{links.map((link) => renderNavLink(link))}
{/* *********** Hiding the about link until the backend support is available ***************** */}
{/* <div class="xl:hidden">
<div class="xl:hidden">
<hr class="border-t border-wildebeest-700 my-3" />
{renderNavLink(aboutLink)}
</div> */}
</div>
{!isAuthorized && (
<a
class="w-full block mb-4 no-underline text-center bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
href={`${loginUrl}`}
>
Sign in
</a>
)}
{isAuthorized && (
<a class="text-semi no-underline" href="/settings/migration">
<i class="fa fa-gear mx-3 w-4" />
Preferences
</a>
)}
</div>
</div>
)

Wyświetl plik

@ -31,7 +31,15 @@ const mastodonRawStatuses: MastodonStatus[] = [
content: '<span>A very simple update: all good!</span>',
account: ben,
}),
generateDummyStatus({ content: '<p>Hi! My name is Rafael! 👋</p>', account: rafael, spoiler_text: 'who am I?' }),
generateDummyStatus({
content: '<p>Hi! My name is Rafael! 👋</p>',
account: rafael,
spoiler_text: 'who am I?',
}),
generateDummyStatus({
content: '<p>Hi! I made a funny! 🤭 <a href="/tags/joke" class="status-link hashtag">#joke</a></p>',
account: george,
}),
generateDummyStatus({
content: "<div><p>I'm Rafael and I am a web designer!</p><p>💪💪</p></div>",
account: rafael,

Wyświetl plik

@ -4,13 +4,58 @@
* It's the entry point for cloudflare-pages when building for production.
*
* Learn more about the cloudflare integration here:
* - https://qwik.builder.io/qwikcity/adaptors/cloudflare-pages/
* - https://qwik.builder.io/integrations/deployments/cloudflare-pages/#cloudflare-pages-entry-middleware
*
*/
import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages'
import qwikCityPlan from '@qwik-city-plan'
import render from './entry.ssr'
import type { Env } from 'wildebeest/backend/src/types/env'
import type { ContextData } from 'wildebeest/backend/src/types/context'
import { parse } from 'cookie'
import * as access from 'wildebeest/backend/src/access'
import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail'
import * as errors from 'wildebeest/backend/src/errors'
import * as actors from 'wildebeest/backend/src/activitypub/actors'
import { getDatabase } from 'wildebeest/backend/src/database'
import type { Person } from 'wildebeest/backend/src/activitypub/actors'
const onRequest = createQwikCity({ render, qwikCityPlan })
const qwikHandler = createQwikCity({ render, qwikCityPlan })
export { onRequest }
type QwikContextData = {
connectedActor: Person | null
}
// eslint-disable-next-line
export const onRequest: PagesFunction<Env, any, ContextData> = async (ctx) => {
const cookie = parse(ctx.request.headers.get('Cookie') || '')
const jwt = cookie['CF_Authorization']
const data: QwikContextData = {
connectedActor: null,
}
if (jwt) {
const validate = access.generateValidator({
jwt,
domain: ctx.env.ACCESS_AUTH_DOMAIN,
aud: ctx.env.ACCESS_AUD,
})
await validate(ctx.request)
let email = ''
try {
email = getJwtEmail(jwt ?? '')
} catch (e) {
return errors.notAuthorized((e as Error)?.message)
}
const db = await getDatabase(ctx.env)
data.connectedActor = await actors.getPersonByEmail(db, email)
}
// eslint-disable-next-line
;(ctx.env as any).data = data
return qwikHandler(ctx)
}

Wyświetl plik

@ -1,5 +1,4 @@
import { component$ } from '@builder.io/qwik'
import * as access from 'wildebeest/backend/src/access'
import type { Client } from 'wildebeest/backend/src/mastodon/client'
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
@ -9,67 +8,59 @@ import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
import { buildRedirect } from 'wildebeest/functions/oauth/authorize'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail'
export const clientLoader = loader$<Promise<Client>, { DATABASE: D1Database }>(async ({ platform, query, html }) => {
export const clientLoader = loader$<Promise<Client>>(async ({ platform, query, html }) => {
const client_id = query.get('client_id') || ''
let client: Client | null = null
try {
client = await getClientById(getDatabase(platform), client_id)
client = await getClientById(await getDatabase(platform), client_id)
} 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) {
throw new Error('client not found')
throw html(500, getErrorHtml('client not found'))
}
return client
})
export const userLoader = loader$<
Promise<{ email: string; avatar: URL; name: string; url: URL }>,
{ DATABASE: D1Database; domain: string }
>(async ({ cookie, platform, html, request, redirect, text }) => {
const jwt = cookie.get('CF_Authorization')
if (jwt === null) {
throw html(500, getErrorHtml('Missing Authorization'))
}
let payload: access.JWTPayload
try {
// TODO: eventually, verify the JWT with Access, however this
// is not critical.
payload = access.getPayload(jwt.value)
} 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'))
}
if (!payload.email) {
throw html(500, getErrorHtml("The Access JWT doesn't contain an email"))
}
const person = await getPersonByEmail(getDatabase(platform), payload.email)
if (person === null) {
const isFirstLogin = true
const res = await buildRedirect(getDatabase(platform), request as Request, isFirstLogin, jwt.value)
if (res.status === 302) {
throw redirect(302, res.headers.get('location') || '')
} else {
throw text(res.status, await res.text())
export const userLoader = loader$<Promise<{ email: string; avatar: URL; name: string; url: URL }>>(
async ({ cookie, platform, html, request, redirect, text }) => {
const jwt = cookie.get('CF_Authorization')
let email = ''
try {
email = getJwtEmail(jwt?.value ?? '')
} catch (e) {
throw html(500, getErrorHtml((e as Error)?.message))
}
const person = await getPersonByEmail(await getDatabase(platform), email)
if (person === null) {
const isFirstLogin = true
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-- jwt is defined otherwise getJwtEmail would have thrown
*/
const res = await buildRedirect(await getDatabase(platform), request as Request, isFirstLogin, jwt!.value)
if (res.status === 302) {
throw redirect(302, res.headers.get('location') || '')
} else {
throw text(res.status, await res.text())
}
}
const name = person.name
const avatar = person.icon?.url
const url = person.url
if (!name || !avatar) {
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
}
return { email, avatar, name, url }
}
const name = person.name
const avatar = person.icon?.url
const url = person.url
if (!name || !avatar) {
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
}
return { email: payload.email, avatar, name, url }
})
)
export default component$(() => {
const client = clientLoader().value

Wyświetl plik

@ -0,0 +1,11 @@
import { component$, Slot } from '@builder.io/qwik'
export { adminLoader } from '~/utils/adminLoader'
export default component$(() => {
return (
<>
<Slot />
</>
)
})

Wyświetl plik

@ -0,0 +1,84 @@
import { component$ } from '@builder.io/qwik'
export default component$(() => {
return (
<div class="max-w-4xl py-14 px-8">
<h2 class="text-2xl font-bold mb-10">Account Migration</h2>
<div class="text-green-700 mb-10">Your account is not currently being redirected to any other account.</div>
<h3 class="text-xl mb-6">Move to a different account</h3>
<p class="text-sm text-wildebeest-400 mb-5">Before proceeding, please read these notes carefully:</p>
<ul class="list-disc list-inside text-sm text-yellow-500 mb-5">
<li class="pb-1">This action will move all followers from the current account to the new account</li>
<li class="pb-1">
Your current account's profile will be updated with a redirect notice and be excluded from searches
</li>
<li class="pb-1">No other data will be moved automatically</li>
<li class="pb-1">The new account must first be configured to back-reference this one</li>
<li class="pb-1">After moving there is a waiting period during which you will not be able to move again</li>
<li class="pb-1">
Your current account will not be fully usable afterwards. However, you will have access to data export as well
as re-activation.
</li>
</ul>
<p class="text-sm text-wildebeest-400 mb-10">
Alternatively, you can <a href="/settings/aliases">only put up a redirect on your profile. </a>
</p>
<div class="flex">
<div class="pr-3">
<div class="my-5">
<label class="font-semibold mb-3" for="old-account">
Handle of the new account
<span class="ml-1 text-red-500">*</span>
</label>
<div class="text-sm text-wildebeest-400">
Specify the username@domain of the account you want to move to
</div>
</div>
<input
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full mb-5"
type="text"
name="old-account"
id="old-account"
/>
</div>
<div class="pl-3">
<div class="my-5">
<label class="font-semibold mb-3" for="password">
Current Password
<span class="ml-1 text-red-500">*</span>
</label>
<div class="text-sm text-wildebeest-400">
For security purposes please enter the password of the current account
</div>
</div>
<input
class="bg-black text-white p-3 rounded outline-none border border-red-500 hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full mb-5"
type="password"
name="password"
id="password"
/>
</div>
</div>
<button
type="submit"
class="w-full mb-10 uppercase bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
>
Create Alias
</button>
<h3 class="text-xl mt-4 mb-8">Moving from a different account</h3>
<p class="text-sm text-wildebeest-400 mb-5">
To move from another account to this one, first you need to{' '}
<a href="/settings/aliases">create an account alias</a>.
</p>
</div>
)
})

Wyświetl plik

@ -0,0 +1,73 @@
import { component$ } from '@builder.io/qwik'
import { action$, Form, Link, z, zod$ } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { updateSettings } from 'wildebeest/backend/src/config/server'
import { TextArea } from '~/components/Settings/TextArea'
import { serverSettingsLoader } from '../layout'
import { SubmitButton } from '~/components/Settings/SubmitButton'
import ResultMessage from '~/components/ResultMessage'
const zodSchema = zod$({
'extended description': z.string().min(1),
'privacy policy': z.string().optional(),
})
export type ServerAboutData = Awaited<typeof zodSchema>['_type']
export const action = action$(async (data, { platform }) => {
const db = await getDatabase(platform)
let success = false
try {
await updateSettings(db, data)
success = true
} catch (e: unknown) {
success = false
}
return {
success,
}
}, zodSchema)
export default component$(() => {
const existingSettings = serverSettingsLoader()
const saveAction = action()
const showSuccessfulResult = !!saveAction.value?.success
const showUnsuccessfulResult = !!saveAction.value && !saveAction.value.success
return (
<Form action={saveAction}>
<p class="mt-12 mb-9">Provide in-depth information about how the server is operated, moderated, funded.</p>
<div class="mb-12">
<TextArea
class="mb-1"
label="Extended description"
name="extended description"
description="Any additional information that may be useful to visitors and your users. Can be structured with Markdown syntax."
value={existingSettings.value['extended description']}
/>
<div class="text-sm text-wildebeest-400">
There is a dedicated area for rules that your users are expected to adhere to{' '}
<Link href="/settings/server-settings/rules">Manage server rules</Link>
</div>
</div>
<TextArea
label="Privacy Policy"
description="Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax."
name="privacy policy"
value={existingSettings.value['privacy policy']}
/>
{showSuccessfulResult && <ResultMessage type="success" message="The changes have been saved successfully." />}
{showUnsuccessfulResult && (
<ResultMessage type="failure" message="There was an error and changes couldn't be saved." />
)}
<SubmitButton text="Save Changes" loading={saveAction.isRunning} />
</Form>
)
})

Wyświetl plik

@ -0,0 +1,75 @@
import { component$ } from '@builder.io/qwik'
import { action$, Form, zod$, z } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { updateSettings } from 'wildebeest/backend/src/config/server'
import { TextArea } from '~/components/Settings/TextArea'
import { TextInput } from '~/components/Settings/TextInput'
import { serverSettingsLoader } from '../layout'
import ResultMessage from '~/components/ResultMessage'
import { SubmitButton } from '~/components/Settings/SubmitButton'
const zodSchema = zod$({
'server name': z.string().min(1),
'server description': z.string().min(1),
})
export type ServerBrandingData = Awaited<typeof zodSchema>['_type']
export const action = action$(async (data, { platform }) => {
const db = await getDatabase(platform)
let success = false
try {
await updateSettings(db, data)
success = true
} catch (e: unknown) {
success = false
}
return {
success,
}
}, zodSchema)
export default component$(() => {
const existingSettings = serverSettingsLoader()
const saveAction = action()
const showSuccessfulResult = !!saveAction.value?.success
const showUnsuccessfulResult = !!saveAction.value && !saveAction.value.success
return (
<Form action={saveAction}>
<p class="mt-12 mb-9">
Your server's branding differentiates it from other servers in the network. This information may be displayed
across a variety of environments, such as Mastodon's web interface, native applications, in link previews on
other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear,
short and concise.
</p>
<TextInput
class="mb-9"
label="Server name"
name="server name"
value={existingSettings.value['server name']}
invalid={!!saveAction.value?.fieldErrors?.['server name']}
description="How people may refer to your server besides its domain name."
/>
<TextArea
label="Server description"
name="server description"
value={existingSettings.value['server description']}
invalid={!!saveAction.value?.fieldErrors?.['server description']}
description="A short description to help uniquely identify your server. Who is running it, who is it for?"
/>
{showSuccessfulResult && <ResultMessage type="success" message="The changes have been saved successfully." />}
{showUnsuccessfulResult && (
<ResultMessage type="failure" message="There was an error and changes couldn't be saved." />
)}
<SubmitButton text="Save Changes" loading={saveAction.isRunning} />
</Form>
)
})

Wyświetl plik

@ -0,0 +1,8 @@
import { component$ } from '@builder.io/qwik'
import { loader$ } from '@builder.io/qwik-city'
export const loader = loader$(({ redirect }) => {
redirect(303, 'server-settings/branding')
})
export default component$(() => <></>)

Wyświetl plik

@ -0,0 +1,68 @@
import { component$, Slot } from '@builder.io/qwik'
import { Link, loader$, useLocation } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getSettings } from 'wildebeest/backend/src/config/server'
import { ServerAboutData } from './about'
import { ServerBrandingData } from './branding'
export type ServerSettingsData = ServerBrandingData & ServerAboutData
export const serverSettingsLoader = loader$<Promise<Partial<ServerSettingsData>>>(async ({ platform }) => {
const database = await getDatabase(platform)
const settings = await getSettings(database)
return JSON.parse(JSON.stringify(settings))
})
export default component$(() => {
const sectionLinks = [
{
text: 'Branding',
faIcon: 'fa-pen',
path: 'branding',
},
{
text: 'About',
faIcon: 'fa-file-lines',
path: 'about',
},
{
text: 'Rules',
faIcon: 'fa-pen-ruler',
path: 'rules',
},
] as const
const currentPath = useLocation().url.pathname.replace(/\/$/, '')
return (
<div class="max-w-4xl py-14 px-8">
<h2 class="text-2xl font-bold mb-6">Server Settings</h2>
<ul class="flex gap-4 mb-6">
{sectionLinks.map(({ text, faIcon, path }) => {
const isActive = currentPath.endsWith(path)
return (
<Link
key={text}
class={`
py-2 px-3 rounded text-sm no-underline flex gap-2
${
isActive
? 'bg-wildebeest-vibrant-500 hover:bg-wildebeest-vibrant-400 focus-visible:bg-wildebeest-vibrant-400'
: 'hover:bg-wildebeest-700 focus-visible:bg-wildebeest-700'
}`}
href={`/settings/server-settings/${path}`}
>
<i class={`fa-solid ${faIcon} leading-normal w-3 h-3`}></i>
{text}
</Link>
)
})}
</ul>
<Slot />
</div>
)
})

Wyświetl plik

@ -0,0 +1,84 @@
import { component$ } from '@builder.io/qwik'
import { action$, Form, loader$, useNavigate, z, zod$ } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getRules, upsertRule } from 'wildebeest/backend/src/config/rules'
import { TextArea } from '~/components/Settings/TextArea'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
import { SubmitButton } from '~/components/Settings/SubmitButton'
export type ServerSettingsData = { rules: string[] }
export const editAction = action$(
async (data, { platform }) => {
let success = false
try {
const result = await upsertRule(await getDatabase(platform), {
id: +data.id,
text: data.text,
})
success = result.success
} catch (e: unknown) {
success = false
}
return {
success,
}
},
zod$({
id: z.string().min(1),
text: z.string().min(1),
})
)
export const ruleLoader = loader$<Promise<{ id: number; text: string }>>(async ({ params, platform, html }) => {
const database = await getDatabase(platform)
const rules = await getRules(database)
const rule: { id: string; text: string } | undefined = rules.find((r) => r.id === params['id'])
if (!rule) {
throw html(404, getErrorHtml('The selected rule could not be found'))
}
return JSON.parse(JSON.stringify(rule))
})
export default component$(() => {
const rule = ruleLoader()
const editActionObj = editAction()
const nav = useNavigate()
if (editActionObj.value?.success) {
nav('/settings/server-settings/rules')
}
return (
<>
<Form action={editActionObj}>
<p class="mt-12 mb-9">
While most claim to have read and agree to the terms of service, usually people do not read through until
after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat
bullet point list. Try to keep individual rules short and simple, but try not to split them up into many
separate items either.
</p>
<input hidden name="id" value={rule.value.id} />
<div class="mb-12">
<TextArea
class="mb-1"
label="Rule"
required
name="text"
value={rule.value.text}
description="Describe a rule or requirement for users on this server. Try to keep it short and simple."
/>
</div>
<SubmitButton text="Save Changes" loading={editActionObj.isRunning} />
</Form>
</>
)
})

Wyświetl plik

@ -0,0 +1,109 @@
import { component$ } from '@builder.io/qwik'
import { action$, Form, Link, loader$, z, zod$ } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getRules, deleteRule, upsertRule } from 'wildebeest/backend/src/config/rules'
import { TextArea } from '~/components/Settings/TextArea'
import { SubmitButton } from '~/components/Settings/SubmitButton'
export type ServerSettingsData = { rules: string[] }
export const addAction = action$(
async (data, { platform }) => {
let success = false
try {
const result = await upsertRule(await getDatabase(platform), data.text)
success = result.success
} catch (e: unknown) {
success = false
}
return {
success,
}
},
zod$({
text: z.string().min(1),
})
)
export const deleteAction = action$(
async (data, { platform }) => {
let success = false
try {
const result = await deleteRule(await getDatabase(platform), data.id)
success = result.success
} catch (e: unknown) {
success = false
}
return {
success,
}
},
zod$({
id: z.number(),
})
)
export const rulesLoader = loader$<Promise<{ id: number; text: string }[]>>(async ({ platform }) => {
const database = await getDatabase(platform)
const rules = await getRules(database)
return JSON.parse(JSON.stringify(rules))
})
export default component$(() => {
const rules = rulesLoader()
const addActionObj = addAction()
const deleteActionObj = deleteAction()
return (
<>
<Form action={addActionObj} spaReset>
<p class="mt-12 mb-9">
While most claim to have read and agree to the terms of service, usually people do not read through until
after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat
bullet point list. Try to keep individual rules short and simple, but try not to split them up into many
separate items either.
</p>
<div class="mb-12">
<TextArea
class="mb-1"
label="Rule"
name="text"
required
description="Describe a rule or requirement for users on this server. Try to keep it short and simple."
/>
</div>
<SubmitButton text="Add Rule" loading={addActionObj.isRunning} />
</Form>
<div>
{rules.value.map(({ id, text }, idx) => {
const ruleNumber = idx + 1
const ruleBtnText = `${ruleNumber}. ${text.slice(0, 27)}${text.length > 27 ? '...' : ''}`
return (
<div key={id} class="p-4 my-4 bg-wildebeest-600 rounded">
<Link href={`./edit/${id}`} class="max-w-max inline-block mb-4 no-underline text-lg font-semibold">
{ruleBtnText}
</Link>
<div class="flex justify-between text-wildebeest-400">
<span>{text}</span>
<button
onClick$={() => {
if (confirm('Are you sure?')) {
deleteActionObj.run({ id })
}
}}
>
<i class="fa-solid fa-trash"></i> Delete
</button>
</div>
</div>
)
})}
</div>
</>
)
})

Wyświetl plik

@ -0,0 +1,115 @@
import { component$, useStore, $ } from '@builder.io/qwik'
import { getDatabase } from 'wildebeest/backend/src/database'
import { action$, Form, zod$, z } from '@builder.io/qwik-city'
import { addAlias } from 'wildebeest/backend/src/accounts/alias'
import ResultMessage from '~/components/ResultMessage'
const zodSchema = zod$({
alias: z.string().min(1),
})
export const action = action$(async (data, { platform, json }) => {
const db = await getDatabase(platform)
const connectedActor = platform.data.connectedActor
if (connectedActor === null) {
throw json(500, { error: 'user not present in context' })
}
try {
await addAlias(db, data.alias, connectedActor)
} catch (e: unknown) {
const error = e as { stack: string; cause: string }
console.error(error.stack, error.cause)
throw json(500, { error: 'failed to add alias' })
}
return {
success: true,
}
}, zodSchema)
export default component$(() => {
const state = useStore({ alias: '' })
const handleInput = $((event: Event) => {
state.alias = (event.target as HTMLInputElement).value
})
const saveAction = action()
return (
<Form class="login-form" action={saveAction}>
<div class="max-w-4xl py-14 px-8">
<h2 class="text-2xl font-bold mb-6">Account Aliases</h2>
{!!saveAction.value && (
<ResultMessage
type={saveAction.value.success ? 'success' : 'failure'}
message={
saveAction.value.success
? 'Successfully created a new alias. You can now initiate the move from the old account.'
: 'Failed to create alias.'
}
/>
)}
<p class="text-sm text-wildebeest-400 mb-10">
If you want to move from another account to this one, here you can create an alias, which is required before
you can proceed with moving followers from the old account to this one. This action by itself is harmless and
reversible. The account migration is initiated from the old account.
</p>
<div class="my-5">
<label class="font-semibold mb-3" for="alias">
Handle of the old account
<span class="ml-1 text-red-500">*</span>
</label>
<div class="text-sm text-wildebeest-400">
Specify the username@domain of the account you want to move from
</div>
</div>
<input
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full mb-5"
type="text"
id="alias"
name="alias"
value={state.alias}
onInput$={handleInput}
/>
<button
type="submit"
class="w-full uppercase mb-9 bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
>
Create Alias
</button>
{/* <table class="table-auto w-full">
<thead class="border-gray-600 border-b-2">
<th class="text-left py-2">Handle of the old account</th>
<th></th>
</thead>
<tbody>
<tr class="border-gray-600 border-t">
<td class="py-2">test</td>
<td class="py-2">
<div class="text-wildebeest-400 hover:text-white cursor-pointer">
<i class="fa fa-trash fa-fw fa-xs mr-1" />
Unlink Alias
</div>
</td>
</tr>
<tr class="border-gray-600 border-t">
<td class="py-2">test 2</td>
<td class="py-2">
<div class="text-wildebeest-400 hover:text-white cursor-pointer">
<i class="fa fa-trash fa-fw fa-xs mr-1" />
Unlink Alias
</div>
</td>
</tr>
</tbody>
</table> */}
</div>
</Form>
)
})

Wyświetl plik

@ -0,0 +1,11 @@
import { component$, Slot } from '@builder.io/qwik'
export { authLoader } from '~/utils/authLoader'
export default component$(() => {
return (
<>
<Slot />
</>
)
})

Wyświetl plik

@ -0,0 +1,6 @@
import { component$ } from '@builder.io/qwik'
export default component$(() => {
// In the future, a settings homepage will be here
return <div></div>
})

Wyświetl plik

@ -0,0 +1,42 @@
import { component$, Slot } from '@builder.io/qwik'
import { WildebeestLogo } from '~/components/MastodonLogo'
export default component$(() => {
return (
<div class="flex w-screen min-h-screen justify-center">
<AccountSidebar />
<div class="flex-auto">
<Slot />
</div>
<div class="flex-auto" />
</div>
)
})
export const AccountSidebar = component$(() => {
return (
<div class="bg-wildebeest-800 min-h-full flex-auto">
<div class="flex flex-col items-end">
<div class="my-12 mr-6">
<WildebeestLogo size="large" />
</div>
<a class="text-semi no-underline text-wildebeest-vibrant-400 bg-transparent p-4" href="/">
<i class="fa fa-chevron-left mr-2 w-3 inline-block" />
<span class="hover:underline">Back to Wildebeest</span>
</a>
<ul class="mr-5">
{/* <li class="mb-3">
<a class="no-underline text-right text-wildebeest-400 hover:text-wildebeest-200" href="/settings/migration">
Account Migration
</a>
</li> */}
<li class="mb-3">
<a class="no-underline text-right text-wildebeest-400 hover:text-wildebeest-200" href="/settings/aliases">
Account Aliases
</a>
</li>
</ul>
</div>
</div>
)
})

Wyświetl plik

@ -1,5 +1,5 @@
import { component$ } from '@builder.io/qwik'
import { Database, getDatabase } from 'wildebeest/backend/src/database'
import { getDatabase } from 'wildebeest/backend/src/database'
import { MastodonStatus, StatusContext } from '~/types'
import Status from '~/components/Status'
import * as statusAPI from 'wildebeest/functions/api/v1/statuses/[id]'
@ -12,14 +12,13 @@ import { getDocumentHead } from '~/utils/getDocumentHead'
import { Person } from 'wildebeest/backend/src/activitypub/actors'
export const statusLoader = loader$<
Promise<{ status: MastodonStatus; statusTextContent: string; context: StatusContext }>,
{ DATABASE: Database }
Promise<{ status: MastodonStatus; statusTextContent: string; context: StatusContext }>
>(async ({ request, html, platform, params }) => {
const domain = new URL(request.url).hostname
let statusText = ''
try {
const statusResponse = await statusAPI.handleRequestGet(
getDatabase(platform),
await getDatabase(platform),
params.statusId,
domain,
{} as Person
@ -37,7 +36,7 @@ export const statusLoader = loader$<
const statusTextContent = await getTextContent(status.content)
try {
const contextResponse = await contextAPI.handleRequest(domain, getDatabase(platform), params.statusId)
const contextResponse = await contextAPI.handleRequest(domain, await getDatabase(platform), params.statusId)
const contextText = await contextResponse.text()
const context = JSON.parse(contextText ?? null) as StatusContext | null
if (!context) {

Wyświetl plik

@ -11,8 +11,7 @@ export const statusesLoader = loader$<
Promise<{
accountId: string
statuses: MastodonStatus[]
}>,
{ DATABASE: D1Database }
}>
>(async ({ platform, request, html }) => {
let statuses: MastodonStatus[] = []
let accountId = ''
@ -22,7 +21,7 @@ export const statusesLoader = loader$<
const handle = parseHandle(accountId)
accountId = handle.localPart
const response = await getLocalStatuses(request as Request, getDatabase(platform), handle, 0, false)
const response = await getLocalStatuses(request as Request, await getDatabase(platform), handle, 0, false)
statuses = await response.json<Array<MastodonStatus>>()
} catch {
throw html(

Wyświetl plik

@ -12,10 +12,10 @@ import { getDocumentHead } from '~/utils/getDocumentHead'
import * as statusAPI from 'wildebeest/functions/api/v1/statuses/[id]'
import { useAccountUrl } from '~/utils/useAccountUrl'
import { getDatabase } from 'wildebeest/backend/src/database'
import { Person } from 'wildebeest/backend/src/activitypub/actors'
export const accountPageLoader = loader$<
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>,
{ DATABASE: D1Database }
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>
>(async ({ platform, params, request, html }) => {
let isValidStatus = false
let account: MastodonAccount | null = null
@ -25,14 +25,19 @@ export const accountPageLoader = loader$<
const accountId = url.pathname.split('/')[1]
try {
const statusResponse = await statusAPI.handleRequestGet(getDatabase(platform), params.statusId, domain)
const statusResponse = await statusAPI.handleRequestGet(
await getDatabase(platform),
params.statusId,
domain,
null as unknown as Person
)
const statusText = await statusResponse.text()
isValidStatus = !!statusText
} catch {
isValidStatus = false
}
account = await getAccount(domain, accountId, getDatabase(platform))
account = await getAccount(domain, accountId, await getDatabase(platform))
} catch {
throw html(
500,

Wyświetl plik

@ -12,8 +12,7 @@ export const statusesLoader = loader$<
Promise<{
accountId: string
statuses: MastodonStatus[]
}>,
{ DATABASE: D1Database }
}>
>(async ({ platform, request, html }) => {
let statuses: MastodonStatus[] = []
let accountId = ''
@ -23,7 +22,7 @@ export const statusesLoader = loader$<
const handle = parseHandle(accountId)
accountId = handle.localPart
const response = await getLocalStatuses(request as Request, getDatabase(platform), handle, 0, true)
const response = await getLocalStatuses(request as Request, await getDatabase(platform), handle, 0, true)
statuses = await response.json<Array<MastodonStatus>>()
} catch {
throw html(

Wyświetl plik

@ -1,66 +1,55 @@
import { component$ } from '@builder.io/qwik'
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
import { getSettings } from 'wildebeest/backend/src/config/server'
import { getRules } from 'wildebeest/backend/src/config/rules'
import { Accordion } from '~/components/Accordion/Accordion'
import { AccountCard } from '~/components/AccountCard/AccountCard'
import { HtmlContent } from '~/components/HtmlContent/HtmlContent'
import { george } from '~/dummyData/accounts'
import { Account } from '~/types'
import { getDocumentHead } from '~/utils/getDocumentHead'
import { instanceLoader } from '../layout'
import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors'
import { loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
import { AccountCard } from '~/components/AccountCard/AccountCard'
import { getAdmins } from 'wildebeest/backend/src/utils/auth/getAdmins'
type AboutInfo = {
image: string
domain: string
contact: {
account: Account
email: string
}
rules: { id: string; text: string }[]
admin: { account: Account | null; email: string }
rules: { id: number; text: string }[]
extended_description: {
updated_at: string
content: string
}
}
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, redirect }) => {
// TODO: properly implement loader and remove redirect
throw redirect(302, '/')
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, platform }) => {
// TODO: fetching the instance for the thumbnail, but that should be part of the settings
const instance = await resolveValue(instanceLoader)
const database = await getDatabase(platform)
const brandingData = await getSettings(database)
const rules = await getRules(database)
const admins = await getAdmins(database)
let adminAccount: Account | null = null
const adminPerson = admins.find((admin) => admin[emailSymbol] === platform.ADMIN_EMAIL)
if (adminPerson) {
try {
adminAccount = (await loadLocalMastodonAccount(database, adminPerson)) as Account
} catch {
/* empty */
}
}
return {
image: instance.thumbnail,
domain: getDomain(request.url),
contact: {
account: george,
email: 'test@test.com',
},
rules: [
{
id: '1',
text: 'Sexually explicit or violent media must be marked as sensitive when posting',
},
{
id: '2',
text: 'No racism, sexism, homophobia, transphobia, xenophobia, or casteism',
},
{
id: '3',
text: 'No incitement of violence or promotion of violent ideologies',
},
{
id: '4',
text: 'No harassment, dogpiling or doxxing of other users',
},
{
id: '7',
text: 'Do not share intentionally false or misleading information',
},
],
admin: { account: JSON.parse(JSON.stringify(adminAccount)), email: platform.ADMIN_EMAIL },
rules: JSON.parse(JSON.stringify(rules.sort(({ id: idA }, { id: idB }) => idA - idB))),
extended_description: {
updated_at: '2023-01-19T14:55:44Z',
content:
'<p>Please mind that the <a href="mailto:staff@mastodon.social">staff@mastodon.social</a> e-mail is for inquiries related to the operation of the mastodon.social server specifically. If your account is on another server, <strong>we will not be able to assist you</strong>. For inquiries not related specifically to the operation of this server, such as press inquiries about Mastodon gGmbH, please contact <a href="mailto:press@joinmastodon.org">press@joinmastodon.org</a>. Additional addresses:</p>\n\n<ul>\n<li>Legal, GDPR, DMCA: <a href="mailto:legal@mastodon.social">legal@mastodon.social</a></li>\n<li>Appeals: <a href="mailto:moderation@mastodon.social">moderation@mastodon.social</a></li>\n</ul>\n\n<h2>Funding</h2>\n\n<p>This server is crowdfunded by <a href="https://patreon.com/mastodon">Patreon donations</a>. For a list of sponsors, see <a href="https://joinmastodon.org/sponsors">joinmastodon.org</a>.</p>\n\n<h2>Reporting and moderation</h2>\n\n<p>When reporting accounts, please make sure to include at least a few posts that show rule-breaking behaviour, when applicable. If there is any additional context that might help make a decision, please also include it in the comment. This is especially important when the content is in a language nobody on the moderation team speaks.</p>\n\n<p>We usually handle reports within 24 hours. Please mind that you are not notified when a report you have made has led to a punitive action, and that not all punitive actions are externally visible. For first time offenses, we may opt to delete offending content, escalating to harsher measures on repeat offenses.</p>\n\n<p>We have a team of paid moderators. If you would like to become a moderator, get in touch with us through the e-mail address above.</p>\n\n<h2>Impressum</h2>\n\n<p>Mastodon gGmbH<br>\nMühlenstraße 8a<br>\n14167 Berlin<br>\nGermany</p>\n\n<p>E-Mail-Adresse: hello@joinmastodon.org</p>\n\n<p>Vertretungsberechtigt: Eugen Rochko (Geschäftsführer)</p>\n\n<p>Umsatzsteuer Identifikationsnummer (USt-ID): DE344258260</p>\n\n<p>Handelsregister<br>\nGeführt bei: Amtsgericht Charlottenburg<br>\nNummer: HRB 230086 B</p>\n',
content: brandingData?.['extended description'] ?? '',
},
}
})
@ -78,21 +67,30 @@ export default component$(() => {
</h2>
<p data-testid="social-text" class="mb-6 text-wildebeest-500">
<span>
Decentralised social media powered by{' '}
<a href="https://joinmastodon.org" class="no-underline text-wildebeest-200 font-semibold" target="_blank">
Mastodon
Decentralized social network powered by{' '}
<a
href="https://github.com/cloudflare/wildebeest"
class="no-underline text-wildebeest-200 font-semibold"
target="_blank"
>
Wildebeest
</a>
</span>
</p>
<div class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5" data-testid="contact">
<div class="flex-1 p-4">
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
<AccountCard account={aboutInfo.contact.account} subText="username" />
</div>
<div class="flex-1 p-4 pt-6 md:pt-4 md:pl-6 border-wildebeest-500 border-solid border-t md:border-t-0 md:border-l">
<div
class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5 overflow-auto"
data-testid="contact"
>
{!!aboutInfo.admin.account && (
<div class="flex-1 p-4 border-wildebeest-500 border-solid border-b md:border-b-0 md:border-r">
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
<AccountCard account={aboutInfo.admin.account} subText="username" />
</div>
)}
<div class="flex-1 p-4 pt-6 md:pt-4 md:pl-6 min-w-max">
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Contact:</span>
<span>{aboutInfo.contact.email}</span>
<span>{aboutInfo.admin.email}</span>
</div>
</div>
@ -107,10 +105,10 @@ export default component$(() => {
<div class="my-1">
<Accordion title="Server rules">
<ol class="list-none flex flex-col gap-1 my-5 px-6">
{aboutInfo.rules.map(({ id, text }) => (
{aboutInfo.rules.map(({ id, text }, idx) => (
<li key={id} class="flex items-center border-wildebeest-700 border-b last-of-type:border-b-0 py-2">
<span class="bg-wildebeest-vibrant-400 text-wildebeest-900 mr-4 my-1 p-4 rounded-full w-5 h-5 grid place-content-center">
{id}
{idx + 1}
</span>
<span>{text}</span>
</li>
@ -131,7 +129,7 @@ export const head: DocumentHead = ({ resolveValue, head }) => {
return getDocumentHead(
{
title: `About - ${instance.title}`,
description: `About page for the ${instance.title} Mastodon instance`,
description: `About page for ${instance.title}`,
og: {
type: 'website',
image: instance.thumbnail,

Wyświetl plik

@ -7,21 +7,19 @@ import type { MastodonStatus } from '~/types'
import { getDocumentHead } from '~/utils/getDocumentHead'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
export const statusesLoader = loader$<Promise<MastodonStatus[]>, { DATABASE: D1Database; domain: string }>(
async ({ platform, html }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, getDatabase(platform))
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 (e: unknown) {
const error = e as { stack: string; cause: string }
console.warn(error.stack, error.cause)
throw html(500, getErrorHtml('The timeline is unavailable, please try again later'))
}
export const statusesLoader = loader$<Promise<MastodonStatus[]>>(async ({ platform, html }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, await getDatabase(platform))
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 (e: unknown) {
const error = e as { stack: string; cause: string }
console.error(error.stack, error.cause)
throw html(500, getErrorHtml('The timeline is unavailable, please try again later'))
}
)
})
export default component$(() => {
const statuses = statusesLoader().value

Wyświetl plik

@ -11,10 +11,7 @@ import { InstanceConfigContext } from '~/utils/instanceConfig'
import { getDocumentHead } from '~/utils/getDocumentHead'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
export const instanceLoader = loader$<
Promise<InstanceConfig>,
{ DATABASE: D1Database; INSTANCE_TITLE: string; INSTANCE_DESCR: string; ADMIN_EMAIL: string }
>(async ({ platform, html }) => {
export const instanceLoader = loader$<Promise<InstanceConfig>>(async ({ platform, html }) => {
const env = {
INSTANCE_DESCR: platform.INSTANCE_DESCR,
INSTANCE_TITLE: platform.INSTANCE_TITLE,

Wyświetl plik

@ -8,21 +8,19 @@ import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
import { getDatabase } from 'wildebeest/backend/src/database'
export const statusesLoader = loader$<Promise<MastodonStatus[]>, { DATABASE: D1Database; domain: string }>(
async ({ platform, html }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, getDatabase(platform))
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 (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'))
}
export const statusesLoader = loader$<Promise<MastodonStatus[]>>(async ({ platform, html }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, await getDatabase(platform))
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 (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'))
}
)
})
export default component$(() => {
const statuses = statusesLoader().value

Wyświetl plik

@ -8,21 +8,19 @@ 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 }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, getDatabase(platform), { local: true })
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 (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'))
}
export const statusesLoader = loader$<Promise<MastodonStatus[]>>(async ({ platform, html }) => {
try {
// TODO: use the "trending" API endpoint here.
const response = await timelines.handleRequest(platform.domain, await getDatabase(platform), { local: true })
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 (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'))
}
)
})
export default component$(() => {
const statuses = statusesLoader().value

Wyświetl plik

@ -0,0 +1,69 @@
import { $, component$ } from '@builder.io/qwik'
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
import { getDatabase } from 'wildebeest/backend/src/database'
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
import { handleRequest } from 'wildebeest/functions/api/v1/timelines/tag/[tag]'
import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel'
import StickyHeader from '~/components/StickyHeader/StickyHeader'
import { MastodonStatus } from '~/types'
import { getDocumentHead } from '~/utils/getDocumentHead'
export const loader = loader$<Promise<{ tag: string; statuses: MastodonStatus[] }>>(
async ({ request, platform, params }) => {
const tag = params.tag
const response = await handleRequest(await getDatabase(platform), request, getDomain(request.url), tag)
const results = await response.text()
const statuses: MastodonStatus[] = JSON.parse(results)
return { tag, statuses }
}
)
export default component$(() => {
const loaderData = loader()
return (
<>
<div class="flex flex-col flex-1">
<StickyHeader withBackButton backButtonPlacement="end">
<h2 class="text-reg text-md m-0 p-4 flex bg-wildebeest-700">
<i class="fa fa-hashtag fa-fw mr-3 w-5 leading-tight inline-block" />
<span>{loaderData.value.tag}</span>
</h2>
</StickyHeader>
<StatusesPanel
initialStatuses={loaderData.value.statuses}
fetchMoreStatuses={$(async (numOfCurrentStatuses: number) => {
let statuses: MastodonStatus[] = []
try {
const response = await fetch(
`/api/v1/timelines/tags/${loaderData.value.tag}/?offset=${numOfCurrentStatuses}`
)
if (response.ok) {
const results = await response.text()
statuses = JSON.parse(results)
}
} catch {
/* empty */
}
return statuses
})}
/>
</div>
</>
)
})
export const requestUrlLoader = loader$(async ({ request }) => request.url)
export const head: DocumentHead = ({ resolveValue }) => {
const { tag } = resolveValue(loader)
const url = resolveValue(requestUrlLoader)
return getDocumentHead({
title: `#${tag} - Wildebeest`,
description: `#${tag} tag page - Wildebeest`,
og: {
url,
},
})
}

Wyświetl plik

@ -0,0 +1,33 @@
import { component$, Slot } from '@builder.io/qwik'
import { loader$ } from '@builder.io/qwik-city'
type AuthLoaderData = {
loginUrl: URL
isAuthorized: boolean
}
export const authLoader = loader$<Promise<AuthLoaderData>>(async ({ platform }) => {
const isAuthorized = platform.data.connectedActor !== null
// defined in migrations/0010_add_ui_client.sql
const UI_CLIENT_ID = '924801be-d211-495d-8cac-e73503413af8'
const params = new URLSearchParams({
redirect_uri: '/',
response_type: 'code',
client_id: UI_CLIENT_ID,
scope: 'all',
})
const loginUrl = new URL('/oauth/authorize?' + params, 'https://' + platform.DOMAIN)
return {
isAuthorized,
loginUrl,
}
})
export default component$(() => {
return (
<>
<Slot />
</>
)
})

Wyświetl plik

@ -216,3 +216,8 @@ export type History = {
accounts: string
uses: string
}
export type WildebeestEnv = {
ACCESS_AUTH_DOMAIN: string
ACCESS_AUD: string
}

Wyświetl plik

@ -0,0 +1,12 @@
import { loader$ } from '@builder.io/qwik-city'
import { isAdminSymbol } from 'wildebeest/backend/src/activitypub/actors'
import { getErrorHtml } from './getErrorHtml/getErrorHtml'
export const adminLoader = loader$(async ({ platform, html }) => {
const isAuthorized = platform.data.connectedActor !== null
const isAdmin = isAuthorized && platform.data.connectedActor[isAdminSymbol]
if (!isAdmin) {
return html(401, getErrorHtml('You need to be an admin to view this page'))
}
})

Wyświetl plik

@ -0,0 +1,10 @@
import { loader$ } from '@builder.io/qwik-city'
import { getErrorHtml } from './getErrorHtml/getErrorHtml'
export const authLoader = loader$(async ({ platform, html }) => {
const isAuthenticated = platform.data.connectedActor !== null
if (!isAuthenticated) {
return html(401, getErrorHtml("You're not authorized to view this page"))
}
})

Wyświetl plik

@ -10,6 +10,8 @@ type DocumentHeadData = {
}
}
type NoReadonly<T> = { -readonly [P in keyof T]: NoReadonly<T[P]> }
/**
* Generates a head to provide to QwikCity
*
@ -18,7 +20,7 @@ type DocumentHeadData = {
* @returns the QwikCity head ready to use
*/
export function getDocumentHead(data: DocumentHeadData, head?: DocumentHeadValue) {
const result: DocumentHeadValue = { meta: [] }
const result: NoReadonly<DocumentHeadValue> = { meta: [] }
const setMeta = (name: string, content: string) => {
if (head?.meta?.some((meta) => meta.name === name)) {

Wyświetl plik

@ -297,10 +297,10 @@
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@builder.io/qwik-city@0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@builder.io/qwik-city/-/qwik-city-0.2.1.tgz#c57f481a75534ff54ddb0f38403acc66b5d02f41"
integrity sha512-g+ZC4Neo1XYQ/8uquUp6GKwr0eagpuCyQ3LkAtFhaIARaO67+cZfR6EFLJzf9wz5AVSt8/0QSD7wJEpni1i4IA==
"@builder.io/qwik-city@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@builder.io/qwik-city/-/qwik-city-0.4.0.tgz#9afb97ba0e11119e44a2527f545e0d84a8fe9759"
integrity sha512-XNpmHzSHam7ZYrd12kJdwFerMEck0iOk3Wgb9IlVIuaN/nLuN033qrNWLVq+ZzlhplUea9DGc4job8qMix7WWA==
dependencies:
"@mdx-js/mdx" "2.3.0"
"@types/mdx" "2.0.3"
@ -308,10 +308,10 @@
vfile "5.3.7"
zod "^3.20.6"
"@builder.io/qwik@0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@builder.io/qwik/-/qwik-0.18.1.tgz#341d01c5749a07230c700a5e4df859b857654cd0"
integrity sha512-11qx5Wh6WRxgvHDJDppJORhykzkACUYuu9qRKEGdS3vTkBQ2Rr8NFDjYon2x6+8Wu9WukHk84ANywWnS91gS/w==
"@builder.io/qwik@0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@builder.io/qwik/-/qwik-0.21.0.tgz#da967259a79f1aa45c23ea0dc395608c74a543b0"
integrity sha512-YDQcCSGPgXLQkY260N4XvYavxPNHewzIzIjqbl9hvnnk7a1s4EkcCrkudbGCl6VkkiQsDKMYPsemy4sFpKlafg==
"@cush/relative@^1.0.0":
version "1.0.0"
@ -1031,6 +1031,11 @@
"@typescript-eslint/types" "5.46.1"
eslint-visitor-keys "^3.3.0"
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
@ -1323,7 +1328,7 @@ chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.0:
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1356,7 +1361,7 @@ character-reference-invalid@^2.0.0:
resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz"
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -1451,21 +1456,6 @@ concat-map@0.0.1:
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
concurrently@^7.6.0:
version "7.6.0"
resolved "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz"
integrity sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==
dependencies:
chalk "^4.1.0"
date-fns "^2.29.1"
lodash "^4.17.21"
rxjs "^7.0.0"
shell-quote "^1.7.3"
spawn-command "^0.0.2-1"
supports-color "^8.1.0"
tree-kill "^1.2.2"
yargs "^17.3.1"
convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.9.0"
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
@ -1495,10 +1485,12 @@ data-uri-to-buffer@^4.0.0:
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz"
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
date-fns@^2.29.1:
version "2.29.3"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
@ -2149,6 +2141,11 @@ human-signals@^2.1.0:
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
@ -2809,11 +2806,6 @@ lodash.merge@^4.6.2:
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
longest-streak@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"
@ -3311,6 +3303,11 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
@ -3359,6 +3356,29 @@ node-releases@^2.0.6:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz"
integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
nodemon@^2.0.20:
version "2.0.20"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.20.tgz#e3537de768a492e8d74da5c5813cb0c7486fc701"
integrity sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==
dependencies:
chokidar "^3.5.2"
debug "^3.2.7"
ignore-by-default "^1.0.1"
minimatch "^3.1.2"
pstree.remy "^1.1.8"
semver "^5.7.1"
simple-update-notifier "^1.0.7"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.5"
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
dependencies:
abbrev "1"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
@ -3621,6 +3641,11 @@ property-information@^6.0.0:
resolved "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
@ -3760,13 +3785,6 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rxjs@^7.0.0:
version "7.8.0"
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz"
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
dependencies:
tslib "^2.1.0"
sade@^1.7.3:
version "1.8.1"
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
@ -3790,11 +3808,21 @@ semver@7.x, semver@^7.3.5, semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^6.0.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@~7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
@ -3807,16 +3835,18 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.7.3:
version "1.7.4"
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz"
integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==
signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-update-notifier@^1.0.7:
version "1.1.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82"
integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==
dependencies:
semver "~7.0.0"
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
@ -3855,11 +3885,6 @@ space-separated-tokens@^2.0.0:
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
spawn-command@^0.0.2-1:
version "0.0.2-1"
resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz"
integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
@ -3948,7 +3973,7 @@ sucrase@^3.20.3:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
supports-color@^5.3.0:
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@ -3962,7 +3987,7 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
supports-color@^8.0.0, supports-color@^8.1.0:
supports-color@^8.0.0:
version "8.1.1"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
@ -4048,10 +4073,12 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
dependencies:
nopt "~1.0.10"
trim-lines@^3.0.0:
version "3.0.1"
@ -4096,11 +4123,6 @@ tslib@^1.8.1, tslib@^1.9.3:
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.4.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
@ -4135,6 +4157,11 @@ typescript@4.9.4:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
undefsafe@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
undici@5.19.1:
version "5.19.1"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.19.1.tgz#92b1fd3ab2c089b5a6bd3e579dcda8f1934ebf6d"

Wyświetl plik

@ -7,7 +7,7 @@ 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, getDatabase(env))
return handleRequest(request, await getDatabase(env))
}
const headers = {

Wyświetl plik

@ -5,7 +5,7 @@ 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, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
const headers = {

Wyświetl plik

@ -7,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, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
const headers = {

Wyświetl plik

@ -11,7 +11,7 @@ const headers = {
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env }) => {
const domain = new URL(request.url).hostname
return handleRequest(domain, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {

Wyświetl plik

@ -8,7 +8,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, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
const headers = {

Wyświetl plik

@ -11,7 +11,7 @@ const headers = {
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env }) => {
const domain = new URL(request.url).hostname
return handleRequest(domain, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
export async function handleRequest(domain: string, db: Database, id: string): Promise<Response> {

Wyświetl plik

@ -8,7 +8,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, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
const headers = {

Wyświetl plik

@ -41,7 +41,7 @@ export const onRequest: PagesFunction<Env, any> = async ({ params, request, env
const domain = new URL(request.url).hostname
return handleRequest(
domain,
getDatabase(env),
await getDatabase(env),
params.id as string,
activity,
env.QUEUE,

Wyświetl plik

@ -7,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, getDatabase(env), params.id as string, env.userKEK)
return handleRequest(domain, await getDatabase(env), params.id as string, env.userKEK)
}
const headers = {

Wyświetl plik

@ -12,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, getDatabase(env), params.id as string)
return handleRequest(domain, await getDatabase(env), params.id as string)
}
const headers = {

Wyświetl plik

@ -13,7 +13,7 @@ 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, getDatabase(env))
return handleRequest(domain, params.id as string, await getDatabase(env))
}
export async function handleRequest(domain: string, id: string, db: Database): Promise<Response> {

Wyświetl plik

@ -13,7 +13,7 @@ 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, getDatabase(env), params.id as string, data.connectedActor, env.userKEK)
return handleRequest(request, await getDatabase(env), params.id as string, data.connectedActor, env.userKEK)
}
export async function handleRequest(

Wyświetl plik

@ -16,7 +16,7 @@ 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, getDatabase(env), params.id as string)
return handleRequest(request, await getDatabase(env), params.id as string)
}
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {

Wyświetl plik

@ -16,7 +16,7 @@ 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, getDatabase(env), params.id as string)
return handleRequest(request, await getDatabase(env), params.id as string)
}
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {

Wyświetl plik

@ -26,7 +26,7 @@ const headers = {
}
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env, params }) => {
return handleRequest(request, getDatabase(env), params.id as string)
return handleRequest(request, await getDatabase(env), params.id as string)
}
export async function handleRequest(request: Request, db: Database, id: string): Promise<Response> {
@ -140,10 +140,10 @@ FROM outbox_objects
INNER JOIN objects ON objects.id=outbox_objects.object_id
INNER JOIN actors ON actors.id=outbox_objects.actor_id
WHERE objects.type='Note'
${withReplies ? '' : "AND json_extract(objects.properties, '$.inReplyTo') IS NULL"}
${withReplies ? '' : 'AND ' + db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')}
AND outbox_objects.target = '${PUBLIC_GROUP}'
AND outbox_objects.actor_id = ?1
AND outbox_objects.cdate > ?2
AND outbox_objects.cdate > ?2${db.qb.psqlOnly('::timestamp')}
ORDER by outbox_objects.published_date DESC
LIMIT ?3 OFFSET ?4
`
@ -161,7 +161,7 @@ LIMIT ?3 OFFSET ?4
return new Response(JSON.stringify(out), { headers })
}
let afterCdate = '00-00-00 00:00:00'
let afterCdate = db.qb.epoch()
if (url.searchParams.has('max_id')) {
// Client asked to retrieve statuses after the max_id
// As opposed to Mastodon we don't use incremental ID but UUID, we need

Wyświetl plik

@ -12,7 +12,7 @@ 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, getDatabase(env), params.id as string, data.connectedActor, env.userKEK)
return handleRequest(request, await getDatabase(env), params.id as string, data.connectedActor, env.userKEK)
}
export async function handleRequest(

Wyświetl plik

@ -8,7 +8,7 @@ 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, getDatabase(env), data.connectedActor)
return handleRequest(request, await getDatabase(env), data.connectedActor)
}
export async function handleRequest(req: Request, db: Database, connectedActor: Person): Promise<Response> {

Wyświetl plik

@ -22,7 +22,7 @@ const headers = {
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, data, env }) => {
return handleRequest(
getDatabase(env),
await getDatabase(env),
request,
data.connectedActor,
env.CF_ACCOUNT_ID,
@ -59,12 +59,12 @@ export async function handleRequest(
if (formData.has('display_name')) {
const value = formData.get('display_name')!
await updateActorProperty(db, connectedActor.id, 'name', value)
await updateActorProperty(db, connectedActor.id, 'name', value as string)
}
if (formData.has('note')) {
const value = formData.get('note')!
await updateActorProperty(db, connectedActor.id, 'summary', value)
await updateActorProperty(db, connectedActor.id, 'summary', value as string)
}
if (formData.has('avatar')) {

Some files were not shown because too many files have changed in this diff Show More