Skip to content

Commit dd8d9c9

Browse files
committed
Web pages
1 parent 6ae6474 commit dd8d9c9

33 files changed

+605
-3
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"multikey",
6363
"nodeinfo",
6464
"phensley",
65+
"pico",
6566
"Pixelfed",
6667
"Tailscale",
6768
"unfollow",

deno.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
"@fedify/fedify": "jsr:@fedify/fedify@^1.3.4",
2020
"@fedify/markdown-it-mention": "jsr:@fedify/markdown-it-mention@^0.2.0",
2121
"@hongminhee/x-forwarded-fetch": "jsr:@hongminhee/x-forwarded-fetch@^0.2.0",
22+
"@hono/hono": "jsr:@hono/hono@^4.6.18",
2223
"@logtape/logtape": "jsr:@logtape/logtape@^0.8.0",
2324
"@phensley/language-tag": "npm:@phensley/language-tag@^1.9.2",
2425
"@std/assert": "jsr:@std/assert@^1.0.10",
2526
"@std/html": "jsr:@std/html@^1.0.3",
27+
"@std/path": "jsr:@std/path@^1.0.8",
2628
"@std/uuid": "jsr:@std/uuid@^1.0.4",
2729
"markdown-it": "npm:markdown-it@^14.1.0",
2830
"xss": "npm:xss@^1.0.15"
@@ -35,6 +37,7 @@
3537
],
3638
"fmt": {
3739
"exclude": [
40+
"src/css/*.css",
3841
"*.md"
3942
]
4043
},

docs/concepts/bot.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,44 @@ Turned off by default.
275275
[ngrok]: https://ngrok.com/
276276
[Tailscale Funnel]: https://tailscale.com/kb/1223/funnel
277277

278+
### `~CreateBotOptions.pages`
279+
280+
The options for the web pages of the bot.
281+
282+
`~PageOptions.color`
283+
: The color of the theme. It will be used for the theme color of the web
284+
pages. The default color is `"green"`.
285+
286+
Here's the list of available colors:
287+
288+
- `"amber"`
289+
- `"azure"`
290+
- `"blue"`
291+
- `"cyan"`
292+
- `"fuchsia"`
293+
- `"green"` (default)
294+
- `"grey"`
295+
- `"indigo"`
296+
- `"jade"`
297+
- `"lime"`
298+
- `"orange"`
299+
- `"pink"`
300+
- `"pumpkin"`
301+
- `"purple"`
302+
- `"red"`
303+
- `"sand"`
304+
- `"slate"`
305+
- `"violet"`
306+
- `"yellow"`
307+
- `"zinc"`
308+
309+
See also the [*Colors* section] of the Pico CSS docs.
310+
311+
`~PageOptions.css`
312+
: The custom CSS to be injected into the web pages. It should be a string
313+
of CSS code.
314+
315+
[*Colors* section]: https://picocss.com/docs/colors
278316

279317
Running the bot
280318
---------------

examples/greet.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createBot, Image, mention, text } from "@fedify/botkit";
1+
import { createBot, Image, link, mention, text } from "@fedify/botkit";
22
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/fedify/x/denokv";
33

44
const kv = await Deno.openKv();
@@ -12,9 +12,17 @@ const bot = createBot<void>({
1212
icon: new URL(
1313
"https://repository-images.githubusercontent.com/913141583/852a1091-14d5-46a0-b3bf-8d2f45ef6e7f",
1414
),
15+
properties: {
16+
"Source code": link(
17+
"examples/greet.ts",
18+
"https://github.com/dahlia/botkit/blob/main/examples/greet.ts",
19+
),
20+
"Powered by": link("BotKit", "https://botkit.fedify.dev/"),
21+
},
1522
kv: new DenoKvStore(kv),
1623
queue: new DenoKvMessageQueue(kv),
1724
behindProxy: true,
25+
pages: { color: "green" },
1826
});
1927

