├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── src
├── db.ts
├── index.ts
├── logging.ts
├── schema.ts
├── schema.sql
├── views.tsx
├── app.tsx
└── federation.ts
├── .zed
└── settings.json
├── biome.json
├── tsconfig.json
├── package.json
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | microblog.sqlite3*
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "biomejs.biome"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/db.ts:
--------------------------------------------------------------------------------
1 | import Database from "better-sqlite3";
2 |
3 | const db = new Database("microblog.sqlite3");
4 | db.pragma("journal_mode = WAL");
5 | db.pragma("foreign_keys = ON");
6 |
7 | export default db;
8 |
--------------------------------------------------------------------------------
/.zed/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "language_servers": ["biome", "..."],
3 | "code_actions_on_format": {
4 | "source.fixAll.biome": true,
5 | "source.organizeImports.biome": true
6 | },
7 | "wrap_guides": [80]
8 | }
9 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "formatter": {
7 | "enabled": true,
8 | "indentStyle": "space",
9 | "indentWidth": 2
10 | },
11 | "linter": {
12 | "enabled": true,
13 | "rules": {
14 | "recommended": true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "@hono/node-server";
2 | import { behindProxy } from "x-forwarded-fetch";
3 | import app from "./app.tsx";
4 | import "./logging.ts";
5 |
6 | serve(
7 | {
8 | port: 8000,
9 | fetch: behindProxy(app.fetch.bind(app)),
10 | },
11 | (info) =>
12 | console.log("Server started at http://" + info.address + ":" + info.port),
13 | );
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext", "DOM"],
4 | "target": "ESNext",
5 | "module": "NodeNext",
6 | "moduleResolution": "NodeNext",
7 | "allowImportingTsExtensions": true,
8 | "verbatimModuleSyntax": true,
9 | "noEmit": true,
10 | "strict": true,
11 | "jsx": "react-jsx",
12 | "jsxImportSource": "hono/jsx"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/logging.ts:
--------------------------------------------------------------------------------
1 | import { configure, getConsoleSink } from "@logtape/logtape";
2 |
3 | await configure({
4 | sinks: {
5 | console: getConsoleSink(),
6 | },
7 | filters: {},
8 | loggers: [
9 | { category: "microblog", level: "debug", sinks: ["console"] },
10 | { category: "fedify", level: "info", sinks: ["console"] },
11 | { category: "logtape", level: "warning", sinks: ["console"] },
12 | ],
13 | });
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "@fedify/fedify": "^1.0.2",
5 | "@hono/node-server": "^1.12.0",
6 | "@js-temporal/polyfill": "^0.4.4",
7 | "@logtape/logtape": "^0.6.2",
8 | "better-sqlite3": "^11.1.2",
9 | "hono": "^4.5.5",
10 | "stringify-entities": "^4.0.4",
11 | "tsx": "^4.17.0",
12 | "x-forwarded-fetch": "^0.2.0"
13 | },
14 | "devDependencies": {
15 | "@biomejs/biome": "^1.8.3",
16 | "@types/better-sqlite3": "^7.6.11"
17 | },
18 | "scripts": {
19 | "dev": "tsx watch ./src/index.ts",
20 | "prod": "node --import tsx ./src/index.ts",
21 | "createdb": "sqlite3 microblog.sqlite3 < src/schema.sql"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/schema.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number;
3 | username: string;
4 | }
5 |
6 | export interface Actor {
7 | id: number;
8 | user_id: number | null;
9 | uri: string;
10 | handle: string;
11 | name: string | null;
12 | inbox_url: string;
13 | shared_inbox_url: string | null;
14 | url: string | null;
15 | created: string;
16 | }
17 |
18 | export interface Key {
19 | user_id: number;
20 | type: "RSASSA-PKCS1-v1_5" | "Ed25519";
21 | private_key: string;
22 | public_key: string;
23 | created: string;
24 | }
25 |
26 | export interface Follow {
27 | following_id: number;
28 | follower_id: number;
29 | created: string;
30 | }
31 |
32 | export interface Post {
33 | id: number;
34 | uri: string;
35 | actor_id: number;
36 | content: string;
37 | url: string | null;
38 | created: string;
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2024 Hong Minhee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.enabled": false,
3 | "editor.detectIndentation": false,
4 | "editor.indentSize": 2,
5 | "editor.insertSpaces": true,
6 | "[javascript]": {
7 | "editor.defaultFormatter": "biomejs.biome",
8 | "editor.formatOnSave": true,
9 | "editor.codeActionsOnSave": {
10 | "source.organizeImports.biome": "always"
11 | }
12 | },
13 | "[javascriptreact]": {
14 | "editor.defaultFormatter": "biomejs.biome",
15 | "editor.formatOnSave": true,
16 | "editor.codeActionsOnSave": {
17 | "source.organizeImports.biome": "always"
18 | }
19 | },
20 | "[json]": {
21 | "editor.defaultFormatter": "biomejs.biome",
22 | "editor.formatOnSave": true
23 | },
24 | "[jsonc]": {
25 | "editor.defaultFormatter": "biomejs.biome",
26 | "editor.formatOnSave": true
27 | },
28 | "[typescript]": {
29 | "editor.defaultFormatter": "biomejs.biome",
30 | "editor.formatOnSave": true,
31 | "editor.codeActionsOnSave": {
32 | "source.organizeImports.biome": "always"
33 | }
34 | },
35 | "[typescriptreact]": {
36 | "editor.defaultFormatter": "biomejs.biome",
37 | "editor.formatOnSave": true,
38 | "editor.codeActionsOnSave": {
39 | "source.organizeImports.biome": "always"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS users (
2 | id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
3 | username TEXT NOT NULL UNIQUE CHECK (trim(lower(username)) = username
4 | AND username <> ''
5 | AND length(username) <= 50)
6 | );
7 |
8 | CREATE TABLE IF NOT EXISTS actors (
9 | id INTEGER NOT NULL PRIMARY KEY,
10 | user_id INTEGER REFERENCES users (id),
11 | uri TEXT NOT NULL UNIQUE CHECK (uri <> ''),
12 | handle TEXT NOT NULL UNIQUE CHECK (handle <> ''),
13 | name TEXT,
14 | inbox_url TEXT NOT NULL UNIQUE CHECK (inbox_url LIKE 'https://%'
15 | OR inbox_url LIKE 'http://%'),
16 | shared_inbox_url TEXT CHECK (shared_inbox_url
17 | LIKE 'https://%'
18 | OR shared_inbox_url
19 | LIKE 'http://%'),
20 | url TEXT CHECK (url LIKE 'https://%'
21 | OR url LIKE 'http://%'),
22 | created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP)
23 | CHECK (created <> '')
24 | );
25 |
26 | CREATE TABLE IF NOT EXISTS keys (
27 | user_id INTEGER NOT NULL REFERENCES users (id),
28 | type TEXT NOT NULL CHECK (type IN ('RSASSA-PKCS1-v1_5', 'Ed25519')),
29 | private_key TEXT NOT NULL CHECK (private_key <> ''),
30 | public_key TEXT NOT NULL CHECK (public_key <> ''),
31 | created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> ''),
32 | PRIMARY KEY (user_id, type)
33 | );
34 |
35 | CREATE TABLE IF NOT EXISTS follows (
36 | following_id INTEGER REFERENCES actors (id),
37 | follower_id INTEGER REFERENCES actors (id),
38 | created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP)
39 | CHECK (created <> ''),
40 | PRIMARY KEY (following_id, follower_id)
41 | );
42 |
43 | CREATE TABLE IF NOT EXISTS posts (
44 | id INTEGER NOT NULL PRIMARY KEY,
45 | uri TEXT NOT NULL UNIQUE CHECK (uri <> ''),
46 | actor_id INTEGER NOT NULL REFERENCES actors (id),
47 | content TEXT NOT NULL,
48 | url TEXT CHECK (url LIKE 'https://%' OR url LIKE 'http://%'),
49 | created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> '')
50 | );
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Federated microblog example using Fedify
2 | ========================================
3 |
4 | > [!WARNING]
5 | > This program is for educational purposes only. Do not use it for any other
6 | > purpose, since it has not been tested for security.
7 |
8 | This is a simple federated microblog example using [Fedify]. The features of
9 | this program are:
10 |
11 | - A user can create an account (up to one account)
12 | - A user can be followed by other actors in the fediverse
13 | - A follower can unfollow a user
14 | - A user can see the list of their followers
15 | - A user can post a message
16 | - Posts made by a user are visible to their followers in the fediverse
17 | - A user can follow other actors in the fediverse
18 | - A user can see the list of actors they are following
19 | - A user can see the list of posts made by actors they are following
20 |
21 | Since it is a simple example for educational purposes, it has a lot of
22 | limitations:
23 |
24 | - A user cannot configure their profile (bio, picture, etc.)
25 | - A user cannot delete their account
26 | - A user cannot edit/delete their posts
27 | - A user cannot unfollow an actor they once followed
28 | - No likes, shares (reposts), or replies
29 | - No search feature
30 | - No security features (authentication, authorization, etc.)
31 |
32 | [Fedify]: https://fedify.dev/
33 |
34 |
35 | Dependencies
36 | ------------
37 |
38 | This program is written in TypeScript and uses [Node.js]. You need to have
39 | Node.js 20.0.0 or later installed on your system to run this program.
40 |
41 | It also depends on few external libraries besides [Fedify]:
42 |
43 | - [Hono] for web framework
44 | - [SQLite] for database
45 | - A few other libraries; see *package.json* for details
46 |
47 | [Node.js]: https://nodejs.org/
48 | [Hono]: https://hono.dev/
49 | [SQLite]: https://www.sqlite.org/
50 |
51 |
52 | How to run
53 | ----------
54 |
55 | To run this program, you need to install the dependencies first. You can do
56 | that by running the following command:
57 |
58 | ~~~~ sh
59 | npm install --include=dev
60 | ~~~~
61 |
62 | After installing the dependencies, you need to create the database schema.
63 | You can do that by running the following command:
64 |
65 | ~~~~ sh
66 | npm run createdb
67 | ~~~~
68 |
69 | > [!NOTE]
70 | > The above command requires the `sqlite3` program to be installed on your
71 | > system. If it is not installed, you can install it using your package
72 | > manager. For example, on Debian-based systems, you can install it using the
73 | > following command:
74 | >
75 | > ~~~~ sh
76 | > sudo apt install sqlite3
77 | > ~~~~
78 | >
79 | > On macOS, you probably already have it installed.
80 | >
81 | > On Windows, you can download *sqlite-tools-win-x64-\*.zip* from the SQLite
82 | > website's [download page][1] and extract it to a directory in your `PATH`.
83 |
84 | After creating the database schema, you can run the program using the following
85 | command:
86 |
87 | ~~~~ sh
88 | npm run prod
89 | ~~~~
90 |
91 | This will start the program on port 8000. You can access the program by
92 | visiting in your web browser. However, since this
93 | program is an ActivityPub server, you probably need to expose it to the public
94 | internet to communicate with other servers in the fediverse. In that case, you
95 | can use [tunneling services][2].
96 |
97 | [1]: https://www.sqlite.org/download.html
98 | [2]: https://fedify.dev/manual/test#exposing-a-local-server-to-the-public
99 |
100 |
101 | License
102 | -------
103 |
104 | This program is licensed under the [MIT License]. See the *LICENSE* file for
105 | details.
106 |
107 | [MIT License]: https://minhee.mit-license.org/2024/
108 |
--------------------------------------------------------------------------------
/src/views.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "hono/jsx";
2 | import type { Actor, Post, User } from "./schema.ts";
3 |
4 | export const Layout: FC = (props) => (
5 |
6 |