kopia lustrzana https://github.com/cloudflare/wildebeest
Merge branch 'cloudflare:main' into main
commit
a851c74fb5
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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');
|
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
|
@ -25,4 +25,6 @@ export interface Env {
|
|||
SENTRY_DSN: string
|
||||
SENTRY_ACCESS_CLIENT_ID: string
|
||||
SENTRY_ACCESS_CLIENT_SECRET: string
|
||||
|
||||
NEON_DATABASE_URL?: string
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.')
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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=?
|
||||
`
|
||||
|
|
|
@ -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> }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
compatibility_date = "2023-01-09"
|
||||
main = "./src/index.ts"
|
||||
usage_model = "unbound"
|
||||
node_compat = true
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { component$, Slot } from '@builder.io/qwik'
|
||||
|
||||
export { adminLoader } from '~/utils/adminLoader'
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<Slot />
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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$(() => <></>)
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
import { component$, Slot } from '@builder.io/qwik'
|
||||
|
||||
export { authLoader } from '~/utils/authLoader'
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<Slot />
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -216,3 +216,8 @@ export type History = {
|
|||
accounts: string
|
||||
uses: string
|
||||
}
|
||||
|
||||
export type WildebeestEnv = {
|
||||
ACCESS_AUTH_DOMAIN: string
|
||||
ACCESS_AUD: string
|
||||
}
|
||||
|
|
|
@ -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'))
|
||||
}
|
||||
})
|
|
@ -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"))
|
||||
}
|
||||
})
|
|
@ -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)) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
Ładowanie…
Reference in New Issue