2028
bot.onFollow = async (session, followRequest) => {

src/bot-impl.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import {
5252
} from "@fedify/fedify/vocab";
5353
import { getXForwardedRequest } from "@hongminhee/x-forwarded-fetch";
5454
import metadata from "../deno.json" with { type: "json" };
55-
import type { Bot, CreateBotOptions } from "./bot.ts";
55+
import type { Bot, CreateBotOptions, PagesOptions } from "./bot.ts";
5656
import type {
5757
AcceptEventHandler,
5858
FollowEventHandler,
@@ -65,6 +65,7 @@ import type {
6565
import { FollowRequestImpl } from "./follow-impl.ts";
6666
import { createMessage, messageClasses } from "./message-impl.ts";
6767
import type { Message, MessageClass } from "./message.ts";
68+
import { app } from "./pages.tsx";
6869
import { KvRepository, type Repository, type Uuid } from "./repository.ts";
6970
import { SessionImpl } from "./session-impl.ts";
7071
import type { Session } from "./session.ts";
@@ -90,6 +91,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
9091
readonly repository: Repository;
9192
readonly software?: Software;
9293
readonly behindProxy: boolean;
94+
readonly pages: Required<PagesOptions>;
9395
readonly collectionWindow: number;
9496
readonly federation: Federation<TContextData>;
9597

@@ -115,6 +117,11 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
115117
this.followerPolicy = options.followerPolicy ?? "accept";
116118
this.repository = options.repository ?? new KvRepository(options.kv);
117119
this.software = options.software;
120+
this.pages = {
121+
color: "green",
122+
css: "",
123+
...(options.pages ?? {}),
124+
};
118125
this.federation = createFederation<TContextData>({
119126
kv: options.kv,
120127
queue: options.queue,
@@ -280,6 +287,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
280287
outbox: ctx.getOutboxUri(identifier),
281288
publicKey: keyPairs[0].cryptographicKey,
282289
assertionMethods: keyPairs.map((pair) => pair.multikey),
290+
url: new URL("/", ctx.origin),
283291
});
284292
}
285293

@@ -659,6 +667,14 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
659667
if (this.behindProxy) {
660668
request = await getXForwardedRequest(request);
661669
}
662-
return await this.federation.fetch(request, { contextData });
670+
const url = new URL(request.url);
671+
if (
672+
url.pathname.startsWith("/.well-known/") ||
673+
url.pathname.startsWith("/ap/") ||
674+
url.pathname.startsWith("/nodeinfo/")
675+
) {
676+
return await this.federation.fetch(request, { contextData });
677+
}
678+
return await app.fetch(request, { bot: this, contextData });
663679
}
664680
}

src/bot.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Deno.test("createBot()", async () => {
6767
rel: "self",
6868
type: "application/activity+json",
6969
},
70+
{
71+
href: "https://example.com/",
72+
rel: "http://webfinger.net/rel/profile-page",
73+
},
7074
],
7175
subject: "acct:bot@example.com",
7276
});

src/bot.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,50 @@ export interface CreateBotOptions<TContextData> {
261261
* @default `false`
262262
*/
263263
readonly behindProxy?: boolean;
264+
265+
/**
266+
* The options for the web pages of the bot. If omitted, the default options
267+
* will be used.
268+
*/
269+
readonly pages?: PagesOptions;
270+
}
271+
272+
/**
273+
* Options for the web pages of the bot.
274+
*/
275+
export interface PagesOptions {
276+
/**
277+
* The color of the theme. It will be used for the theme color of the web
278+
* pages. The default color is `"green"`.
279+
* @default `"green"`
280+
*/
281+
readonly color?:
282+
| "amber"
283+
| "azure"
284+
| "blue"
285+
| "cyan"
286+
| "fuchsia"
287+
| "green"
288+
| "grey"
289+
| "indigo"
290+
| "jade"
291+
| "lime"
292+
| "orange"
293+
| "pink"
294+
| "pumpkin"
295+
| "purple"
296+
| "red"
297+
| "sand"
298+
| "slate"
299+
| "violet"
300+
| "yellow"
301+
| "zinc";
302+
303+
/**
304+
* The CSS code for the bot. It will be used for the custom CSS of the web
305+
* pages.
306+
*/
307+
readonly css?: string;
264308
}
265309

