├── .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 | 7 | 8 | 9 | 10 | Microblog 11 | 15 | 16 | 17 |
{props.children}
18 | 19 | 20 | ); 21 | 22 | export interface HomeProps extends PostListProps { 23 | user: User & Actor; 24 | } 25 | 26 | export const Home: FC = ({ user, posts }) => ( 27 | <> 28 |
29 |

{user.name}'s microblog

30 |

31 | {user.name}'s profile 32 |

33 |
34 |
35 | {/* biome-ignore lint/a11y/noRedundantRoles: required by picocss */} 36 |
37 | 43 | 44 |
45 |
46 |
47 |
48 |