A bonus/federation.md => bonus/federation.md +80 -0
@@ 0,0 1,80 @@
+# federating with basingstoke
+
+basingstoke is mainly concerned with federating with itself. following
+basingstoke users from other platforms (like Mastodon) will work, but some
+things might look a bit weird.
+
+## json ld
+
+basingstoke doesn't bother with anything JSON-LD, although an effort has been
+made for compatibility with servers that do (assuming they exist).
+
+```
+{
+ "@context": ["https://www.w3.org/ns/activitystreams", {
+ "stoke": "https://ns.pronounmail.com"
+ }]
+}
+```
+
+## actors
+
+
+## posts
+
+basingstoke accepts the following object types:
+
+* `Article`
+* `Note`
+* `Page`
+* `Question`
+
+### reposts
+
+reposts are identified by a `RE: <post link>` line at the bottom of a post's
+content. basingstoke also includes a `Link` containing a link to the previous
+post in the repost chain; This is the preferred method of parsing reposts.
+
+```json
+{
+ "@context": ["https://www.w3.org/ns/activitystreams", {
+ "stoke": "https://ns.pronounmail.com"
+ }],
+ "id": "https://basingstoke.example/post/abcd",
+ "type": "Note",
+ "content": "cool post!\nRE: https://basingstoke.example/post/asdf",
+ "tag": [
+ {
+ "type": "Link",
+ "href": "https://basingstoke.example/post/asdf",
+ "rel": "prev"
+ }
+ ]
+}
+```
+
+### tags
+
+### content warnings
+
+#### common CWs
+
+to make filtering/auto-unfurling common content warnings easier, basingstoke
+defines some common content warning names:
+
+* `18+`: Content is suitable for adults only
+
+## securitie
+
+all incoming requests are required to have a valid HTTP Signature.
+both RSA-SHA256 and Ed25519 are supported (although the latter is greatly
+preferred!).
+
+basingstoke signs all outgoing requests (apart from WebFinger requests) with a
+HTTP Signature. unlike other implementations, basingstoke assigns a unique key
+to each user page when it's created.
+
+## migration
+
+an instance migration system is in progress. it'll be a bit different than
+Mastodon's.<
\ No newline at end of file
A bonus/innit/basingstoke.dinit => bonus/innit/basingstoke.dinit +6 -0
@@ 0,0 1,6 @@
+type = process
+command = /usr/bin/node /opt/basingstoke/index.js --config /etc/basingstoke/config.toml
+logfile = /var/log/stoke.log
+smooth-recovery = true
+restart = false
+depends-ms = networking<
\ No newline at end of file
R src/lib/Forms/FormItem.svelte => bonus/innit/basingstoke.service +0 -0
A bonus/setup.md => bonus/setup.md +46 -0
@@ 0,0 1,46 @@
+# setting up a basingstoke
+
+## prerequisites
+
+## downloading
+
+### packaged by your distro
+
+todo
+
+### from the source
+
+assuming you have node.js, git and sqlite3 installed, you should be able to clone
+the repository, `npm install` and then `npm run build`:
+
+```bash
+git clone https://git.sr.ht/~leah/basingstoke && cd basingstoke/
+npm install
+npm run build
+```
+
+if everything goes well, you should end up with a new build/ directory in the
+basingstoke directory. you can copy this wherever and run `node index.js` inside
+it to run the server.
+
+```bash
+sudo mv build /opt/basingstoke
+
+```
+
+## configuration
+
+
+
+## make it a service
+
+it's generally a good idea to create a service using your system's init system
+(usually systemd or openrc) to keep the server running across reboots and crashes.
+
+sample files are available in the `bonus/` directory:
+
+| innit system | where |
+| --------------------------------------- | --------------------------------- |
+| **systemd** | `bonus/innit/basingstoke.service` |
+| **openrc** | todo |
+| **dinit** (trying to future proof here) | `bonus/innit/basingstoke.dinit` |<
\ No newline at end of file
D cache_schema.sql => cache_schema.sql +0 -11
@@ 1,11 0,0 @@
-CREATE TABLE IF NOT EXISTS cached (
- key TEXT PRIMARY KEY UNIQUE NOT NULL,
- value BLOB,
- created INTEGER NOT NULL DEFAULT (unixepoch(CURRENT_TIMESTAMP))
- ttl INTEGER NOT NULL DEFAULT 1800
-);
-
-CREATE TRIGGER IF NOT EXISTS tidy_up AFTER UPDATE ON cached
-BEGIN
- DELETE FROM cached WHERE created < (unixepoch(CURRENT_TIMESTAMP) - ttl)
-END;
M config.stoke.example.toml => config.stoke.example.toml +3 -2
@@ 1,4 1,5 @@
[stoke]
hostname = "bazinga.gov"
-db_path = "stoke.db"
-blob_db_path = "stoke-blob.db">
\ No newline at end of file
+db_path = "data/stoke.db"
+blob_db_path = "data/stoke-blob.db"
+cache_db_path = "data/stoke-cache.db"<
\ No newline at end of file
R blob_schema.sql => data/blob-schema.sql +0 -0
A data/cache-schema.sql => data/cache-schema.sql +19 -0
@@ 0,0 1,19 @@
+CREATE TABLE IF NOT EXISTS cached (
+ key TEXT PRIMARY KEY UNIQUE NOT NULL,
+ value BLOB,
+ created INTEGER NOT NULL DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
+ ttl INTEGER NOT NULL DEFAULT 1800
+);
+
+CREATE TABLE IF NOT EXISTS webfinger (
+ hostname TEXT NOT NULL,
+ resource TEXT NOT NULL,
+ response TEXT NOT NULL,
+
+ CONSTRAINT name_resource_unique UNIQUE (hostname, resource)
+);
+
+-- CREATE TRIGGER IF NOT EXISTS tidy_up AFTER UPDATE ON cached
+-- BEGIN
+-- DELETE FROM cached WHERE created < (unixepoch(CURRENT_TIMESTAMP) - ttl)
+-- END;
A data/innit.sh => data/innit.sh +5 -0
@@ 0,0 1,5 @@
+#!/bin/bash
+
+sqlite3 stoke.db '.read schema.sql'
+sqlite3 stoke-blob.db '.read blob-schema.sql'
+sqlite3 stoke-cache.db '.read cache-schema.sql'<
\ No newline at end of file
A data/reset.sh => data/reset.sh +4 -0
@@ 0,0 1,4 @@
+#!/bin/bash
+
+rm stoke*.db*
+./innit.sh<
\ No newline at end of file
R schema.sql => data/schema.sql +12 -7
@@ 32,10 32,12 @@ CREATE TABLE IF NOT EXISTS user_page (
-- separate table from `user_page` so we can SELECT * with reckless abandon
CREATE TABLE IF NOT EXISTS key_pair (
- page_id INTEGER NOT NULL PRIMARY KEY,
+ page_id INTEGER NOT NULL,
+ kind TEXT NOT NULL,
private TEXT NOT NULL,
public TEXT NOT NULL,
+ CONSTRAINT key_unique UNIQUE (page_id, kind),
FOREIGN KEY(page_id) REFERENCES user_page(id) ON DELETE CASCADE
);
@@ 121,12 123,15 @@ CREATE TABLE IF NOT EXISTS session (
-- );
CREATE TABLE IF NOT EXISTS follow (
- follower_id INTEGER NOT NULL,
- followee_id INTEGER NOT NULL,
+ follower_id TEXT NOT NULL,
+ followee_id TEXT NOT NULL,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
- CONSTRAINT follow_unique UNIQUE (follower_id, followee_id),
+ CONSTRAINT follow_unique UNIQUE (follower_id, followee_id)
+);
- FOREIGN KEY(follower_id) REFERENCES user_page(id) ON DELETE CASCADE,
- FOREIGN KEY(followee_id) REFERENCES user_page(id) ON DELETE CASCADE
-);>
\ No newline at end of file
+CREATE TRIGGER IF NOT EXISTS follow_delete_cascade BEFORE DELETE ON user_page
+ BEGIN
+ DELETE FROM follow
+ WHERE followee_id = OLD.id OR follower_id = OLD.id;
+ END;<
\ No newline at end of file
M src/lib/ActivityPub/actor.ts => src/lib/ActivityPub/actor.ts +55 -16
@@ 1,8 1,10 @@
import config from '$lib/config';
import { from_markdown_sync } from '$lib/markdown';
-import { fetch_keypair, fetch_page, type KeyPairData, type UserPageData } from '$lib/server/Model/userpage';
-import { ap_context } from '.';
-import { ActivityPubError, ActorType } from './model';
+import UserPage, { fetch_page, type KeyPair, type UserPageData } from '$lib/server/Model/userpage';
+import { ap_accept, ap_context } from '.';
+import { ActivityPubError, ActorType, type Attachment } from './model';
+import { signed_fetch } from './signing';
+import WebFinger from "./webfinger";
export const id_from_username = (username: string) =>
`https://${config.hostname}/user/${username}`;
@@ 15,12 17,6 @@ const linkify = (link: string) => `<a
${link}
</a>`
-type Image = {
- type: string,
- mediaType: string,
- url: string,
-}
-
export class Actor {
type: ActorType;
id: string;
@@ 36,12 32,12 @@ export class Actor {
name?: string;
summary?: string;
- icon?: string;
- image?: Image;
+ icon?: Attachment;
+ image?: Attachment;
url: string;
attachment: Record<string, any>[] = [];
- constructor(stuff: UserPageData, key_pair: KeyPairData) {
+ constructor(stuff: UserPageData, key_pair: KeyPair) {
const {
username,
display_name,
@@ 69,7 65,11 @@ export class Actor {
if (display_name) this.name = display_name;
if (bio) this.summary = from_markdown_sync(bio);
- if (avatar_uri) this.icon = avatar_uri;
+ if (avatar_uri) this.icon = {
+ type: "Image",
+ mediaType: "image/jpeg", // todo: take a wild guess
+ url: avatar_uri,
+ };
if (pronouns) {
this.attachment.push({
type: 'PropertyValue',
@@ 94,16 94,55 @@ export class Actor {
}
- static async from_db(username: string) {
+ static async fetch(username: string, host: string) {
+
+ }
+
+ static async fetch_local(username: string) {
const user = fetch_page(username);
if (!user) throw Error(ActivityPubError.NoSuchUser);
- const keys = fetch_keypair(user.id);
+ const keys = UserPage.fetch_keypair(user.id);
if (!keys) throw Error(ActivityPubError.MissingKeys);
return new Actor(user, keys);
}
- to_object(): string {
+ // todo: error handling, everything else
+ static async fetch_remote(
+ username: string,
+ host: string,
+ opt?: { as?: string }
+ ) {
+ const key: [string, string] = (() => {
+ if (opt?.as) {
+ const { private: priv } = UserPage.fetch_keypair({ username: opt?.as }) ?? {};
+ if (!priv) throw ActivityPubError.MissingKeys;
+ return [`https://${config.hostname}/user/${opt?.as}`, priv]
+ } else {
+ throw "todo";
+ return [`https://${config.hostname}/root`, ""]
+ }
+ })()
+
+ const wf_res = await WebFinger.fetch(`acct:${username}@${host}`, host);
+ if ("err" in wf_res) return;
+
+ const actor_url = WebFinger.extract_actor(wf_res);
+ if (!actor_url) return;
+
+ const actor_res = await signed_fetch(key, actor_url, {
+ headers: { "Accept": ap_accept }
+ });
+
+ if (!actor_res.ok) return {
+ status: actor_res.status,
+ err: await actor_res.text()
+ };
+
+ return await actor_res.json();
+ }
+
+ to_ap(): string {
const json = {
...ap_context,
...this,
A src/lib/ActivityPub/caching.ts => src/lib/ActivityPub/caching.ts +0 -0
M src/lib/ActivityPub/index.ts => src/lib/ActivityPub/index.ts +4 -6
@@ 1,17 1,15 @@
-import { redirect } from '@sveltejs/kit';
-
export function check_for_accept_header(request: Request, username: string) {
// get accept headers
const accept = request.headers
.get('accept')
?.split(',')
.map((o) => o.trim());
- // redirect if client doesn't understand activitystreams
- if (accept && !accept.includes('application/activity+json')) {
- throw redirect(303, `/its/${username}`);
- }
+
+ return accept && accept.includes('application/activity+json');
}
+export const ap_accept = "application/activity+json";
+
export const ap_context = {
'@context': [
'https://www.w3.org/ns/activitystreams',
M src/lib/ActivityPub/model.ts => src/lib/ActivityPub/model.ts +7 -2
@@ 1,5 1,3 @@
-import { Create } from "./create";
-
export enum ActivityPubError {
NoSuchUser = 'No such user',
MissingKeys = 'Missing keys',
@@ 69,3 67,10 @@ export enum ObjectType {
Tombstone = 'Tombstone',
Video = 'Video',
}
+
+export type Attachment = {
+ type: string,
+ content?: string,
+ mediaType?: string,
+ url: string,
+}<
\ No newline at end of file
A src/lib/ActivityPub/signing.ts => src/lib/ActivityPub/signing.ts +43 -0
@@ 0,0 1,43 @@
+import Crypto from "node:crypto";
+
+/**
+ *
+ * @param key A tuple containing the key owner and private key in PEM format
+ * @param params The usual {@link fetch} parameters
+ */
+export async function signed_fetch(
+ [owner, private_key]: [owner: string, private_key: string],
+ ...params: Parameters<typeof fetch>
+): ReturnType<typeof fetch> {
+ const url = params[0] instanceof Request
+ ? new URL(params[0].url)
+ : new URL(params[0]);
+
+ const req = params[0] instanceof Request
+ ? params[0]
+ : params[1]
+
+ const method = req?.method ?? "GET"
+ const date = new Date(Date.now()).toUTCString();
+
+ const sig_string = `(request-target): ${method.toLowerCase()} ${url.pathname}\n`
+ + `host: ${url.hostname}\n`
+ + `date: ${date}`;
+
+ const sig = Crypto.sign("sha256", Buffer.from(sig_string), private_key)
+ .toString("base64");
+
+ const sig_headers = "(request-target) host date"
+ const key_id = owner
+ const sig_header = `keyId="${key_id}",headers="${sig_headers}",signature="${sig}"`
+
+ return fetch(url, {
+ ...req,
+ headers: {
+ ...req?.headers,
+ 'Date': date,
+ 'Host': url.hostname,
+ 'Signature': sig_header,
+ }
+ });
+}<
\ No newline at end of file
A src/lib/ActivityPub/webfinger/fetch.ts => src/lib/ActivityPub/webfinger/fetch.ts +57 -0
@@ 0,0 1,57 @@
+import { cache_db } from "$lib/server/db";
+
+interface WfResponse {
+ subject: string,
+ aliases: string[],
+ links: WfLink[]
+}
+
+type WfLink = {
+ rel: string,
+ type: string,
+ href: string,
+}
+
+type FetchError = { err: number };
+
+export async function wf_fetch(resource: string, host: string)
+ : Promise<WfResponse | FetchError> {
+ const cached = fetch_cached(resource, host);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ const res = await fetch(`https://${host}/.well-known/webfinger?resource=${resource}`);
+ if (!res.ok) {
+ return { err: res.status }
+ }
+ const json = await res.json();
+
+ cache_response(resource, host, JSON.stringify(json));
+ return json;
+}
+
+const cached_stmt = cache_db.prepare(`
+ SELECT response FROM webfinger
+ WHERE resource = ? AND hostname = ?
+`).pluck();
+
+function fetch_cached(resource: string, host: string) {
+ return cached_stmt.get(resource, host) as string | undefined
+}
+
+const insert_stmt = cache_db.prepare(`
+ INSERT INTO webfinger (resource, hostname, response)
+ VALUES (?, ?, ?)
+`);
+
+function cache_response(resource: string, host: string, response: string) {
+ insert_stmt.run(resource, host, response);
+}
+
+export function wf_extract_actor(res: WfResponse): string | undefined {
+ return res.links.find(x => (
+ x.rel == "self"
+ && x.type == "application/activity+json"
+ ))?.href;
+}<
\ No newline at end of file
A src/lib/ActivityPub/webfinger/index.ts => src/lib/ActivityPub/webfinger/index.ts +6 -0
@@ 0,0 1,6 @@
+import { wf_extract_actor, wf_fetch } from "./fetch";
+
+export default {
+ fetch: wf_fetch,
+ extract_actor: wf_extract_actor
+}<
\ No newline at end of file
M => +3 -2
@@ 25,6 25,7 @@
open = true;
}
}
</script>
<div
@@ 36,8 37,8 @@
<slot />
</div>
<Menu title="Context Menu" {anchor} bind:trigger bind:xy bind:open>
<slot name="menu" />
<Menu title="Context Menu" {anchor} bind:trigger bind:xy bind:open let:close>
<slot name="menu" {close} />
</Menu>
<style>
M => +2 -2
@@ 150,11 150,11 @@
<div bind:this={bg} class="bg" aria-hidden></div>
{#if mode == "freestyle"}
<div bind:this={content}>
<slot />
<slot close={close_menu} />
</div>
{:else}
<ul role="menu" bind:this={content} class="content">
<slot />
<slot close={close_menu} />
</ul>
{/if}
</dialog>
M => +1 -1
@@ 58,7 58,7 @@
scrolled = true;
}
onDestroy(() => observer.disconnect());
onDestroy(() => observer?.disconnect());
</script>
<div bind:this={bg} class="bg" class:scrolled></div>
M => +5 -0
@@ 10,6 10,11 @@
trigger: HTMLButtonElement;
const open_menu = (e: MouseEvent) => {
if (open) {
open = false;
return;
}
xy = [e.clientX, e.clientY];
trigger = e.target as HTMLButtonElement;
open = true;
A src/lib/Bonus/WarningBox.svelte => src/lib/Bonus/WarningBox.svelte +44 -0
@@ 0,0 1,44 @@
+<script lang="ts">
+ import { TriangleExclamation } from "$lib/Icons";
+ import Icon from "$lib/Icons/Icon.svelte";
+
+ export let title;
+</script>
+
+<section>
+ <h2>
+ <Icon source={TriangleExclamation} />
+ {title}
+ </h2>
+ <p>
+ <slot />
+ </p>
+</section>
+
+<style>
+ section {
+ position: relative;
+ border: 1px solid var(--foreground);
+ padding: 1rem;
+ margin: 2rem 0;
+ }
+ h2 {
+ position: absolute;
+ top: -0.5rem;
+ left: 0.5rem;
+ margin: 0;
+ padding: 0 0.5rem;
+ display: flex;
+ gap: 0.5rem;
+
+ font-family: var(--font-sans);
+ font-size: 0.9rem;
+ font-weight: 500;
+ text-transform: uppercase;
+
+ background-color: var(--background);
+ }
+ p {
+ margin: 0;
+ }
+</style>
M src/lib/Colours/colours.json => src/lib/Colours/colours.json +3 -1
@@ 12,5 12,7 @@
["foliage-y", "#F2FAF0", "#2F8C74"],
["quiet", "#E7F6DA", "#7C5DB4"],
["parma violet", "#E7E4FB", "#490DF4"],
- ["charcoal", "#021F2C", "#808FA6"]
+ ["charcoal", "#021F2C", "#808FA6"],
+ ["transgender (couldt'n fit the white in sorry", "#0000FF", "#FF00FF"],
+ [") hot dog stand", "#FFFF00", "#FF0000"]
]
M src/lib/Colours/index.ts => src/lib/Colours/index.ts +6 -0
@@ 30,3 30,9 @@ export function set_colours(set: ColourSet) {
localStorage.setItem(local_storage_key, JSON.stringify(set));
}
+export const is_selected = (current: ColourSet, colour_set: ColourSet) =>
+ current.includes(colour_set[1]) &&
+ current.includes(colour_set[2]);
+
+export const is_selected_strict = (current: ColourSet, colour_set: ColourSet) =>
+ current[1] == colour_set[1] && current[2] == colour_set[2];
A src/lib/Forms/FoldingThing.svelte => src/lib/Forms/FoldingThing.svelte +67 -0
@@ 0,0 1,67 @@
+<script lang="ts">
+ import { CaretDown, CaretUp } from "$lib/Icons";
+ import Icon from "$lib/Icons/Icon.svelte";
+
+ export let title: string,
+ subtitle: string | null = null;
+</script>
+
+<details {...$$restProps}>
+ <summary>
+ <div>
+ <h2>{title}</h2>
+ {#if subtitle}
+ <small>{subtitle}</small>
+ {/if}
+ </div>
+ <Icon class="down" source={CaretDown} />
+ <Icon class="up" source={CaretUp} />
+ </summary>
+ <slot />
+</details>
+
+<style>
+ details {
+ padding: .75rem 0.5rem;
+
+ & > summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ list-style: none;
+
+ user-select: none;
+ }
+
+ &:has(> summary:hover) {
+ background-color: var(--background-tone);
+ }
+
+ & > summary > .up {
+ display: none;
+ }
+
+ &[open] > summary {
+ margin-bottom: 1rem;
+
+ & > .up {
+ display: block;
+ }
+ & > .down {
+ display: none;
+ }
+ }
+
+ & + details {
+ border-top: 1px solid var(--foreground-tone);
+ }
+ }
+
+ h2 {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 1rem;
+ margin: 0;
+ }
+</style>
M src/lib/Forms/FormInput.svelte => src/lib/Forms/FormInput.svelte +1 -1
@@ 3,7 3,7 @@
flavour_text: string | undefined = undefined;
</script>
-<label>
+<label {...$$restProps}>
<span>{label}</span>
<slot />
{#if flavour_text}
M src/lib/Media/index.ts => src/lib/Media/index.ts +31 -27
@@ 2,38 2,42 @@ import { blob_db } from "$lib/server/db"
import { randomUUID } from "crypto";
import config from "$lib/config";
-export default class Media {
- static store_stmt = blob_db.prepare(`
- INSERT INTO blob (id, content_type, bytes)
- VALUES (@id, @content_type, @bytes);
- `);
+const store_stmt = blob_db.prepare(`
+ INSERT INTO blob (id, content_type, bytes)
+ VALUES (@id, @content_type, @bytes);
+`);
+
+function store(content_type: string, bytes: Uint8Array | Buffer) {
+ if (bytes instanceof Buffer) {
+ bytes = Uint8Array.from(bytes)
+ }
- static store(content_type: string, bytes: Uint8Array | Buffer) {
- if (bytes instanceof Buffer) {
- bytes = Uint8Array.from(bytes)
- }
+ const id = randomUUID()
+ store_stmt.run({ id, content_type, bytes });
- const id = randomUUID()
- this.store_stmt.run({ id, content_type, bytes });
+ return id;
+}
- return id;
- }
+const retrieve_stmt = blob_db.prepare(`
+ SELECT content_type, bytes FROM blob WHERE id = @id;
+`);
- static retrieve_stmt = blob_db.prepare(`
- SELECT content_type, bytes FROM blob WHERE id = @id;
- `);
+function retrieve(id: string): [string, Buffer] | null | undefined {
+ console.time("retrieve")
+ const file = retrieve_stmt.get({ id }) as {
+ content_type: string,
+ bytes: Buffer,
+ } | null;
+ if (!file) return;
+ console.timeEnd("retrieve")
- static retrieve(id: string): [string, Buffer] | null | undefined {
- console.time("retrieve")
- const file = this.retrieve_stmt.get({ id }) as {
- content_type: string,
- bytes: Buffer,
- } | null;
- if (!file) return;
- console.timeEnd("retrieve")
+ return [file.content_type, file.bytes];
+}
- return [file.content_type, file.bytes];
- }
+const resolve = (id: string) => `https://${config.hostname}/media/${id}`
- static get_uri_of = (id: string) => `https://${config.hostname}/media/${id}`
+export default {
+ resolve,
+ store,
+ retrieve,
}=
\ No newline at end of file
M => +63 -13
@@ 2,12 2,16 @@
import { goto } from "$app/navigation";
import ContextMenu from "$lib/Bonus/ContextMenu.svelte";
import Item from "$lib/Bonus/MenuItem.svelte";
import { colours } from "$lib/Colours";
import {
colour_store,
colours,
is_selected,
update_colours,
} from "$lib/Colours";
import {
Accessibility,
Bell,
CircleUser,
Cog,
Cube,
Hand,
HeadSide,
@@ 15,23 19,52 @@
PencilSquare,
Rss,
Swatches,
User,
Users,
} from "$lib/Icons";
import * as Stores from "svelte/store";
export let username: string, email: string;
</script>
<ContextMenu anchor="right">
<slot />
<svelte:fragment slot="menu">
<ul class="colours">
<svelte:fragment slot="menu" let:close>
<ul class="colours" aria-label="colour scheme picker">
{#each colours.slice(0, 6) as colour}
<li class="colour">
<div style="--colour: {colour[1]}" />
<div style="--colour: {colour[2]}" />
<li>
<button
on:click={() => {
update_colours(colour);
close();
}}
type="button"
class:selected={is_selected(
// just take a snapshot of the store because
// otherwise selecting the theme makes the menu infinitely
// shift to the left
Stores.get(colour_store),
colour,
)}
title={colour[0]}
class="colour"
>
<div style="--colour: {colour[1]}" />
<div style="--colour: {colour[2]}" />
</button>
</li>
{/each}
<li class="colour more">-></li>
<li>
<button
on:click={() => {
goto("/prefs/appearance");
close();
}}
class="colour more"
title="appearance preferences"
>
->
</button>
</li>
</ul>
<h3>@{username}</h3>
<Item on:click={() => goto("/prefs/profile")} icon={CircleUser}>
@@ 43,6 76,9 @@
<Item on:click={() => goto("/prefs/syndication")} icon={Rss}>
Syndication
</Item>
<Item on:click={() => goto("/prefs/syndication")} icon={Users}>
Cliques
</Item>
<h3>{email}</h3>
<Item on:click={() => goto("/prefs/account")} icon={List}>Details</Item>
<Item on:click={() => goto("/prefs/appearance")} icon={Swatches}>
@@ 54,11 90,10 @@
>
Accessibility
</Item>
<Item on:click={() => goto("/prefs/wellbeing")} icon={HeadSide}>
Wellbeing
</Item>
<Item on:click={() => goto("/prefs/pings")} icon={Bell}>Pings</Item>
<Item on:click={() => goto("/prefs/apps")} icon={Cube}>Applications</Item>
<Item on:click={() => goto("/prefs/apps")} icon={Cube}
>Applications</Item
>
<Item on:click={() => goto("/prefs/safety")} icon={Hand}>Safety</Item>
</svelte:fragment>
</ContextMenu>
@@ 77,10 112,19 @@
min-width: 1.5rem;
height: 1.5rem;
aspect-ratio: 1 / 1;
}
li > button {
min-width: 1.5rem;
height: 1.5rem;
aspect-ratio: 1 / 1;
margin: 0;
padding: 0;
display: grid;
border-radius: 2px;
overflow: hidden;
grid-template-columns: 1fr 1fr;
cursor: unset;
}
div {
@@ 96,5 140,11 @@
var(--foreground) 15%,
transparent
);
color: var(--foreground);
}
.selected {
outline: 2px solid
color-mix(in srgb, var(--foreground), var(--background) 50%);
}
</style>
R src/lib/Pings/HeadsUp.svelte => src/lib/Pings/Toast.svelte +3 -3
@@ 1,11 1,11 @@
<script lang="ts">
import { stagger, timeline, type Easing } from "motion";
import { onMount } from "svelte";
- import { dismiss_heads_up, type HeadsUp } from ".";
+ import { dismiss_toast, type Toast } from ".";
import Icon from "$lib/Icons/Icon.svelte";
import { Close } from "$lib/Icons";
- export let data: HeadsUp;
+ export let data: Toast;
let bg: HTMLDivElement, content: HTMLDivElement, buttons: HTMLUListElement;
@@ 76,7 76,7 @@
async function close() {
await out().finished;
- dismiss_heads_up(data.id);
+ dismiss_toast(data.id);
}
</script>
R src/lib/Pings/HeadsUpses.svelte => src/lib/Pings/ToastList.svelte +39 -32
@@ 1,48 1,56 @@
<script lang="ts">
import { afterUpdate } from "svelte";
- import { heads_ups } from ".";
- import HeadsUp from "./HeadsUp.svelte";
+ import { visible_toasts } from ".";
+ import Toast from "./Toast.svelte";
import { animate } from "motion";
let bg: HTMLDivElement,
- anim_done: boolean = false;
+ bg_visible: boolean = false;
afterUpdate(() => {
- if ($heads_ups.length != 0 && !anim_done) {
- anim_done = true;
- animate(
- bg,
- {
- opacity: [0, 1],
- filter: ["blur(0px)", "blur(50px)"],
- },
- { duration: 0.5 },
- );
- }
+ if ($visible_toasts.length != 0 && !bg_visible) first_toast_added();
- if ($heads_ups.length == 0) {
- console.log("here");
- anim_done = false;
- animate(
- bg,
- {
- opacity: [1, 0],
- },
- {
- duration: 0.25,
- easing: "ease-in-out",
- },
- );
- }
+ if ($visible_toasts.length == 0) all_toasts_dismissed();
});
+
+ function first_toast_added() {
+ bg_visible = true;
+ animate(
+ bg,
+ {
+ opacity: [0, 1],
+ filter: ["blur(0px)", "blur(50px)"],
+ },
+ { duration: 0.5 },
+ )
+ .finished.then(() => bg.style.height = bg.clientHeight + "px");
+ }
+
+ function toast_added() {
+
+ }
+
+ function all_toasts_dismissed() {
+ bg_visible = false;
+ animate(
+ bg,
+ {
+ opacity: [1, 0],
+ },
+ {
+ duration: 0.25,
+ easing: "ease-in-out",
+ },
+ );
+ }
</script>
<div class="container">
<div class="bg" bind:this={bg} aria-hidden />
- {#if $heads_ups.length != 0}
+ {#if $visible_toasts.length != 0}
<div class="scrolly">
- {#each $heads_ups as thing (thing.id)}
- <HeadsUp data={thing} />
+ {#each $visible_toasts as thing (thing.id)}
+ <Toast data={thing} />
{/each}
</div>
{/if}
@@ 79,7 87,6 @@
border-radius: 200px;
padding: 100%;
width: 100%;
- height: 100%;
z-index: -1;
}
</style>
M src/lib/Pings/index.ts => src/lib/Pings/index.ts +6 -6
@@ 7,7 7,7 @@ export enum PingType {
Mention,
}
-export interface HeadsUp {
+export interface Toast {
id: string,
occasion?: [icon: any, name: string],
text: string,
@@ 15,13 15,13 @@ export interface HeadsUp {
noisy: boolean,
}
-export const heads_ups = writable<HeadsUp[]>([]);
+export const visible_toasts = writable<Toast[]>([]);
-export function show_heads_up(thing: Omit<HeadsUp, "id">) {
+export function show_toast(thing: Omit<Toast, "id">) {
const id = window.crypto.randomUUID();
- heads_ups.update(val => [...val, { ...thing, id }]);
+ visible_toasts.update(val => [...val, { ...thing, id }]);
}
-export function dismiss_heads_up(id: string) {
- heads_ups.update(val => val.filter(o => o.id != id));
+export function dismiss_toast(id: string) {
+ visible_toasts.update(val => val.filter(o => o.id != id));
}=
\ No newline at end of file
M src/lib/config.ts => src/lib/config.ts +1 -0
@@ 9,6 9,7 @@ export const config_schema = z.object({
hostname: z.string(),
db_path: z.string(),
blob_db_path: z.string(),
+ cache_db_path: z.string(),
});
export const config = config_schema.parse(data);
M src/lib/server/Auth/keys.ts => src/lib/server/Auth/keys.ts +19 -19
@@ 1,22 1,22 @@
import { generateKeyPair } from 'crypto';
+import util from "node:util";
-export async function make_keys(): Promise<{ publicKey: string, privateKey: string }> {
- const p: Promise<{ publicKey: string, privateKey: string }> = new Promise((resolve, reject) => {
- generateKeyPair('rsa', {
- modulusLength: 2048,
- publicKeyEncoding: {
- type: 'spki',
- format: 'pem'
- },
- privateKeyEncoding: {
- type: 'pkcs8',
- format: 'pem'
- }
- }, (err: Error | null, publicKey: string, privateKey: string) => {
- if (err) throw err
- resolve({ publicKey, privateKey })
- })
- });
+const p_generateKeyPair = util.promisify(generateKeyPair)
+const key_opts = {
+ modulusLength: 2048,
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'pem'
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'pem'
+ }
+}
- return p;
-}>
\ No newline at end of file
+export async function make_keys() {
+ const rsa = await p_generateKeyPair('rsa', key_opts);
+ const ed25519 = await p_generateKeyPair("ed25519", key_opts);
+
+ return { rsa, ed25519 };
+}
M src/lib/server/Model/userpage.ts => src/lib/server/Model/userpage.ts +53 -9
@@ 1,6 1,17 @@
+import type { Actor } from '$lib/ActivityPub/actor';
import { make_keys } from '$lib/server/Auth/keys';
import { db } from '$lib/server/db';
+export type UserPage = InternalPage | ExternalPage;
+
+export interface InternalPage extends UserPageData {
+
+}
+
+export interface ExternalPage extends UserPageData {
+ hostname: string,
+}
+
export interface UserPageData {
id: number
username: string
@@ 13,7 24,8 @@ export interface UserPageData {
display_name?: string
bio?: string
pronouns?: string
- homepage?: string
+ homepage?: string,
+ bonus_metadata?: Map<string, string>,
avatar_uri?: string
banner_uri?: string
@@ 41,8 53,8 @@ const last_inserted = db.prepare(`
`).pluck();
const insert_keypair = db.prepare(`
- INSERT INTO key_pair (page_id, public, private)
- VALUES (@id, @publicKey, @privateKey);
+ INSERT INTO key_pair (page_id, kind, public, private)
+ VALUES (@id, @kind, @publicKey, @privateKey);
`);
export async function create_page(props: {
@@ 54,8 66,9 @@ export async function create_page(props: {
db.transaction(() => {
create_stmt.run(props);
const id = last_inserted.run().lastInsertRowid;
- console.log(id);
- insert_keypair.run({ ...keys, id });
+
+ insert_keypair.run({ ...keys.rsa, kind: "rsa", id });
+ insert_keypair.run({ ...keys.ed25519, kind: "ed25519", id });
})();
}
@@ 68,7 81,7 @@ export function fetch_pages_belonging_to(id: number) {
return fetch_pages_stmt.all(id) as UserPageData[] | undefined;
}
-export interface KeyPairData {
+export interface KeyPair {
public: string,
private: string,
page_id: number,
@@ 76,9 89,40 @@ export interface KeyPairData {
const keypair_stmt = db.prepare(`
SELECT * FROM key_pair
- WHERE page_id = ?;
+ WHERE page_id = ? AND kind = 'rsa';
+`)
+
+const keypair_username_stmt = db.prepare(`
+ SELECT *
+ FROM user_page, key_pair
+ WHERE user_page.username = ? AND page_id = user_page.id AND kind = 'rsa';
`)
-export function fetch_keypair(page_id: number) {
- return keypair_stmt.get(page_id) as KeyPairData | undefined;
+const fetch_keypair = (page_id: number | { username: string }) => (
+ typeof page_id == "number"
+ ? keypair_stmt.get(page_id) as KeyPair | undefined
+ : keypair_username_stmt.get(page_id.username) as KeyPair | undefined
+)
+
+const from_actor = (actor: Actor): ExternalPage => ({
+ id: 0, // todo
+ contributors: [],
+ hostname: new URL(actor.id).hostname,
+ username: actor.preferredUsername,
+ avatar_uri: actor.icon?.url,
+ bio: actor.summary,
+ display_name: actor.name,
+ // yeah that'll do
+ bonus_metadata: <Map<string, string>>actor.attachment?.reduce((acc, x) => {
+ if (x.type == "PropertyValue") acc.set(x.name, x.value);
+ return acc;
+ }, new Map()),
+ followers: [],
+ following: [],
+ public_key: ''
+})
+
+export default {
+ fetch_keypair,
+ from_actor
}=
\ No newline at end of file
M src/lib/server/db.ts => src/lib/server/db.ts +5 -8
@@ 26,15 26,12 @@ class DebugDatabase extends Database {
}
}
-export const db = building
- ? new DebugDatabase(config.db_path)
- : new Database(config.db_path);
+export const [db, blob_db, cache_db]
+ = [config.db_path, config.blob_db_path, config.cache_db_path].map(
+ x => building ? new DebugDatabase(x) : new Database(x)
+ );
-export const blob_db = building
- ? new DebugDatabase(config.blob_db_path)
- : new Database(config.blob_db_path);
-
-[db, blob_db].forEach(d => {
+[db, blob_db, cache_db].forEach(d => {
d.pragma('journal_mode = WAL ');
d.pragma('synchronous = NORMAL');
d.pragma('foreign_keys = ON ');
M src/routes/(guarded)/prefs/+page.svelte => src/routes/(guarded)/prefs/+page.svelte +1 -1
@@ 16,6 16,7 @@
<li><a class="big-button" href="/prefs/profile">profile</a></li>
<li><a class="big-button" href="/prefs/stationery">stationery</a></li>
<li><a class="big-button" href="/prefs/syndication">syndication</a></li>
+ <li><a class="big-button" href="/prefs/cliques">cliques</a></li>
</ul>
<h3>
@@ 26,7 27,6 @@
<li><a class="big-button" href="/prefs/account">details</a></li>
<li><a class="big-button" href="/prefs/appearance">appearance</a></li>
<li><a class="big-button" href="/prefs/accessibility">accessibility</a></li>
- <li><a class="big-button" href="/prefs/wellbeing">wellbeing</a></li>
<li><a class="big-button" href="/prefs/pings">pings</a></li>
<li><a class="big-button" href="/prefs/safety">safety</a></li>
</ul>
M src/routes/(guarded)/prefs/appearance/ColourPicker.svelte => src/routes/(guarded)/prefs/appearance/ColourPicker.svelte +13 -16
@@ 1,17 1,11 @@
<script lang="ts">
import {
- colour_store,
- colours,
- update_colours,
- type ColourSet,
+ colour_store,
+ colours,
+ is_selected,
+ is_selected_strict,
+ update_colours
} from "$lib/Colours";
-
- const is_selected = (colour_set: ColourSet) =>
- $colour_store.includes(colour_set[1]) &&
- $colour_store.includes(colour_set[2]);
-
- const is_selected_strict = (colour_set: ColourSet) =>
- $colour_store[1] == colour_set[1] && $colour_store[2] == colour_set[2];
</script>
<ul>
@@ 25,10 19,13 @@
--set-foreground-tone: color-mix(in srgb, {colour_set[2]}, {colour_set[1]} 10%);"
>
<button
- class:selected={is_selected(colour_set)}
+ class:selected={is_selected($colour_store, colour_set)}
on:click={() =>
update_colours(colour_set, {
- reverse: is_selected_strict(colour_set),
+ reverse: is_selected_strict(
+ $colour_store,
+ colour_set,
+ ),
})}
>
<div class="foreground" />
@@ 54,7 51,7 @@
button {
padding: 0;
- height: 6rem;
+ height: 5.8rem;
aspect-ratio: 1/1;
display: flex;
@@ 68,12 65,12 @@
.foreground {
background-color: var(--set-foreground);
- height: 6rem;
+ height: inherit;
aspect-ratio: 1/2;
}
.background {
background-color: var(--set-background);
- height: 6rem;
+ height: inherit;
aspect-ratio: 1/2;
}
A src/routes/(guarded)/prefs/cliques/+page.svelte => src/routes/(guarded)/prefs/cliques/+page.svelte +22 -0
@@ 0,0 1,22 @@
+<script>
+ import WarningBox from "$lib/Bonus/WarningBox.svelte";
+ import Header from "$lib/Navigation/Header.svelte";
+</script>
+
+<Header parents={[["preferences", "/prefs"]]}>cliques</Header>
+
+<p>
+ Cliques allow you to privately send posts to a limited audience of people.
+</p>
+
+<WarningBox title="Privacy Notice">
+ While messages to cliques are encryped and invisible to outside users, they
+ can still be reported by clique members. Moderators will be able to view
+ reported posts in plain text. Don't post any sensitive information!
+</WarningBox>
+
+<WarningBox title="Alpha Warning">
+ Basingstoke alpha-quality software. A massive unnoticed security
+ vulnerability is entirely feasible here. Seriously, don't post anything
+ sensitive!!
+</WarningBox>
M src/routes/(guarded)/prefs/profile/+page.server.ts => src/routes/(guarded)/prefs/profile/+page.server.ts +2 -2
@@ 53,7 53,7 @@ async function process_banner(banner: File) {
const id = Media.store("image/webp", image);
console.log(id)
- return Media.get_uri_of(id);
+ return Media.resolve(id);
}
async function process_avatar(avatar: File) {
@@ 61,5 61,5 @@ async function process_avatar(avatar: File) {
const id = Media.store("image/webp", buf);
console.log(id)
- return Media.get_uri_of(id);
+ return Media.resolve(id);
}=
\ No newline at end of file
M src/routes/+layout.svelte => src/routes/+layout.svelte +3 -3
@@ 15,7 15,7 @@
import DevHeader from "$lib/Bonus/DevHeader.svelte";
import { themed_favicon } from "$lib/Bonus/favicon";
import { setContext } from "svelte";
- import HeadsUpses from "$lib/Pings/HeadsUpses.svelte";
+ import ToastList from "$lib/Pings/ToastList.svelte";
import Footer from "./Footer.svelte";
export let data;
@@ 27,7 27,7 @@
$: style = `
--foreground-color: ${$colour_store[2] ?? "black"};
--background-color: ${$colour_store[1] ?? "white"};
- --foreground-tone: color-mix(in srgb, ${$colour_store[2]}, ${$colour_store[1]} 10%);
+ --foreground-tone: color-mix(in srgb, ${$colour_store[2]}, ${$colour_store[1]} 50%);
--background-tone: color-mix(in srgb, ${$colour_store[1]}, ${$colour_store[2]} 10%);
`;
@@ 63,7 63,7 @@
/>
{/if}
<main class:meta id="content"><slot /></main>
- <HeadsUpses />
+ <ToastList />
<Footer wisdom={data.wisdom} />
</div>
M => +2 -1
@@ 66,13 66,14 @@
grid-column: 1 / span 2;
background: linear-gradient(
to bottom,
var(--background),
transparent,
color-mix(in srgb, var(--foreground) 15%, transparent)
);
margin-left: -1rem;
margin-right: -1rem;
margin-top: -4rem;
padding: 4rem 0;
z-index: 1;
}
.inner {
M src/routes/admin/instance/+page.svelte => src/routes/admin/instance/+page.svelte +85 -7
@@ 1,5 1,9 @@
<script>
- import Header from '$lib/Navigation/Header.svelte';
+ import PrefsFooter from "$lib/Bonus/PrefsFooter.svelte";
+ import Button from "$lib/Buttons/Button.svelte";
+ import FoldingThing from "$lib/Forms/FoldingThing.svelte";
+ import FormInput from "$lib/Forms/FormInput.svelte";
+ import Header from "$lib/Navigation/Header.svelte";
export let data, form;
let {
@@ 12,12 16,9 @@
<title>Instance Settings – Basingstoke</title>
</svelte:head>
-<Header parents={[['admin', '/admin']]}>instance</Header>
+<Header parents={[["admin", "/admin"]]}>instance</Header>
-<p>
- These details will be presented to users of this instance, as well as other
- instances looking to federate with this one. Make sure they're accurate.
-</p>
+<p>some details about your instance and its general vibe</p>
{#if form?.success}
<p role="status">Saved!</p>
@@ 31,7 32,67 @@
<p role="alert">{form.error}</p>
{/if}
-<form class="bz-form" method="post" style="margin-top: 2rem;">
+<form method="post" class="grid">
+ <FormInput
+ label="hostname"
+ flavour_text="you probably shouldn't change this.
+ if you need to, set it in the configuration file"
+ >
+ <input type="text" disabled value={config.hostname} />
+ </FormInput>
+
+ <FormInput label="name" flavour_text="what your instance is called">
+ <input type="text" value={name} />
+ </FormInput>
+
+ <FormInput
+ style="grid-column: 1 / span 2"
+ label="subtitle"
+ flavour_text="a short tagline or what have you"
+ >
+ <input type="text" name="short_desc" value={short_desc} />
+ </FormInput>
+
+ <FormInput
+ style="grid-column: 1 / span 2"
+ label="description"
+ flavour_text="
+ a place to put more words about your instance.
+ markdown works fine here
+ "
+ >
+ <textarea rows="4" name="long_desc" value={long_desc} />
+ </FormInput>
+
+ <div style="grid-column: 1 / span 2;">
+ <FoldingThing title="Rules" subtitle="lay down the law">
+ <p>having some rules is a good idea.</p>
+ </FoldingThing>
+ <FoldingThing
+ title="Terms of Service"
+ subtitle="don't get sued"
+ ></FoldingThing>
+ <FoldingThing
+ title="Privacy Policy"
+ subtitle="don't get sued (in europe)"
+ ></FoldingThing>
+ </div>
+
+ <PrefsFooter
+ style="grid-column: 1 / span 2; margin-top: 2rem;"
+ let:scrolled
+ >
+ <Button size="medium">↶ Revert</Button>
+
+ <Button
+ size={scrolled ? "medium" : "big"}
+ style="margin-left: auto;"
+ type="submit">Save ✓</Button
+ >
+ </PrefsFooter>
+</form>
+
+<!-- <form class="bz-form" method="post" style="margin-top: 2rem;">
<section class="two-cols">
<label>
<span>Host Name</span>
@@ 74,4 135,21 @@
</form>
<style>
+</style> -->
+
+<style>
+ .grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ }
+
+ .text {
+ grid-column: 1 / span 2;
+ }
+
+ p[role="alert"],
+ p[role="status"] {
+ margin-bottom: 1rem;
+ }
</style>
M src/routes/its/[username]/+page.svelte => src/routes/its/[username]/+page.svelte +0 -1
@@ 3,7 3,6 @@
import Filters from "./Filters.svelte";
import HeaderImage from "./HeaderImage.svelte";
import Metadata from "./Metadata.svelte";
- import Pinned from "./Pinned.svelte";
import Placeholder from "./Placeholder.svelte";
export let data;
M src/routes/its/[username]/Filters.svelte => src/routes/its/[username]/Filters.svelte +10 -31
@@ 1,6 1,6 @@
<script lang="ts">
- import Menu from "$lib/Bonus/Menu.svelte";
import MenuProse from "$lib/Bonus/MenuProse.svelte";
+ import SensibleMenu from "$lib/Bonus/SensibleMenu.svelte";
import Checkbox from "$lib/Forms/Checkbox.svelte";
import { Sliders } from "$lib/Icons";
import Icon from "$lib/Icons/Icon.svelte";
@@ 15,8 15,6 @@
export let filter: FilterType, own_page: boolean;
- let trigger: HTMLButtonElement, open: boolean, xy: [number, number];
-
const filters = <{ id: FilterType; label: string }[]>(
[
{ id: "everything", label: own_page ? "Public" : "All Posts" },
@@ 27,21 25,6 @@
{ id: "media", label: "Media" },
].filter((o) => o)
);
-
- function onclick(e: unknown) {
- let event = e as MouseEvent & {
- currentTarget: EventTarget & HTMLButtonElement;
- };
-
- event.preventDefault();
- if (!open) {
- const rect = event.currentTarget.getBoundingClientRect();
- xy = [event.clientX || rect.x, event.clientY || rect.y];
- open = true;
- } else {
- open = false;
- }
- }
</script>
<div class="filters">
@@ 54,18 37,14 @@
</li>
{/each}
</ul>
- <button bind:this={trigger} on:click={onclick}>
- <Icon source={Sliders} label="Settings" />
- </button>
- <Menu
- anchor="right"
- mode="freestyle"
- title="Filter Settings"
- bind:trigger
- bind:xy
- bind:open
- >
- <MenuProse style="height:max-content">
+ <SensibleMenu mode="freestyle" anchor="right" title="filter posts">
+ <svelte:fragment slot="button" let:open>
+ <button on:click={open}>
+ <Icon source={Sliders} label="Settings" />
+ </button>
+ </svelte:fragment>
+
+ <MenuProse>
<h1>Filters</h1>
<div class="boxes">
<Checkbox checked bigness="maybe">Show Reposts</Checkbox>
@@ 74,7 53,7 @@
</Checkbox>
</div>
</MenuProse>
- </Menu>
+ </SensibleMenu>
</div>
<style>
M => +1 -1
@@ 8,7 8,7 @@
@keyframes fade-in {
0% {
opacity: 0;
scale: 1.1;
scale: 1.05;
}
100% {
opacity: 1;
M src/routes/its/[username]/Metadata.svelte => src/routes/its/[username]/Metadata.svelte +7 -7
@@ 1,20 1,20 @@
<script lang="ts">
- import ProfileHeader from "./ProfileHeader.svelte";
+ import type { PostData } from "$lib/server/Model/post";
+ import type { UserPage } from "$lib/server/Model/userpage";
import MetadataTable from "./MetadataTable.svelte";
import Pinned from "./Pinned.svelte";
- import type { UserPageData } from "$lib/server/Model/userpage";
- import type { PostData } from "$lib/server/Model/post";
+ import ProfileHeader from "./ProfileHeader.svelte";
- export let page: UserPageData;
+ export let page: UserPage;
export let own_page = false;
export let logged_in = false;
export let is_following: boolean;
- export let pinned: PostData;
+ export let pinned: PostData | undefined = undefined;
const table_values = new Map<
string,
string | { value: string; linkify: boolean }
- >();
+ >(page.bonus_metadata);
if (page.pronouns) table_values.set("Pronouns", page.pronouns);
@@ 25,7 25,7 @@
<ProfileHeader {page} {own_page} {logged_in} {is_following}></ProfileHeader>
<div>
- {#if page.homepage || page.pronouns}
+ {#if table_values.size != 0}
<section>
<MetadataTable values={table_values} />
</section>
M src/routes/its/[username]/MetadataTable.svelte => src/routes/its/[username]/MetadataTable.svelte +3 -1
@@ 1,4 1,6 @@
<script lang="ts">
+ import sanitize from "sanitize-html";
+
export let values: Map<
string,
string | undefined | { value?: string; linkify: boolean }
@@ 23,7 25,7 @@
{value.value}
{/if}
{:else}
- {value}
+ {@html sanitize(value ?? "")}
{/if}
</td>
</tr>
M => +6 -3
@@ 5,15 5,18 @@
import Button from "$lib/Buttons/Button.svelte";
import { Pencil } from "$lib/Icons";
import Icon from "$lib/Icons/Icon.svelte";
import type { UserPageData } from "$lib/server/Model/userpage";
import type { UserPage } from "$lib/server/Model/userpage";
import MoreButton from "./MoreButton.svelte";
export let page: UserPageData;
export let page: UserPage;
export let own_page = false;
export let logged_in = false;
export let is_following: boolean;
const { bio, display_name, username, avatar_uri } = page;
const { bio, display_name, avatar_uri } = page;
const username = "hostname" in page
? `${page.username}@${page.hostname}`
: page.username;
</script>
<header>
A src/routes/its/[username]/at/[server]/+page.server.ts => src/routes/its/[username]/at/[server]/+page.server.ts +13 -0
@@ 0,0 1,13 @@
+import { Actor } from "$lib/ActivityPub/actor";
+import UserPage from "$lib/server/Model/userpage";
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async ({ params, locals }) => {
+ const actor = await Actor.fetch_remote(params.username, params.server, {
+ as: locals.page?.username,
+ });
+ console.log(actor);
+ const page = UserPage.from_actor(actor);
+
+ return { page }
+};<
\ No newline at end of file
A src/routes/its/[username]/at/[server]/+page.svelte => src/routes/its/[username]/at/[server]/+page.svelte +12 -0
@@ 0,0 1,12 @@
+<script>
+ import Metadata from "../../Metadata.svelte";
+
+ export let data;
+</script>
+
+<Metadata
+ is_following={false}
+ page={data.page}
+ logged_in={true}
+ own_page={false}
+/>
M src/routes/its/[username]/follow/+page.svelte => src/routes/its/[username]/follow/+page.svelte +2 -1
@@ 10,7 10,8 @@
<style>
big {
- display: block;
+ display: flex;
+ align-items: center;
font-family: var(--font-display);
font-size: 5rem;
M src/routes/testpage/+page.svelte => src/routes/testpage/+page.svelte +2 -2
@@ 15,7 15,7 @@
Number9Alt,
} from "$lib/Icons";
import ContextMenuItem from "$lib/Bonus/MenuItem.svelte";
- import { show_heads_up } from "$lib/Pings";
+ import { show_toast } from "$lib/Pings";
import { goto } from "$app/navigation";
import { setContext } from "svelte";
@@ 47,7 47,7 @@
<br />
<Button
- on:click={(e) => show_heads_up({
+ on:click={(e) => show_toast({
text: "Post Published!",
noisy: false,
buttons: [
M src/routes/user/[username]/+server.ts => src/routes/user/[username]/+server.ts +10 -6
@@ 1,16 1,20 @@
+import { ap_accept, check_for_accept_header } from '$lib/ActivityPub';
import { Actor } from '$lib/ActivityPub/actor';
+import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
-import { check_for_accept_header } from '$lib/ActivityPub';
export const GET: RequestHandler = async ({
request,
params: { username },
}) => {
- check_for_accept_header(request, username);
-
- const actor = await Actor.from_db(username);
+ const accepted = check_for_accept_header(request, username);
+ if (!accepted) {
+ throw redirect(303, `/its/${username}`);
+ }
- return new Response(actor.to_object(), {
- headers: { 'Content-Type': 'application/activity+json' },
+ const actor = await Actor.fetch_local(username);
+
+ return new Response(actor.to_ap(), {
+ headers: { 'Content-Type': ap_accept },
});
};