266310
/**

src/components/Layout.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// BotKit by Fedify: A framework for creating ActivityPub bots
2+
// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
/** @jsx react-jsx */
17+
/** @jsxImportSource @hono/hono/jsx */
18+
import type { JSX } from "@hono/hono/jsx/jsx-runtime";
19+
import type { BotImpl } from "../bot-impl.ts";
20+
21+
export interface LayoutProps extends JSX.ElementChildrenAttribute {
22+
readonly bot: BotImpl<unknown>;
23+
readonly host: string;
24+
readonly title?: string;
25+
readonly activityLink?: string | URL;
26+
}
27+
28+
export function Layout(
29+
{ bot, host, title, activityLink, children }: LayoutProps,
30+
) {
31+
const handle = `@${bot.username}@${host}`;
32+
return (
33+
<html>
34+
<head>
35+
<meta charset="utf-8" />
36+
<title>
37+
{title == null
38+
? bot.name == null ? handle : `${bot.name} (${handle})`
39+
: title}
40+
</title>
41+
{activityLink &&
42+
(
43+
<link
44+
rel="alternate"
45+
type="application/activity+json"
46+
href={activityLink.toString()}
47+
/>
48+
)}
49+
<link rel="stylesheet" href={`/css/pico.${bot.pages.color}.min.css`} />
50+
<style>{bot.pages.css}</style>
51+
</head>
52+
<body>
53+
{children}
54+
</body>
55+
</html>
56+
);
57+
}

src/components/Message.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// BotKit by Fedify: A framework for creating ActivityPub bots
2+
// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
/** @jsx react-jsx */
17+
/** @jsxImportSource @hono/hono/jsx */
18+
import type { Context } from "@fedify/fedify/federation";
19+
import { LanguageString } from "@fedify/fedify/runtime";
20+
import { getActorHandle, Link } from "@fedify/fedify/vocab";
21+
import type { MessageClass } from "../message.ts";
22+
23+
export interface MessageProps {
24+
readonly message: MessageClass;
25+
readonly context: Context<unknown>;
26+
}
27+
28+
export async function Message({ context, message }: MessageProps) {
29+
const author = await message.getAttribution({
30+
documentLoader: context.documentLoader,
31+
contextLoader: context.contextLoader,
32+
suppressError: true,
33+
});
34+
const authorIcon = await author?.getIcon({
35+
documentLoader: context.documentLoader,
36+
contextLoader: context.contextLoader,
37+
suppressError: true,
38+
});
39+
const authorHandle = author == null ? null : await getActorHandle(author);
40+
return (
41+
<article>
42+
<header>
43+
{author?.id
44+
? (
45+
<hgroup>
46+
{authorIcon?.url && (
47+
<img
48+
src={authorIcon.url instanceof Link
49+
? authorIcon.url.href?.href
50+
: authorIcon.url.href}
51+
width={authorIcon.width ?? undefined}
52+
height={authorIcon.height ?? undefined}
53+
alt={authorIcon.name?.toString() ?? undefined}
54+
style="float: left; margin-right: 1em; height: 64px;"
55+
/>
56+
)}
57+
<h3>
58+
<a href={author.url?.href?.toString() ?? author.id.href}>
59+
{author.name}
60+
</a>
61+
</h3>{" "}
62+
<p>
63+
<span style="user-select: all;">{authorHandle}</span>
64+
</p>
65+
</hgroup>
66+
)
67+
: <em>(Deleted account)</em>}
68+
</header>
69+
<div
70+
dangerouslySetInnerHTML={{ __html: `${message.content}` }}
71+
lang={message.content instanceof LanguageString
72+
? message.content.language.compact()
73+
: undefined}
74+
/>
75+
<footer>
76+
{message.published &&
77+
(
78+
<a href={message.url?.href?.toString() ?? message.id?.href}>
79+
<small>
80+
<time dateTime={message.published.toString()}>
81+
{message.published.toLocaleString("en", {
82+
dateStyle: "full",
83+
timeStyle: "short",
84+
})}
85+
</time>
86+
</small>
87+
</a>
88+
)}
89+
</footer>
90+
</article>
91+
);
92+
}

src/css/pico.amber.min.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)