Skip to content

Commit 97d9984

Browse files
committed
Merge pull request #13 from dodok8/dodok8-fronted-followers
2 parents c6e410a + 7832fd6 commit 97d9984

File tree

3 files changed

+119
-3
lines changed

3 files changed

+119
-3
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@ To be released.
3535
- Added `Create` class.
3636
- Added `MemoryCachedRepository` class.
3737

38+
- Added web frontend followers page. [[#2], [#13] by Hyeonseo Kim]
39+
40+
- Added `/followers` route that displays a list of bot followers.
41+
- Made follower count on the main page clickable, linking to `/followers`.
42+
3843
- Upgraded Fedify to 1.8.8.
3944

45+
[#2]: https://github.com/fedify-dev/botkit/issues/2
4046
[#7]: https://github.com/fedify-dev/botkit/issues/7
4147
[#8]: https://github.com/fedify-dev/botkit/pull/8
48+
[#13]: https://github.com/fedify-dev/botkit/pull/13
4249

4350
### @fedify/botkit-sqlite
4451

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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/jsx */
18+
import { type Actor, getActorHandle, Link } from "@fedify/fedify/vocab";
19+
import type { Session } from "../session.ts";
20+
21+
export interface FollowerProps {
22+
readonly actor: Actor;
23+
readonly session: Session<unknown>;
24+
}
25+
26+
export async function Follower({ actor, session }: FollowerProps) {
27+
const { context } = session;
28+
const author = actor;
29+
const authorIcon = await actor?.getIcon({
30+
documentLoader: context.documentLoader,
31+
contextLoader: context.contextLoader,
32+
suppressError: true,
33+
});
34+
const authorHandle = await getActorHandle(author);
35+
36+
return (
37+
<article>
38+
<header>
39+
{author?.id
40+
? (
41+
<hgroup>
42+
{authorIcon?.url && (
43+
<img
44+
src={authorIcon.url instanceof Link
45+
? authorIcon.url.href?.href
46+
: authorIcon.url.href}
47+
width={authorIcon.width ?? undefined}
48+
height={authorIcon.height ?? undefined}
49+
alt={authorIcon.name?.toString() ?? undefined}
50+
style="float: left; margin-right: 1em; height: 64px;"
51+
/>
52+
)}
53+
<h3>
54+
<a href={author.url?.href?.toString() ?? author.id.href}>
55+
{author.name}
56+
</a>
57+
</h3>{" "}
58+
<p>
59+
<span style="user-select: all;">{authorHandle}</span>
60+
</p>
61+
</hgroup>
62+
)
63+
: <em>(Deleted account)</em>}
64+
</header>
65+
</article>
66+
);
67+
}

packages/botkit/src/pages.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { decode } from "html-entities";
3131
import type { BotImpl } from "./bot-impl.ts";
3232
import { Layout } from "./components/Layout.tsx";
3333
import { Message } from "./components/Message.tsx";
34+
import { Follower } from "./components/Follower.tsx";
3435
import { getMessageClass, isMessageObject, textXss } from "./message-impl.ts";
3536
import type { MessageClass } from "./message.ts";
3637
import type { Uuid } from "./repository.ts";
@@ -142,9 +143,11 @@ app.get("/", async (c) => {
142143
</a>{" "}
143144
&middot;{" "}
144145
<span>
145-
{followersCount === 1
146-
? `1 follower`
147-
: `${followersCount.toLocaleString("en")} followers`}
146+
<a href="/followers">
147+
{followersCount === 1
148+
? `1 follower`
149+
: `${followersCount.toLocaleString("en")} followers`}
150+
</a>
148151
</span>{" "}
149152
&middot;{" "}
150153
<span>
@@ -206,6 +209,45 @@ app.get("/", async (c) => {
206209
);
207210
});
208211

212+
app.get("/followers", async (c) => {
213+
const { bot } = c.env;
214+
const ctx = bot.federation.createContext(c.req.raw, c.env.contextData);
215+
const session = bot.getSession(ctx);
216+
const followersCount = await bot.repository.countFollowers();
217+
const followers = await Array.fromAsync(bot.repository.getFollowers());
218+
219+
const url = new URL(c.req.url);
220+
const activityLink = ctx.getActorUri(bot.identifier);
221+
const feedLink = new URL("/feed.xml", url);
222+
223+
return c.html(
224+
<Layout
225+
bot={bot}
226+
host={url.host}
227+
activityLink={activityLink}
228+
feedLink={feedLink}
229+
>
230+
<header class="container">
231+
<h1>
232+
<a href="/">&larr;</a>{" "}
233+
{followersCount === 1
234+
? `1 follower`
235+
: `${followersCount.toLocaleString("en")} followers`}
236+
</h1>
237+
</header>
238+
<main class="container">
239+
{followers.map((follower, index) => (
240+
<Follower
241+
key={follower.id?.href ?? index}
242+
actor={follower}
243+
session={session}
244+
/>
245+
))}
246+
</main>
247+
</Layout>,
248+
);
249+
});
250+
209251
app.get("/tags/:hashtag", async (c) => {
210252
const hashtag = c.req.param("hashtag");
211253
const { bot } = c.env;

0 commit comments

Comments
 (0)