Compare commits

...

410 commits
v0.7.0 ... main

Author SHA1 Message Date
Jesse Wierzbinski 6462669e9e
chore(packages/client): 🔖 Release 0.2.1
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / detect-circular (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-11-21 12:17:15 +01:00
Jesse Wierzbinski d18f135fbd
refactor(packages/client): 🔥 Remove broken SSO login code 2025-11-21 12:17:02 +01:00
Jesse Wierzbinski 59ad71964b
feat(api): Add FormData support in SSO login 2025-11-21 12:16:47 +01:00
Jesse Wierzbinski bf890aec15
fix(packages/client): 🚨 Remove useless default value 2025-11-21 11:03:16 +01:00
Jesse Wierzbinski 9cf85e951e
test(api): Remove bad streaming timeline tests 2025-11-21 11:02:22 +01:00
Jesse Wierzbinski 814d63554f
chore(packages/client): 🔖 Release 0.2.0 2025-11-21 11:00:40 +01:00
Jesse Wierzbinski ce650a69d4
feat(packages/client): Add SSO login helper to client 2025-11-21 10:59:28 +01:00
Jesse Wierzbinski 5e84fb66f9
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / detect-circular (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-11-21 09:00:37 +01:00
Jesse Wierzbinski 1430d6f7e7
chore: ⬆️ Upgrade to Bun 1.3.2 2025-11-21 08:49:19 +01:00
Jesse Wierzbinski f00ac1a590
chore: ⬆️ Upgrade dependencies 2025-11-21 08:31:02 +01:00
Jesse Wierzbinski f260064083
fix: 🚨 Remove useless function overloading 2025-11-21 07:26:35 +01:00
Gaspard Wierzbinski f2e9c862a6
Merge pull request #42 from versia-pub/refactor/openid
Rewrite old authentication code and go OpenID-only
2025-11-21 07:21:01 +01:00
Jesse Wierzbinski 82bb92768c
fix: 🐛 Set test pattern in DeepSource code analysis config 2025-11-21 06:53:50 +01:00
Jesse Wierzbinski c63b2b320b
fix(config): 💚 Fix incorrect CI config 2025-11-21 06:50:41 +01:00
Jesse Wierzbinski a9dbd2cc4e
fix: 🔥 Remove old tests and docs related to old auth endpoints 2025-11-21 06:45:12 +01:00
Jesse Wierzbinski ae207c10b6
fix: 💚 Update Nix hash 2025-11-03 00:17:57 +01:00
Jesse Wierzbinski 955a933fe9
refactor(api): 🔥 Remove old forced OpenID auth code 2025-10-24 19:12:40 +02:00
Jesse Wierzbinski 45c3f6ae3f
fix(database): 🐛 Cascade application ID deletion 2025-10-24 18:45:07 +02:00
Jesse Wierzbinski bfa7a06958
fix(database): 🐛 Fix applications table not getting deleted correctly during migration 2025-10-24 18:19:22 +02:00
Jesse Wierzbinski c93071666a
fix(packages/client): 🐛 Remove nonexistent options from client's media API
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / detect-circular (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-08-28 07:07:33 +02:00
Jesse Wierzbinski 0d53436f7e
ci: 💚 Fix CI docs
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 8s
Build Docker Images / tests (push) Failing after 8s
Build Docker Images / detect-circular (push) Failing after 8s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 1s
Test Publish / build (sdk) (push) Failing after 1s
2025-08-22 20:46:38 +02:00
Jesse Wierzbinski d8f9f47814
ci: 💚 Add detect-circular as dependency for docker build 2025-08-22 20:45:11 +02:00
Jesse Wierzbinski b46f7828a5
feat: 🔒 Harden Systemd unit config 2025-08-22 20:44:26 +02:00
Jesse Wierzbinski 1a0a27bee1
refactor(database): 🚚 Rename Application to Client everywhere 2025-08-21 01:21:32 +02:00
Jesse Wierzbinski 6f97903f3b
fix(api): Fix all failing tests 2025-08-21 01:15:38 +02:00
Jesse Wierzbinski 1bfc5fb013
refactor(api): ♻️ Rewrite full authentication code to go OpenID-only 2025-08-21 00:45:58 +02:00
Jesse Wierzbinski 4eae4cd062
feat: 🔒 Harden Systemd unit config 2025-08-09 17:15:05 +02:00
Jesse Wierzbinski a6c9d6cd4f
chore: 🐛 Update Nix hashes
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Build Docker Images / detect-circular (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-07-07 06:11:21 +02:00
Jesse Wierzbinski b5e9e35427
refactor: 🔥 Remove plugin functionality, move OpenID plugin to core 2025-07-07 05:52:11 +02:00
Gaspard Wierzbinski 278bf960cb
Merge pull request #41 from versia-pub/refactor/packages
Refactor/packages
2025-07-07 05:16:09 +02:00
Jesse Wierzbinski 0bf5f7c983
fix: 🚨 Fix further DeepSource issues 2025-07-07 05:12:23 +02:00
Jesse Wierzbinski 870b6dbe85
fix: 🚨 Fix Deepsource warnings 2025-07-07 05:08:34 +02:00
Jesse Wierzbinski 2fffbcbede
fix: 🐛 Fix weird imports failing build 2025-07-07 04:52:46 +02:00
Jesse Wierzbinski 551b9a94fe
ci: 💚 Use correct name in CI 2025-07-07 04:39:36 +02:00
Jesse Wierzbinski 24d4150da4
refactor: ⬆️ Upgrade to Zod v4 and hono-openapi 0.5.0 2025-07-07 03:42:35 +02:00
Jesse Wierzbinski add2429606
docs: 📝 Improve Copilot instructions file
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 1s
Test Publish / build (sdk) (push) Failing after 0s
2025-07-06 05:06:05 +02:00
Jesse Wierzbinski eb096c5991
fix: 💚 Fix the Nix build, remove dependency on PNPM 2025-07-06 02:25:06 +02:00
Jesse Wierzbinski 30bb801f9f
fix: 💚 Fix Docker image builds 2025-07-06 02:10:44 +02:00
Jesse Wierzbinski 6d7c545c88
chore: ⬆️ Upgrade Bun to 1.2.18 2025-07-06 02:03:27 +02:00
Jesse Wierzbinski a1300466f4
chore: ⬆️ Upgrade dependencies 2025-07-06 02:02:48 +02:00
Jesse Wierzbinski 90b6399407
refactor: ♻️ Rewrite build system to fit the monorepo architecture
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 1s
Test Publish / build (sdk) (push) Failing after 0s
2025-07-04 06:29:43 +02:00
Jesse Wierzbinski 7de4b573e3
refactor(worker): 🚚 Move queue code to plugin-kit package
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 1s
Test Publish / build (client) (push) Failing after 1s
Test Publish / build (sdk) (push) Failing after 1s
2025-06-29 22:56:52 +02:00
Jesse Wierzbinski dc802ff5f6
feat(api): Begin work on Streaming API 2025-06-29 22:23:03 +02:00
Jesse Wierzbinski 59cd519337
fix(api): 🐛 Fix error when masto-fe stupidly sends empty spoiler_text
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-23 18:53:40 +02:00
Jesse Wierzbinski aff51b651c
refactor: ♻️ Rewrite logging logic into a unified package
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-22 18:43:03 +02:00
Jesse Wierzbinski e1bd389bf1
refactor: 🔥 Remove Devcontainer
It was useless and nobody has ever used it once.
2025-06-22 17:56:54 +02:00
Jesse Wierzbinski 2310e8b33d
chore: ⬆️ Upgrade Bun to 1.2.17 2025-06-22 17:55:50 +02:00
Jesse Wierzbinski 129bc97b09
chore: ⬆️ Upgrade dependencies 2025-06-22 17:53:53 +02:00
Jesse Wierzbinski 1a666e8371
refactor: ♻️ [BROKEN] Refactor Nix build to use fetchBunDeps PR 2025-06-22 17:46:29 +02:00
Jesse Wierzbinski 03940cd8fd
fix: 📄 Add licenses to both JSR packages
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-17 19:59:37 +02:00
Jesse Wierzbinski 1f03017327
refactor: 🚚 Rename @versia/kit to @versia-server/kit
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-15 23:50:34 +02:00
Jesse Wierzbinski 3798e170d0
refactor: 🚚 Move more utilities into packages 2025-06-15 23:43:27 +02:00
Jesse Wierzbinski 5cae547f8d
chore: 💚 Update Nix hashes 2025-06-15 22:26:43 +02:00
Jesse Wierzbinski fde70fa61a
refactor: 🚚 Move testing to its own sub-package 2025-06-15 22:17:33 +02:00
Jesse Wierzbinski a211772309
fix: 🐛 Fix Nix build
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-15 19:23:11 +02:00
Jesse Wierzbinski a6d3ebbeef
refactor: 🚚 Organize code into sub-packages, instead of a single large package 2025-06-15 04:38:20 +02:00
Jesse Wierzbinski 79742f47dc
chore(api): 🔥 Remove clustering ability from API worker
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-15 02:28:02 +02:00
Jesse Wierzbinski 4cc6284eb4
chore: ⬆️ Upgrade dependencies 2025-06-15 02:24:56 +02:00
Jesse Wierzbinski 13d43e8e71
fix: 🐛 Fix Nix build
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-06-04 16:24:09 +02:00
Jesse Wierzbinski 0ae8f632b5
chore: ⬆️ Upgrade to Bun 1.2.15
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 1s
Test Publish / build (sdk) (push) Failing after 1s
2025-06-04 16:20:24 +02:00
Jesse Wierzbinski 85aceb2e48
chore: ⬆️ Upgrade dependencies 2025-06-04 16:18:49 +02:00
Jesse Wierzbinski 0692aa6efa
fix(federation): 👽 Add Reactions to list of supported extensions
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-28 20:42:29 +02:00
Jesse Wierzbinski 15e291b487
chore(packages/client): 🔖 Release 0.2.0-alpha.4 2025-05-28 17:19:04 +02:00
Jesse Wierzbinski 343a507ecc
feat(federation): Federate Reactions 2025-05-28 17:17:03 +02:00
Jesse Wierzbinski fa1dd69e2d
feat(api): Make Reactions API correctly output whether a reaction is remote 2025-05-28 17:07:24 +02:00
Jesse Wierzbinski e0adaca2a2
feat(federation): Add inbound Reaction processing 2025-05-28 16:50:59 +02:00
Jesse Wierzbinski 1fba91f772
chore(config): 👽 Update JSON schema files
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 8s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-28 03:06:49 +02:00
Jesse Wierzbinski 710f965144
fix(federation): 🔒 Enforce content filters for remote content as well 2025-05-28 02:59:26 +02:00
Jesse Wierzbinski c737aeba8e
fix(api): 🐛 Enforce emoji shortcode filters 2025-05-28 02:45:53 +02:00
Jesse Wierzbinski 9eac364e01
refactor(database): 🔥 Always import SQL operators directly from drizzle
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
Build Docker Images / check (push) Failing after 7s
2025-05-26 19:00:24 +02:00
Jesse Wierzbinski d3f411915f
chore(packages/client): 🔖 Release 0.2.0-alpha.3 2025-05-26 18:42:20 +02:00
Jesse Wierzbinski 7bd07801f2
feat(api): Add support for batch account data API 2025-05-26 18:41:45 +02:00
Jesse Wierzbinski 287f428a83
fix(api): 🐛 Add Reaction custom emoji data in statuses 2025-05-26 15:13:56 +02:00
Jesse Wierzbinski 8c0a20a743
chore(packages/client): 🔖 Release 0.2.0-alpha.2
Some checks failed
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-26 11:05:02 +02:00
Jesse Wierzbinski 6d85dbdfcb
fix(packages/client): 🐛 Fix client types, and add missing banner schema for Instance 2025-05-26 11:04:09 +02:00
Jesse Wierzbinski 77cd27a458
ci: 👷 Add CI workflow to publish packages 2025-05-26 09:08:14 +02:00
Jesse Wierzbinski e5e688a154
fix: 🐛 Add type: json specifier to all JSON imports 2025-05-26 08:55:06 +02:00
Jesse Wierzbinski fa5be6bd6a
chore: 🐛 Add jsr registry file to SDK package 2025-05-26 08:52:30 +02:00
Jesse Wierzbinski bf9840bd14
ci: 💚 Fix test publish CI 2025-05-26 08:47:27 +02:00
Jesse Wierzbinski 0551b8e12d
chore: 🔖 Set version to 0.9.0-alpha.0
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-25 16:14:24 +02:00
Jesse Wierzbinski 9722b94eae
feat(api): Add Emoji Reactions 2025-05-25 16:11:56 +02:00
Jesse Wierzbinski 70974d3c35
chore: ⬆️ Upgrade dependencies
Some checks failed
Build Docker Images / check (push) Failing after 6s
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
2025-05-23 19:21:49 +02:00
Jesse Wierzbinski 99a7658956
refactor: 🏷️ Update tsconfig.json and fix resulting errors
Some checks failed
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 8s
Build Docker Images / tests (push) Failing after 8s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-23 17:29:27 +02:00
Jesse Wierzbinski 64068f9d23
docs: 📝 Update changelog
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-19 15:16:00 +02:00
Jesse Wierzbinski 1d96db5313
Merge branch 'chore/v0.8' of https://github.com/versia-pub/server into chore/v0.8 2025-05-19 15:07:50 +02:00
Jesse Wierzbinski e64457c7e2
chore: 🔖 Set all versions to v0.8 2025-05-19 15:07:08 +02:00
Jesse Wierzbinski 6bb4f90c18
chore: 🔖 Set all versions to v0.8 2025-05-19 15:06:48 +02:00
Jesse Wierzbinski e50c8b6a5b
docs: 📝 Update docs to reflect latest changes 2025-05-19 15:05:22 +02:00
Jesse Wierzbinski d12bbb2a1b
ci: 👷 Correctly populate NPM token 2025-05-19 14:44:02 +02:00
Jesse Wierzbinski 0fc94cab3b
ci(api): 🐛 Fix public test workflow 2025-05-19 14:37:48 +02:00
Jesse Wierzbinski affe456fb8
chore: ⬆️ Upgrade dependencies 2025-05-19 14:35:27 +02:00
Jesse Wierzbinski 980b902927
chore: ⬆️ Upgrade Bun to 1.2.13
Some checks failed
Build Docker Images / lint (push) Failing after 6s
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-16 01:51:06 +02:00
Jesse Wierzbinski 7f23aa893b
chore: ⬆️ Upgrade dependencies 2025-05-16 01:50:19 +02:00
Jesse Wierzbinski 5dfcfc548f
refactor(api): ♻️ Make SDK and client package only use resources in their own package
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-13 11:51:59 +02:00
Jesse Wierzbinski c0060f1baf
test(federation): Add test for Note deletion 2025-05-13 11:06:50 +02:00
Jesse Wierzbinski 58bcbc4da7
fix(federation): 🐛 Properly handle manually_approves_followers
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-09 18:47:28 +02:00
Jesse Wierzbinski 4d8fe93188
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-08 12:57:24 +02:00
Jesse Wierzbinski cf08479c48
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-05 18:03:57 +02:00
Jesse Wierzbinski ddb3cfc978
perf(api): Store user and post metrics directly in database instead of recalculating them on-the-fly
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
2025-05-04 16:38:37 +02:00
Jesse Wierzbinski cd12ccd6c1
feat(federation): Implement Share federation support
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-02 12:48:47 +02:00
Jesse Wierzbinski ec69fc2ac0
feat(api): Add media attachments to RSS and Atom feeds
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-01 22:47:29 +02:00
Jesse Wierzbinski 3832328aaf
feat(api): Add RSS and Atom feed functionality 2025-05-01 22:35:32 +02:00
Jesse Wierzbinski 70aff2df68
fix: 🐛 Re-add mathjax dependency
Required for building docs
2025-05-01 22:02:45 +02:00
Jesse Wierzbinski 55329eaae0
chore: ⬆️ Upgrade Bun to 1.2.11 2025-05-01 16:30:58 +02:00
Jesse Wierzbinski 4a4f72fd66
chore: ⬆️ Upgrade dependencies 2025-05-01 16:27:34 +02:00
Jesse Wierzbinski 441c7714d9
fix(api): 🐛 Fix source not being correctly saved when creating notes 2025-05-01 15:52:00 +02:00
Jesse Wierzbinski 294924fc49
fix(api): 🐛 Don't allow replying to reblogs
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-05-01 04:36:08 +02:00
Jesse Wierzbinski 2155ca12be
fix(federation): 🐛 Fix remote interactions not sending out notifications 2025-05-01 03:19:38 +02:00
Jesse Wierzbinski 8874688054
fix(api): 🐛 Massively increase lookup ratelimit 2025-05-01 03:03:14 +02:00
Jesse Wierzbinski cf75679d7f
fix: 🐛 Remove usage of old cli start command 2025-05-01 01:58:22 +02:00
Jesse Wierzbinski 37cbe12c4d
fix: 🐛 Copy detect-libc inside node_modules
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-30 21:26:35 +02:00
Jesse Wierzbinski d2531e8ace
fix: 🐛 Fix incorrect worker Dockerfile executable
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-30 02:53:58 +02:00
Jesse Wierzbinski 9e08248f0c
refactor: 🎨 De-clutter flake
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-20 17:08:46 +02:00
Jesse Wierzbinski 138f4fade3
refactor(database): ♻️ Use Bun.SQL instead of pg
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 7s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-19 14:15:08 +02:00
Jesse Wierzbinski f95f57c4d4
chore: ⬆️ Upgrade to Bun 1.2.10 2025-04-19 13:29:35 +02:00
Jesse Wierzbinski fd9145b7a8
chore: 🔥 Remove useless file 2025-04-19 13:27:25 +02:00
Jesse Wierzbinski 8ae4f3815a
fix(federation): 🚑 Fix broken inbound federation and add end-to-end testing for federation 2025-04-19 13:16:53 +02:00
Jesse Wierzbinski 85ef96fc7f
fix(federation): 🐛 Use explicit header object destructuring in inbox processing
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
2025-04-18 14:58:04 +02:00
Jesse Wierzbinski 6edb0310d8
fix(api): 🐛 Don't require JWT cookie for static content in bull-board UI 2025-04-18 14:38:00 +02:00
Jesse Wierzbinski 054b8bc5cb
fix(federation): 🐛 Fix incorrect destructuring causing federation issues 2025-04-18 14:27:53 +02:00
Jesse Wierzbinski 1d17831454
chore: Remove unused dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 5s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-16 17:31:34 +02:00
Jesse Wierzbinski 98616ceefb
chore: ⬆️ Upgrade dependencies 2025-04-16 17:03:28 +02:00
Jesse Wierzbinski ffa0c209b6
fix(api): 👽 Use new Scalar API
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-16 16:40:47 +02:00
Jesse Wierzbinski a2e907390f
fix(api): 🐛 Don't use URL in Versia entity schemas, fixes OpenAPI 2025-04-16 16:35:17 +02:00
Jesse Wierzbinski 0a712128a5
fix(api): 🐛 Fix OpenID provider logos not showing up
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-15 22:59:05 +02:00
Jesse Wierzbinski d54527454f
build: 🚑 Remove old workaround that isn't needed anymore 2025-04-15 21:45:21 +02:00
Jesse Wierzbinski f35aae6c44
chore: 💚 Update dependency hash 2025-04-15 21:39:16 +02:00
Jesse Wierzbinski fb5c3fcd12
refactor(config): 🔥 Remove dependency on c12, use confbox instead 2025-04-15 21:37:36 +02:00
Jesse Wierzbinski da1e209f9e
fix: 🐛 Replace dataDir with module path 2025-04-15 21:07:47 +02:00
Jesse Wierzbinski d6b15b1b85
fix: 🐛 Disable msgpackr native acceleration in Nix deployment
It keeps causing issues with the module loading and being annoying to work with in general
2025-04-15 21:04:08 +02:00
Jesse Wierzbinski 26f2dca5d6
fix(config): 🐛 Make vapid key schema more consistent with other key schemas 2025-04-15 20:46:29 +02:00
Jesse Wierzbinski 88944712fe
fix(config): 🐛 Trigger vapid key autogeneration even when block is not commented out 2025-04-15 20:43:32 +02:00
Jesse Wierzbinski b67d86dc57
fix: 🐛 Fix NixOS module passing incorrect environment variable 2025-04-15 20:31:25 +02:00
Jesse Wierzbinski dad99e854d
fix: 🐛 Fix NixOS module incorrect systemd definitions 2025-04-15 20:27:50 +02:00
Jesse Wierzbinski 2d4465617b
fix: 🐛 Fix more errors in NixOS module definition 2025-04-15 20:19:24 +02:00
Jesse Wierzbinski d46befbd1d
fix: 🐛 Fix NixOS module definitions 2025-04-15 18:11:14 +02:00
Jesse Wierzbinski 765348c440
fix: 🐛 Fix Nix module errors when importing
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
2025-04-15 14:15:36 +02:00
Jesse Wierzbinski 404d63f6d0
feat(media): Add path_style and path configuration to S3 2025-04-15 13:28:12 +02:00
Jesse Wierzbinski 5bb4e967a7
fix: 📝 Fix typo in Nix docs 2025-04-15 13:05:00 +02:00
Jesse Wierzbinski c26e896afe
docs: 📝 Document Nix installation 2025-04-15 13:03:52 +02:00
Jesse Wierzbinski 2d921438a9
feat: Add DevShell to flake 2025-04-15 11:32:11 +02:00
Jesse Wierzbinski 385997cdcc
feat: Add NixOS module 2025-04-15 11:15:17 +02:00
Jesse Wierzbinski 37bc4458e5
test: 🧪 Fix failing tests due to incorrect cwd resolving
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 5s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-14 17:30:01 +02:00
Jesse Wierzbinski 1beb18e321
build: 🏗️ Package Worker 2025-04-14 17:13:36 +02:00
Jesse Wierzbinski 5a4ce29206
build: 🏗️ Make Nix build great again 2025-04-14 16:51:00 +02:00
Jesse Wierzbinski 1679585c4c
fix: 🚨 Enable more Biome 2.0 rules
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 4s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
2025-04-10 19:56:42 +02:00
Jesse Wierzbinski 963173cdae
chore: ⬆️ Upgrade to Biome 2.0 2025-04-10 19:15:31 +02:00
Jesse Wierzbinski e7aec8752c
refactor(database): 🔥 Remove unnecessary Redis connections 2025-04-10 18:58:44 +02:00
Jesse Wierzbinski dc1b58a791
chore: ⬆️ Upgrade Bun to 1.2.9 2025-04-10 18:53:01 +02:00
Jesse Wierzbinski dbde49b9bd
chore: ⬆️ Upgrade dependencies 2025-04-10 18:50:41 +02:00
Gaspard Wierzbinski 7e44e55b3f
Merge pull request #36 from versia-pub/refactor/federation
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 54s
Build Docker Images / lint (push) Successful in 28s
Build Docker Images / check (push) Successful in 53s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m28s
Refactor/federation
2025-04-09 02:18:14 +02:00
Jesse Wierzbinski 1d301d72ae
fix: 🚨 Fix DeepSource linter warnings 2025-04-09 02:15:00 +02:00
Jesse Wierzbinski 45e5460975
docs(federation): 📝 Update SDK documentation 2025-04-08 21:54:55 +02:00
Jesse Wierzbinski f79b0bc999
refactor(federation): 🔥 Refactor Note federation and creation code 2025-04-08 18:13:30 +02:00
Jesse Wierzbinski 54b2dfb78d
refactor(federation): 🔥 Remove confusing User federation methods 2025-04-08 17:27:08 +02:00
Jesse Wierzbinski 9ff9b90f6b
refactor(federation): ♻️ Refactor User federation code 2025-04-08 16:59:18 +02:00
Jesse Wierzbinski d638610361
refactor(federation): ♻️ Rewrite federation SDK 2025-04-08 16:01:10 +02:00
Jesse Wierzbinski ad1dc13a51
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1m1s
Build Docker Images / lint (push) Successful in 38s
Build Docker Images / check (push) Successful in 51s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 6s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-04-07 21:51:59 +02:00
Jesse Wierzbinski 2908fcc9e8
fix(worker): 🐛 Remove old bull-board patch, use official fix instead
Explicitely specifying the path prevents the module from using `eval`
2025-04-07 21:50:43 +02:00
Jesse Wierzbinski 512e0295a2
fix(config): 🐛 Fix bundling errors related to config
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 52s
Build Docker Images / check (push) Failing after 5m20s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 13s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Build Docker Images / lint (push) Successful in 26s
Nix Build / check (push) Failing after 32m35s
2025-04-06 22:11:18 +02:00
Jesse Wierzbinski 12740a2d06
feat(config): Allow specifying config path via env variable 2025-04-06 21:40:00 +02:00
Jesse Wierzbinski 838f2fd4cf
fix: 🐛 Fix cyclical imports causing crashes 2025-04-06 21:18:24 +02:00
Jesse Wierzbinski 52630e7042
chore: ⬆️ Upgrade dependencies 2025-04-06 20:58:00 +02:00
Jesse Wierzbinski 40b34e4855
fix: 🐛 Fix build error because of missing library
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 4s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-04-01 21:07:56 +02:00
Jesse Wierzbinski 9840b5e10f
fix(config): 🐛 Expand contexts in which the config will autogenerate keys 2025-04-01 20:32:43 +02:00
Jesse Wierzbinski 14855b9dfe
fix: 🐛 Update example docker-compose.yml
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 45s
Build Docker Images / lint (push) Successful in 28s
Build Docker Images / check (push) Successful in 50s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m30s
2025-04-01 16:05:51 +02:00
Jesse Wierzbinski 844fbf7c9e
docs: 📝 Update installation documentation to match latest guidelines 2025-04-01 13:52:47 +02:00
Jesse Wierzbinski 7a6b93a36c
chore: ⬆️ Upgrade Bun to 1.2.8 2025-04-01 13:18:14 +02:00
Jesse Wierzbinski dc1ddb758d
fix(media): 🐛 Don't proxy media from trusted origins, use new ProxiedUrl class
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 6s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 11s
Build Docker Images / tests (push) Failing after 27s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 6s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-03-30 23:44:50 +02:00
Jesse Wierzbinski 411fcd8af5
refactor(api): ♻️ Reduce complexity of a few functions 2025-03-30 23:17:11 +02:00
Jesse Wierzbinski 25ea870f71
refactor: ♻️ Don't use Bun global 2025-03-30 23:06:34 +02:00
Jesse Wierzbinski d55668d529
fix(database): 💩 Replace uuid_generate_v7 in migrations to a dummy PostgreSQL function
Prevents a fatal error when running migrations on vanilla PostgreSQL instance without the pg_uuidv7 extension
2025-03-30 22:32:04 +02:00
Jesse Wierzbinski dde085464c
fix(database): 🐛 Remove some leftover parts of pg_uuidv7 2025-03-30 22:21:01 +02:00
Jesse Wierzbinski 9d79543951
ci: 👷 Use vanilla PostgreSQL 17 in CI tests 2025-03-30 22:12:40 +02:00
Jesse Wierzbinski 37f68bbffd
refactor(database): Remove dependency on pg_uuidv7 extension 2025-03-30 22:10:33 +02:00
Jesse Wierzbinski 2bb3731187
fix: 🐛 Correctly set youch to beta version 2025-03-30 21:45:51 +02:00
Jesse Wierzbinski ed06d0b54c
chore: ⬆️ Upgrade dependencies 2025-03-30 21:15:02 +02:00
Jesse Wierzbinski c68bfdf6e1
refactor(federation): ♻️ Simplify inbox processing by using ApiError 2025-03-30 21:13:47 +02:00
Jesse Wierzbinski 757c227f00
fix(federation): 🐛 Update user processing to not refetch user when its data is already available 2025-03-30 21:02:36 +02:00
Jesse Wierzbinski c9a1581932
feat(api): Implement duration controls on mutes 2025-03-30 20:54:47 +02:00
Jesse Wierzbinski 9d1d56bd08
feat(api): Implement indexing toggle and followers/following privacy settings 2025-03-30 20:32:42 +02:00
Jesse Wierzbinski 666eef063c
chore: 📝 Update changelog 2025-03-30 19:56:46 +02:00
Jesse Wierzbinski 1b983f9334
fix(api): 🐛 Fix routes using incorrect path parameter notation
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 44s
Build Docker Images / lint (push) Successful in 29s
Build Docker Images / check (push) Failing after 5m42s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 13s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m30s
2025-03-29 03:59:06 +01:00
Jesse Wierzbinski e5b7325379
fix: 🐛 Remove broken import from dependency 2025-03-29 03:39:55 +01:00
Jesse Wierzbinski 58342e86e1
refactor(api): ♻️ Move from @hono/zod-openapi to hono-openapi
hono-openapi is easier to work with and generates better OpenAPI definitions
2025-03-29 03:30:06 +01:00
Jesse Wierzbinski 0576aff972
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 46s
Build Docker Images / lint (push) Successful in 33s
Build Docker Images / check (push) Successful in 1m6s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m37s
2025-03-28 22:26:20 +01:00
Jesse Wierzbinski cd4cfa6a70
fix(api): 🐛 Add tags to all API routes that were missing one 2025-03-28 22:12:07 +01:00
Jesse Wierzbinski d75254fc71
refactor(federation): 🚚 Change Like path from /objects/{id} to /likes/{id} 2025-03-28 22:06:42 +01:00
Jesse Wierzbinski 3d3e64edab
feat(api): Implement rate limiting
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 42s
Build Docker Images / lint (push) Successful in 31s
Build Docker Images / check (push) Successful in 1m3s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 13s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 33m18s
2025-03-27 20:12:00 +01:00
Jesse Wierzbinski 1993231663
feat(api): 🧑‍💻 Improve error quality with Youch 2025-03-27 19:08:38 +01:00
Jesse Wierzbinski 58b4d7454f
refactor(api): ♻️ Serve frontend from static files instead of proxying another process 2025-03-27 18:51:22 +01:00
Jesse Wierzbinski 5f8c57b3e1
chore: ⬆️ Upgrade Bun to 1.2.7 2025-03-27 14:41:43 +01:00
Jesse Wierzbinski ebb0f52f1e
chore: ⬆️ Upgrade dependencies 2025-03-27 14:16:22 +01:00
Jesse Wierzbinski c674a1309c
feat(api): Add OpenAPI visualizer
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 53s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-03-24 15:25:40 +01:00
Jesse Wierzbinski 65e2e19ff1
refactor(api): ♻️ Properly reuse error messages and schemas 2025-03-24 14:42:09 +01:00
Jesse Wierzbinski 7112a66e4c
refactor: Refactor tests to not use module mocks, so bun test can be used
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 45s
Build Docker Images / lint (push) Successful in 27s
Build Docker Images / check (push) Successful in 1m7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m31s
2025-03-23 04:12:28 +01:00
Jesse Wierzbinski ec506241f0
test(api): Remove old tests and introduce new, better ones
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 6s
Build Docker Images / lint (push) Successful in 50s
Build Docker Images / check (push) Successful in 1m24s
Build Docker Images / tests (push) Failing after 8s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 15s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 33m5s
2025-03-23 03:34:17 +01:00
Jesse Wierzbinski f1ef85b314
fix: 🏷️ Remove unnecessary ts-expect-error
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-03-22 18:17:53 +01:00
Jesse Wierzbinski 2a1a164d59
fix: 🐛 Recreate broken lockfile 2025-03-22 18:10:48 +01:00
Jesse Wierzbinski 8d1af1b0cd
refactor(api): 🔥 Remove old @versia/client version 2025-03-22 18:04:47 +01:00
Jesse Wierzbinski 54e282b03c
refactor(api): ♻️ Refactor all tests to use new client 2025-03-22 17:32:46 +01:00
Jesse Wierzbinski b6373dc185
fix: 🚨 Use ts-expect-error over ts-ignore
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 45s
Build Docker Images / lint (push) Successful in 30s
Build Docker Images / check (push) Successful in 1m11s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-03-22 04:06:37 +01:00
Jesse Wierzbinski 84b9fc3719
refactor(api): ♻️ Refactor test code to use new client 2025-03-22 04:04:06 +01:00
Jesse Wierzbinski 232ce83e4d
refactor(api): 🔥 Remove old ID lookup API 2025-03-22 03:34:59 +01:00
Jesse Wierzbinski dd38a3900c
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 58s
Build Docker Images / lint (push) Successful in 30s
Build Docker Images / check (push) Failing after 1m11s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 16s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 33m10s
2025-03-22 02:38:01 +01:00
Jesse Wierzbinski c2d270e4e3
chore: ⬆️ Upgrade Bun to 1.2.5 2025-03-22 02:36:18 +01:00
Jesse Wierzbinski 3fe07a79b8
refactor(api): ♻️ Move all client schema code to new package 2025-03-22 02:34:03 +01:00
Jesse Wierzbinski 52602c3da7
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 56s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 11s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-03-16 17:01:07 +01:00
Jesse Wierzbinski 956a5fd2b3
chore: ⚰️ Remove unused dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-02-26 00:37:54 +01:00
Jesse Wierzbinski 764061b4be
fix: 🔊 Automatically create logs folder if it doesn't exist 2025-02-26 00:28:05 +01:00
Gaspard Wierzbinski 457a4054b7
Merge pull request #35 from versia-pub/refactor/cli
Rewrite CLI
2025-02-26 00:14:47 +01:00
Jesse Wierzbinski ce64afe283
fix(cli): 🚨 Use RegExp literal instead of .match() 2025-02-26 00:12:31 +01:00
Jesse Wierzbinski f98d7ec560
fix: 📌 Recalculate lockfile 2025-02-26 00:09:22 +01:00
Jesse Wierzbinski 21b4f8a024
Merge branch 'main' into refactor/cli 2025-02-26 00:07:44 +01:00
Jesse Wierzbinski 5b756ea2dd
refactor(cli): ♻️ Rewrite CLI with Clerk. Removes a bunch of commands now covered by API. 2025-02-26 00:00:21 +01:00
Jesse Wierzbinski fc1877c6cc
chore: ⬆️ Upgrade Bun to 1.2.3
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 58s
Build Docker Images / lint (push) Successful in 33s
Build Docker Images / check (push) Failing after 5m31s
Build Docker Images / tests (push) Failing after 8s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 17s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m49s
2025-02-25 23:33:22 +01:00
Jesse Wierzbinski f114f9a51a
chore: ⬆️ Upgrade dependencies 2025-02-25 23:32:05 +01:00
Jesse Wierzbinski 066220ffbd
feat: Add Copilot Instructions file 2025-02-25 23:18:39 +01:00
Jesse Wierzbinski e19a1b061a
chore(federation): ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 41s
Build Docker Images / lint (push) Successful in 31s
Build Docker Images / check (push) Successful in 1m1s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m23s
2025-02-18 12:18:52 +01:00
Jesse Wierzbinski 28577d017a
docs: 📝 Update changelog
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 4s
2025-02-18 00:59:55 +01:00
Jesse Wierzbinski 7fc7959712
chore: ⬆️ Upgrade dependencies 2025-02-17 23:33:12 +01:00
Jesse Wierzbinski 6622ee9020
refactor(federation): ♻️ Move Versia Note URIs to /notes, instead of /objects 2025-02-17 23:31:39 +01:00
Jesse Wierzbinski 4063d58d79
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1m1s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 11s
Build Docker Images / tests (push) Failing after 5s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 4s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s
2025-02-17 13:09:26 +01:00
Jesse Wierzbinski ed9ffe34f4
feat(federation): ⬆️ Upgrade to Versia 0.5 2025-02-17 13:07:43 +01:00
Jesse Wierzbinski e6c7e8a597
docs: 📝 Remove support for from-source installs
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 5s
2025-02-15 18:49:31 +01:00
Jesse Wierzbinski e5b44cb946
docs: 💸 Add Fastly sponsorship note 2025-02-15 14:43:17 +01:00
Gaspard Wierzbinski 131fd1c6e9
Merge pull request #34 from versia-pub/refactor/config
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 47s
Build Docker Images / lint (push) Successful in 30s
Build Docker Images / check (push) Successful in 1m1s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 1s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m28s
Overhaul config system
2025-02-15 02:59:06 +01:00
Jesse Wierzbinski ef57198220
fix(config): 💚 Enable Challenges in CI tests 2025-02-15 02:54:22 +01:00
Jesse Wierzbinski 935ad72936
fix: 🚨 Throw new error instead of using process.exit() 2025-02-15 02:53:08 +01:00
Jesse Wierzbinski bf42f3d677
fix(config): 💚 Enable Sonic in the CI tests 2025-02-15 02:51:57 +01:00
Jesse Wierzbinski 045b7d6083
fix(config): 🐛 Update JSON schema for config 2025-02-15 02:48:39 +01:00
Jesse Wierzbinski 54fd81f076
refactor(config): ♻️ Redo config structure from scratch, simplify validation code, improve checks, add support for loading sensitive data from paths 2025-02-15 02:47:29 +01:00
Jesse Wierzbinski d4afd84019
refactor(media): 🐛 Use hono/proxy in media proxy
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 38s
Build Docker Images / lint (push) Successful in 28s
Build Docker Images / check (push) Failing after 5m26s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 14s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m28s
2025-02-14 18:16:07 +01:00
Jesse Wierzbinski 3fe9926fcf
chore: ⬆️ Upgrade dependencies 2025-02-14 18:09:15 +01:00
Gaspard Wierzbinski 416e3009a0
Merge pull request #33 from versia-pub/refactor/types
Overhaul OpenAPI schemas and validation
2025-02-14 18:04:41 +01:00
Gaspard Wierzbinski 276f82882f
Merge branch 'main' into refactor/types 2025-02-14 17:59:18 +01:00
Jesse Wierzbinski 59a3463c72
fix(api): 🚨 Use shorthand property syntax everywhere, remove useless template literals 2025-02-14 17:55:54 +01:00
Jesse Wierzbinski 6a810529bc
refactor(api): 🏷️ Finish OpenAPI documentation refactor 2025-02-14 17:49:34 +01:00
Jesse Wierzbinski 1856176de5
refactor(api): 🏷️ Port almost all remaining v1 endpoints to OpenAPI 2025-02-14 16:44:32 +01:00
Jesse Wierzbinski 247a8fbce3
refactor(api): 🏷️ Port more misc endpoints to use new schemas
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
2025-02-13 02:34:44 +01:00
Jesse Wierzbinski e3e285571e
refactor(api): 🏷️ Port all /api/v1/accounts to use new schemas
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
2025-02-13 01:31:15 +01:00
Jesse Wierzbinski a0ce18337a
refactor(api): 🏷️ Use more new schemas 2025-02-12 23:33:07 +01:00
Jesse Wierzbinski bff1c5f734
refactor(api): 🏷️ Begin porting all code over to new schemas 2025-02-12 23:25:22 +01:00
Jesse Wierzbinski fda1167234
feat(api): 🏷️ Finish porting full Mastodon API to OpenAPI 2025-02-12 23:04:44 +01:00
Jesse Wierzbinski 6ff27ede73
feat(cli): Add token generation command to CLI
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 51s
Build Docker Images / lint (push) Successful in 28s
Build Docker Images / check (push) Successful in 54s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 32m24s
2025-02-11 18:51:28 +01:00
Jesse Wierzbinski 03d3a2d3d4
feat(cli): Implement note recalculation command 2025-02-11 18:39:38 +01:00
Jesse Wierzbinski 264e2fe8ac
feat(api): 🏷️ Port Role and CustomEmoji OpenAPI schemas
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 1s
2025-02-11 18:22:39 +01:00
Jesse Wierzbinski e5f222c529
chore: Remove unused dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 2m19s
Build Docker Images / lint (push) Successful in 51s
Build Docker Images / check (push) Successful in 1m9s
Build Docker Images / tests (push) Failing after 10s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 22s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 3m32s
2025-02-05 22:57:05 +01:00
Jesse Wierzbinski 546b7446b9
chore: ⬆️ Upgrade dependencies
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5m26s
Build Docker Images / lint (push) Successful in 50s
Build Docker Images / check (push) Successful in 1m13s
Build Docker Images / tests (push) Failing after 40s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 25s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 3m49s
2025-02-05 22:50:47 +01:00
Jesse Wierzbinski 7c622730dc
feat(api): 🏷️ Port Status OpenAPI schemas from Mastodon API docs 2025-02-05 22:49:07 +01:00
Jesse Wierzbinski 2aeada4904
feat(api): 🏷️ Port Account OpenAPI schemas from Mastodon API docs 2025-02-05 21:49:39 +01:00
Jesse Wierzbinski 76d1ccc859
refactor(api): ♻️ Use URL literal instead of strings 2025-02-01 16:32:18 +01:00
Jesse Wierzbinski 99fac323c8
chore: ⬆️ Upgrade Bun to 1.2.2 2025-02-01 11:10:57 +01:00
Jesse Wierzbinski ff7b11440d
chore: ⬆️ Upgrade dependencies 2025-02-01 11:07:36 +01:00
Gaspard Wierzbinski 450058213d
Merge pull request #32 from versia-pub/refactor/media
Refactor and simplify the media pipeline
2025-02-01 11:02:28 +01:00
Jesse Wierzbinski 1d2ea36fac
refactor: 🔥 Remove old pre Bun 1.2 compile error check
Bun 1.2 now default to actually throwing an error instead of always succeeding
2025-01-29 17:25:04 +01:00
Jesse Wierzbinski bf071c1b27
chore: 🔥 Remove old S3 client dependency 2025-01-29 17:23:32 +01:00
Jesse Wierzbinski 9ba6237f13
refactor(media): ♻️ Massively simplify media pipeline with Bun.S3 2025-01-29 17:21:40 +01:00
Jesse Wierzbinski f60663506a
fix(api): 🐛 Add missing attributes to /api/v1/instance 2025-01-29 16:05:04 +01:00
Jesse Wierzbinski 29cbe7d293
chore: ⬆️ Upgrade Bun to 1.2.1 2025-01-28 19:32:03 +01:00
Jesse Wierzbinski ba431e2b11
refactor(database): ♻️ Make user avatar and header into a Media instead of plaintext 2025-01-28 19:07:55 +01:00
Jesse Wierzbinski bc961b70bb
refactor(database): 🔥 Simplify media management code 2025-01-28 18:06:33 +01:00
Jesse Wierzbinski cf1104d762
refactor(database): ♻️ Make emojis use a Media instead of just rawdogging the URI 2025-01-28 17:43:43 +01:00
Jesse Wierzbinski c7aae24d42
style(database): 🚚 Reorder declarations of db tables
Putting relations next to the tables makes it easier to not break everything
2025-01-28 17:06:28 +01:00
Jesse Wierzbinski 8ac476fe66
chore: ⬆️ Upgrade dependencies 2025-01-28 16:55:43 +01:00
Jesse Wierzbinski 3216fc339a
refactor(database): ♻️ Move Note <-> Media relations to a many-to-many model instead of one-to-many 2025-01-23 20:36:09 +01:00
Jesse Wierzbinski 9c30dacda7
refactor(database): ♻️ Use ContentFormat to store media data 2025-01-23 19:37:17 +01:00
Jesse Wierzbinski 2f61cd8f0a
refactor(database): 🚚 Rename "Attachment" to "Media" 2025-01-23 16:08:42 +01:00
Jesse Wierzbinski bbd56b600d
refactor: 👷 Switch to Bun's text lockfile 2025-01-23 15:48:12 +01:00
Jesse Wierzbinski c4339e64bd
chore: ⬆️ Upgrade Bun to 1.2.0 2025-01-23 15:28:16 +01:00
Jesse Wierzbinski e32b6f9f8e
chore: ⬆️ Upgrade dependencies 2025-01-23 15:15:34 +01:00
Jesse Wierzbinski 88bb724ae0
chore: ⬆️ Upgrade dependencies 2025-01-17 14:18:31 +01:00
Jesse Wierzbinski 24efc77770
fix: 🚑 Put Sharp back into the API worker
The Attachment processor needs it for metadata
2025-01-07 15:16:51 +01:00
Jesse Wierzbinski 11ba1ab5c8
fix(api): 🐛 Fix failing build
The node_modules directory didn't exist, so cp threw an error
2025-01-07 14:10:54 +01:00
Jesse Wierzbinski b086e65404
refactor(worker): Move blurhash processing to worker 2025-01-06 19:45:32 +01:00
Jesse Wierzbinski 8188a6ffc7
refactor: 🔥 Remove sharp from api worker output 2025-01-06 19:31:42 +01:00
Jesse Wierzbinski ded8799a9c
fix: 🚑 Bundle Sharp into worker 2025-01-06 19:30:51 +01:00
Jesse Wierzbinski 80b874e5fb
refactor(api): Move media processing to background job 2025-01-06 19:21:57 +01:00
Jesse Wierzbinski dcdc8c7365
fix(api): 🐛 Fix user registration incorrectly counting remote users as local 2025-01-06 18:14:09 +01:00
Jesse Wierzbinski 0e9db83279
style: 🚨 Run Biome 2025-01-02 04:52:30 +01:00
Jesse Wierzbinski 3484b1e1a1
fix(api): 🐛 Improve notification text 2025-01-02 04:49:36 +01:00
Jesse Wierzbinski 1c543723fb
fix(api): 🐛 Use aesgcm, not aes128gcm during push notifications
Mastodon's server hates aes128gcm
2025-01-02 04:36:28 +01:00
Jesse Wierzbinski bedc25bacf
fix(api): 🐛 Properly await notification result 2025-01-02 04:27:52 +01:00
Jesse Wierzbinski cde2836982
fix(api): 👽 Ignore what the Mastodon docs say, they're wrong 2025-01-02 04:13:12 +01:00
Jesse Wierzbinski 5d64ecd04f
fix(api): 🐛 Switch base64 version to base64url in subscriptions 2025-01-02 04:02:29 +01:00
Jesse Wierzbinski ea0afdaf22
fix(api): 🐛 Make validation on push subscription more lax 2025-01-02 03:53:38 +01:00
Jesse Wierzbinski 59cf4e384a
fix(api): 🐛 Correctly calculate user based on token 2025-01-02 03:36:54 +01:00
Jesse Wierzbinski 8706c7b405
feat(federation): Add Push Queue to Bull Board 2025-01-02 03:27:26 +01:00
Jesse Wierzbinski 85de7b8ddc
ci: 🐛 Fix bug causing worker Docker image to be a copy of Server 2025-01-02 03:22:18 +01:00
Jesse Wierzbinski c58c8c6cc8
fix(api): 🐛 Correctly start push worker 2025-01-02 03:12:59 +01:00
Jesse Wierzbinski 7b3158c102
fix(api): 🚑 Fix incorrect builds
Everything under api/ should be a route, or it messes up bundling
2025-01-02 02:55:56 +01:00
Jesse Wierzbinski d839c274b1
feat(api): Finish push notification delivery 2025-01-02 02:45:40 +01:00
Jesse Wierzbinski d096ab830c
feat(api): Add initial Push Notifications support 2025-01-02 01:29:33 +01:00
Jesse Wierzbinski acd2bcb469
feat: 🧑‍💻 Add VSCode launch json 2025-01-01 23:42:14 +01:00
Jesse Wierzbinski 1137782f2a
ci: 🐛 Remove unnecessary permissions from tests 2024-12-31 17:38:13 +01:00
Jesse Wierzbinski 9d88fdbe53
ci: ♻️ Make tests, linting and checks pass before build is run 2024-12-31 17:35:27 +01:00
Jesse Wierzbinski deada6cbd9
ci: ♻️ Merge Worker & Server build into one CI file 2024-12-31 17:21:49 +01:00
Jesse Wierzbinski fbd352e23c
fix: 🚑 Correctly apply patches to bull-board 2024-12-31 17:16:16 +01:00
Jesse Wierzbinski 82da70bcac
refactor(api): ♻️ Group note/account fetching code in some routes 2024-12-30 21:30:10 +01:00
Jesse Wierzbinski 16f302c2dc
refactor(api): ♻️ Simplify route schema definitions 2024-12-30 20:26:56 +01:00
Jesse Wierzbinski 4926d6ff5d
refactor(api): 🔥 Remove all useless route metadata objects 2024-12-30 20:18:48 +01:00
Jesse Wierzbinski a9ea5eb672
chore: ⬆️ Upgrade TypeScript peer dependency to latest 2024-12-30 19:41:36 +01:00
Jesse Wierzbinski 09f30db83a
refactor(api): ♻️ Remove useless authorization checks 2024-12-30 19:38:41 +01:00
Jesse Wierzbinski dc12b269f5
refactor(api): ♻️ Improve authentication checker API 2024-12-30 19:18:31 +01:00
Jesse Wierzbinski 621dd7e9d9
refactor(api): ♻️ Upgrade zod-openapi to 0.18.3
Needed to add "as const" to all middleware handlers :)
2024-12-30 18:20:22 +01:00
Jesse Wierzbinski fbfd237f27
refactor(api): ♻️ Throw ApiError instead of returning error JSON 2024-12-30 18:00:23 +01:00
Jesse Wierzbinski c14621ee06
refactor(api): 🔥 Simplify oauth authorize handler 2024-12-30 16:47:48 +01:00
Jesse Wierzbinski 44d7264b79
refactor: 🔥 Remove deprecated ioredis type package 2024-12-30 16:20:47 +01:00
Jesse Wierzbinski a7b29d563e
fix(api): 🏷️ Use context.body for 204 responses 2024-12-30 16:18:28 +01:00
Jesse Wierzbinski 6af6bde12a
chore: ⬆️ Upgrade dependencies 2024-12-30 16:07:10 +01:00
Jesse Wierzbinski 8d2451cafc
chore: 🔖 Bump version to 0.8.0-alpha 2024-12-30 16:05:45 +01:00
Jesse Wierzbinski 20970a76fd
chore: ⬆️ Upgrade Bun to 1.1.42 2024-12-22 11:57:32 +01:00
Jesse Wierzbinski c621d9251e
chore: ⬆️ Upgrade dependencies 2024-12-22 11:56:52 +01:00
Jesse Wierzbinski 7268bd74f7
fix(api): ✏️ Remove extra attribute on NoteReaction
Was left there by mistake
2024-12-19 15:45:06 +01:00
Jesse Wierzbinski 98d63d85d4
docs(api): 📝 Document Reactions API 2024-12-19 15:41:56 +01:00
Jesse Wierzbinski 6f97f9f8f1
refactor(database): ♻️ Clean up database schema 2024-12-18 21:52:53 +01:00
Jesse Wierzbinski c334cd9cc8
chore: ⬆️ Upgrade Bun to 1.1.40 2024-12-18 20:46:26 +01:00
Jesse Wierzbinski 1509786090
chore: ⬆️ Upgrade dependencies 2024-12-18 20:42:40 +01:00
Jesse Wierzbinski f67fed12e0
feat(database): Add Reaction database class 2024-12-18 20:01:26 +01:00
Jesse Wierzbinski e00182cf54
feat(database): Add reactions table to database schema 2024-12-18 19:25:45 +01:00
Jesse Wierzbinski 4fdb96930f
fix(api): 🐛 Fetch media content-type from data, instead of doing naive guesses 2024-12-16 23:57:21 +01:00
Jesse Wierzbinski 6f67881d96
feat(api): Add ability to set URL as avatar or banner 2024-12-16 15:46:11 +01:00
Jesse Wierzbinski 41341cf252
docs: 📝 Improve README 2024-12-16 14:47:51 +01:00
Gaspard Wierzbinski 43b87dbfd3
docs: 📖 Add license for Versia assets 2024-12-15 15:09:01 +01:00
Jesse Wierzbinski e293bd280d
fix(api): 🐛 Fix duplicated mentions, general refactorings 2024-12-09 15:30:18 +01:00
Jesse Wierzbinski 84a0a07ea6
ci: 👷 Add the short commit SHA as a docker container tag 2024-12-09 15:08:32 +01:00
Jesse Wierzbinski 0ae9cfe26c
refactor(federation): ♻️ More federation logic cleanup 2024-12-09 15:01:19 +01:00
Jesse Wierzbinski 83399ba5f1
refactor(database): ♻️ Simplify User and Note logic further 2024-12-09 13:50:46 +01:00
Jesse Wierzbinski a8541bdc44
refactor(database): ♻️ Simplify Note and User federation logic 2024-12-09 13:36:15 +01:00
Jesse Wierzbinski cbbf49905b
fix(federation): 🐛 Fix remote emojis being incorrectly marked as local 2024-12-09 13:11:23 +01:00
Jesse Wierzbinski c94dd7c59d
fix(api): 🐛 Don't transform raw URLs as if they were attachments 2024-12-09 12:43:53 +01:00
Jesse Wierzbinski 8796f694bc
feat(api): 🚩 Add emoji shortcode size controls 2024-12-09 11:09:40 +01:00
Jesse Wierzbinski cfefd56a55
feat(api): 👽 Expose emoji limits in /api/v2/instance 2024-12-09 11:02:15 +01:00
Jesse Wierzbinski c8b909db08
chore: ⬆️ Upgrade Bun to 1.1.38 2024-12-09 10:57:24 +01:00
Jesse Wierzbinski 0708b3c45d
chore: ⬆️ Upgrade dependencies 2024-12-09 10:56:56 +01:00
Jesse Wierzbinski b14fa17e1a
feat(config): 🚩 Add emoji size/description size controls 2024-12-09 10:55:04 +01:00
Jesse Wierzbinski 5074ac788f
fix(api): 🐛 Force text content-type header on all empty responses
Fixes a problem where the content-type would default to application/json
2024-12-07 13:24:24 +01:00
Jesse Wierzbinski 06376cf58a
fix(api): 🐛 Correctly return empty body without content-type headers, when returning empty responses 2024-12-07 12:20:06 +01:00
Jesse Wierzbinski 2743528727
fix(api): 🐛 Fix emoji editing always making the emoji non-global 2024-12-07 11:12:17 +01:00
Jesse Wierzbinski 57e17e7607
fix(cli): 🏷️ Handle possible undefined values on some variables 2024-12-02 15:43:56 +01:00
Jesse Wierzbinski e4768620e2
fix(api): 🐛 Fix account lookup address parsing (again) 2024-12-02 15:40:20 +01:00
Jesse Wierzbinski 91da99c934
feat(federation): Handle instances not existing 2024-12-02 15:07:05 +01:00
Jesse Wierzbinski deee65ad6d
refactor(api): ♻️ Refactor user lookup endpoint 2024-12-02 15:00:37 +01:00
Jesse Wierzbinski ca42df1dfd
fix(api): 🐛 Properly include global field in API emojis 2024-11-29 21:49:41 +01:00
Jesse Wierzbinski 46933c1bef
fix(api): Fix tests expecting null instead of undefined in Roles API 2024-11-28 11:24:56 +01:00
Jesse Wierzbinski d1d7ca25a4
chore(api): ⬆️ Upgrade @versia/client to 0.1.1 2024-11-28 11:21:11 +01:00
Jesse Wierzbinski caa071d353
docs(api): 🐛 Fix incorrect Role API docs 2024-11-28 11:04:22 +01:00
Jesse Wierzbinski 594e8ca4e6
docs(api): 🐛 Add proper parameters to Roles API docs 2024-11-28 10:59:14 +01:00
Jesse Wierzbinski eb405d33cd
fix(api): 🐛 Don't use null in Role properties 2024-11-28 10:54:44 +01:00
Jesse Wierzbinski 8f339669b5
chore: ⬆️ Upgrade Bun to 1.1.37 2024-11-28 10:27:17 +01:00
Jesse Wierzbinski cd4b021aec
chore: ⬆️ Upgrade dependencies 2024-11-28 10:26:28 +01:00
Jesse Wierzbinski 4e38749ccb
docs(api): 📝 Update Roles API docs and changelog 2024-11-26 15:41:08 +01:00
Jesse Wierzbinski 49c53de99e
feat(api): Overhaul Role API, add ability to edit roles and assign/unassign them from any user 2024-11-26 15:27:39 +01:00
Jesse Wierzbinski 7431c1e21d
fix: 🏗️ Update file to patch's location in builder 2024-11-25 23:25:37 +01:00
Jesse Wierzbinski 49a301663a
feat(federation): Use instance messaging to send errors to remote instance 2024-11-25 23:14:42 +01:00
Jesse Wierzbinski a037448ebb
refactor(federation): ♻️ Remove Response return semantics from inbox worker 2024-11-25 23:11:17 +01:00
Jesse Wierzbinski 025d5bea94
docs: 📝 Restrict reference docker-compose permissions 2024-11-25 22:21:00 +01:00
Jesse Wierzbinski ece36f6adc
fix: 💚 Also include repo owner in worker image name 2024-11-25 21:58:41 +01:00
Jesse Wierzbinski 87bb0b6bcb
fix: 💚 Make worker CI build use correct image name 2024-11-25 21:56:57 +01:00
Jesse Wierzbinski 1b98381242
feat: Split off queue workers into a separate worker process 2024-11-25 21:54:31 +01:00
Jesse Wierzbinski 0b3e74107e
refactor(federation): ♻️ Make user inbox use the delayed processing 2024-11-25 21:17:52 +01:00
Jesse Wierzbinski a6574249df
docs: 📝 Update Changelog 2024-11-25 21:00:18 +01:00
Jesse Wierzbinski 55256e3568
refactor(config): 🚩 Remove enable flag on Redis queue config 2024-11-25 20:55:55 +01:00
Jesse Wierzbinski fb9a0feac8
fix: 💚 Add Redis to GitHub tester 2024-11-25 20:54:06 +01:00
Jesse Wierzbinski c899f12893
feat(federation): Prioritize delivery to instance inbox, and use delivery queue in more places 2024-11-25 20:50:55 +01:00
Jesse Wierzbinski 7a73a1a24e
feat(federation): Store remote instance shared inbox and extensions as well 2024-11-25 20:37:00 +01:00
Jesse Wierzbinski 5fc6c4dcfa
feat(federation): Implement queue for fetches 2024-11-25 20:29:59 +01:00
Jesse Wierzbinski 79cf43d752
refactor(api): 🛂 Increase JWT cookie lifetime to 2 weeks 2024-11-25 20:25:42 +01:00
Jesse Wierzbinski eb466a0cc7
feat(federation): Implement Shared Inboxes 2024-11-25 17:05:53 +01:00
Jesse Wierzbinski 756f67c0f3
feat(federation): Implement Instance Messaging Extension 2024-11-25 16:54:46 +01:00
Jesse Wierzbinski 4594c69808
docs: 📝 Update changelog 2024-11-25 14:27:34 +01:00
Jesse Wierzbinski 61b773ed11
feat(federation): Add config option to control automatic queue purge time 2024-11-25 13:53:14 +01:00
Jesse Wierzbinski 048dd6b0ab
fix: 🐛 Use CommonJS resolve instead of ESM one in Bull-Board UI 2024-11-25 13:37:58 +01:00
Jesse Wierzbinski fb84db3ea7
fix: 🐛 Correctly include bull-board UI package in dist 2024-11-25 13:24:14 +01:00
Jesse Wierzbinski ecc7d1eee7
feat(federation): Add UI to view BullMQ metadata 2024-11-25 13:09:28 +01:00
Jesse Wierzbinski 8a920218ea
feat(federation): Add queue to note delivery 2024-11-25 11:29:48 +01:00
Jesse Wierzbinski 3ef361f521
fix(federation): 🐛 Correctly remove listeners after job processing finishes 2024-11-25 08:59:48 +01:00
Jesse Wierzbinski 3e19b11609
refactor: 🔊 Fix duplicate logs 2024-11-24 23:13:29 +01:00
Jesse Wierzbinski 005a3a2721
fix(federation): 🚑 Don't always try to use instance key when an instance is not the request signer 2024-11-24 23:01:47 +01:00
Jesse Wierzbinski 34370a082a
refactor(federation): ♻️ Make Instance updateFromRemote non-static 2024-11-24 22:48:34 +01:00
Jesse Wierzbinski 8b23eb888d
refactor(cli): ♻️ Rewrite instance fetch command to refetch instances instead 2024-11-24 22:45:41 +01:00
Jesse Wierzbinski 50ebc12783
fix(federation): 🐛 Show error for inbox failures 2024-11-24 22:33:51 +01:00
Jesse Wierzbinski d527947182
fix(federation): 🐛 Correctly handle job failures in inboxes 2024-11-24 22:28:29 +01:00
Jesse Wierzbinski c59ebef851
feat(federation): Add more debugging to inbox processing 2024-11-24 22:17:45 +01:00
Jesse Wierzbinski be69407c01
refactor(federation): 🔊 Logging color tweaks 2024-11-24 22:10:23 +01:00
Jesse Wierzbinski 40e7903d90
feat(federation): 🔊 Add processing finished log 2024-11-24 22:06:32 +01:00
Jesse Wierzbinski b333ecc816
refactor(federation): 🔊 Add more logging to some federation logic 2024-11-24 22:01:14 +01:00
Jesse Wierzbinski ef0cca671a
feat(federation): Add Redis online check 2024-11-24 22:01:01 +01:00
Jesse Wierzbinski b320ddf3ae
refactor(federation): ♻️ Queue all incoming inbox processing events 2024-11-24 21:35:59 +01:00
Jesse Wierzbinski 26f1407efe
chore: 📝 Update changelog 2024-11-24 20:20:56 +01:00
Jesse Wierzbinski 8d968fa98c
chore: ⬆️ Upgrade dependencies 2024-11-24 17:54:42 +01:00
Jesse Wierzbinski 340ed7b258
fix(federation): 🐛 Correctly handle non-lowercase acct queries in account lookups 2024-11-24 17:42:30 +01:00
Jesse Wierzbinski 259fba17a7
fix(federation): 🐛 Make an empty allowed_ip list for bridge correctly allow any IP 2024-11-24 16:54:24 +01:00
Jesse Wierzbinski b55237cdc8
refactor(federation): ♻️ Allow ActivityPub bridge requests to omit all signature headers, including x-signed-by 2024-11-24 16:40:23 +01:00
Jesse Wierzbinski 80b5184d6a
test(api): 🐛 Fix S3 tests 2024-11-24 16:32:05 +01:00
Jesse Wierzbinski 59b069ce2c
fix(api): 🐛 Specify content-type when uploading to S3 2024-11-24 13:16:52 +01:00
Jesse Wierzbinski 6301121900
refactor(cli): 🔥 Removed unused variables 2024-11-24 00:48:41 +01:00
Jesse Wierzbinski 083b77bbb9
feat(cli): Add automatic setup script 2024-11-24 00:46:40 +01:00
Jesse Wierzbinski 36b25e0307
feat(cli): Add direct password option to CLI 2024-11-24 00:37:26 +01:00
Jesse Wierzbinski da369e604c
fix(api): 🐛 Fix lookup endpoints thinking local user is remote 2024-11-23 23:09:59 +01:00
Jesse Wierzbinski ace6921447
refactor(federation): ♻️ Correctly handle bridge requests and instance signatures in user inboxes 2024-11-23 23:02:18 +01:00
Jesse Wierzbinski afc5a74a40
fix: 🚑 Correctly use Musl version of Sharp in the production build 2024-11-23 14:35:04 +01:00
Jesse Wierzbinski 5b6924810e
chore: ⬆️ Upgrade Bun to 1.1.36 2024-11-23 14:27:26 +01:00
Jesse Wierzbinski fb9dbcdff0
chore: ⬆️ Upgrade dependencies 2024-11-23 14:26:58 +01:00
Jesse Wierzbinski 8444ff5741
fix(federation): 🐛 Use correct URL for bridge queries 2024-11-22 22:17:25 +01:00
Jesse Wierzbinski 217d3c286d
feat(api): Allow divs and spans in HTML 2024-11-22 19:12:52 +01:00
Jesse Wierzbinski fa0d48b88d
fix(api): 🐛 Use consistent user address matching patterns 2024-11-22 16:53:36 +01:00
Jesse Wierzbinski 569ba8bf2d
fix(api): 🔒 Correctly put all URIs in profiles through proxy 2024-11-22 15:06:46 +01:00
Jesse Wierzbinski bd1f09837b
fix(api): 🐛 Correctly handle underscores when parsing usernames 2024-11-22 14:51:11 +01:00
Jesse Wierzbinski bbfd26bb64
docs: 📝 Point to v0.7.0 installation info in docs instead of main branch 2024-11-21 20:34:21 +01:00
734 changed files with 86266 additions and 36276 deletions

View file

@ -1,16 +1,18 @@
version = 1
test_patterns = ["**/*.test.ts"]
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
environment = ["nodejs"]
[analyzers.meta]
environment = ["nodejs"]
[[analyzers]]
name = "docker"
[analyzers.meta]
dockerfile_paths = ["Dockerfile"]
[analyzers.meta]
dockerfile_paths = ["Dockerfile"]

View file

@ -1,9 +0,0 @@
# Bun doesn't run well on Musl but this seems to work
FROM oven/bun:1.1.34-alpine as base
# Switch to Bash by editing /etc/passwd
RUN apk add --no-cache libstdc++ git bash curl openssh cloc && \
sed -i -e 's|/bin/ash|/bin/bash|g' /etc/passwd
# Extract Node from its docker image (node:22-alpine)
COPY --from=node:22-alpine /usr/local/bin/node /usr/local/bin/node

View file

@ -1,34 +0,0 @@
{
"name": "versia Dev Container",
"dockerFile": "Dockerfile",
"runArgs": [
"-v",
"${localWorkspaceFolder}/config:/workspace/config",
"-v",
"${localWorkspaceFolder}/logs:/workspace/logs",
"-v",
"${localWorkspaceFolder}/uploads:/workspace/uploads",
"--network=host"
],
"mounts": [
"source=node_modules,target=/workspace/node_modules,type=bind,consistency=cached",
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/root/.ssh,readonly"
],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"biomejs.biome",
"ms-vscode-remote.remote-containers",
"oven.bun-vscode",
"vivaxy.vscode-conventional-commits",
"EditorConfig.EditorConfig",
"tamasfe.even-better-toml",
"YoavBls.pretty-ts-errors",
"eamodio.gitlens"
]
}
}
}

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
tab_width = 4
trim_trailing_whitespace = true

View file

@ -1,100 +1,171 @@
[database]
# You can change the URL to the commit/tag you are using
#:schema https://raw.githubusercontent.com/versia-pub/server/main/config/config.schema.json
# All values marked as "sensitive" can be set to "PATH:/path/to/file" to read the value from a file (e.g. a secret manager)
[postgres]
# PostgreSQL database configuration
host = "localhost"
port = 5432
username = "versia"
# Sensitive value
password = "versia"
database = "versia"
# Additional read-only replicas
# [[postgres.replicas]]
# host = "other-host"
# port = 5432
# username = "versia"
# password = "mycoolpassword2"
# database = "replica1"
[redis.queue]
# A Redis database used for managing queues.
# Required for federation
host = "localhost"
port = 6379
password = ""
# Sensitive value
# password = "test"
database = 0
# A Redis database used for caching SQL queries.
# Optional, can be the same as the queue instance
# [redis.cache]
# host = "localhost"
# port = 6380
# database = 1
# password = ""
# Search and indexing configuration
[search]
# Enable indexing and searching?
enabled = false
[redis.cache]
host = "localhost"
port = 6379
password = ""
database = 1
enabled = false
[sonic]
# Optional if search is disabled
[search.sonic]
host = "localhost"
port = 40007
# Sensitive value
password = ""
enabled = false
[signups]
# Whether to enable registrations or not
registration = true
rules = [
"Do not harass others",
"Be nice to people",
"Don't spam",
"Don't post illegal content",
]
[registration]
# Can users sign up freely?
allow = true
# NOT IMPLEMENTED
require_approval = false
# Message to show to users when registration is disabled
# message = "ran out of spoons to moderate registrations, sorry"
[http]
# URL that the instance will be accessible at
base_url = "http://0.0.0.0:8080"
# Address to bind to (0.0.0.0 is suggested for proxies)
bind = "0.0.0.0"
bind_port = 8080
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
banned_ips = []
# Banned user agents, regex format
banned_user_agents = [
# "curl\/7.68.0",
# "wget\/1.20.3",
]
[smtp]
# URL to an eventual HTTP proxy
# Will be used for all outgoing requests
# proxy_address = "http://localhost:8118"
# TLS configuration. You should probably be using a reverse proxy instead of this
# [http.tls]
# key = "/path/to/key.pem"
# cert = "/path/to/cert.pem"
# Sensitive value
# passphrase = "awawa"
# ca = "/path/to/ca.pem"
[frontend]
# Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API)
# Frontends also control the OpenID flow, so if you disable this, you will need to use the Mastodon frontend
enabled = true
# Path that frontend files are served from
# Edit this property to serve custom frontends
# If this is not set, Versia Server will also check
# the VERSIA_FRONTEND_PATH environment variable
# path = ""
[frontend.routes]
# Special routes for your frontend, below are the defaults for Versia-FE
# Can be set to a route already used by Versia Server, as long as it is on a different HTTP method
# e.g. /oauth/authorize is a POST-only route, so you can serve a GET route at /oauth/authorize
# home = "/"
# login = "/oauth/authorize"
# consent = "/oauth/consent"
# register = "/register"
# password_reset = "/oauth/reset"
[frontend.settings]
# Arbitrary key/value pairs to be passed to the frontend
# This can be used to set up custom themes, etc on supported frontends.
# theme = "dark"
# NOT IMPLEMENTED
[email]
# Enable email sending
send_emails = false
# If send_emails is true, the following settings are required
# [email.smtp]
# SMTP server to use for sending emails
server = "smtp.example.com"
port = 465
username = "test@example.com"
password = "password123"
tls = true
# server = "smtp.example.com"
# port = 465
# username = "test@example.com"
# Sensitive value
# password = "password123"
# tls = true
[media]
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
# If you need to change this value after setting up your instance, you must move all the files
# from one backend to the other manually
# Changing this value will not retroactively apply to existing data
# Don't forget to fill in the s3 config :3
backend = "local"
# Whether to check the hash of media when uploading to avoid duplication
deduplicate_media = true
# If media backend is "local", this is the folder where the files will be stored
local_uploads_folder = "uploads"
# Can be any path
uploads_path = "uploads"
[media.conversion]
# Whether to automatically convert images to another format on upload
convert_images = false
# Can be: "jxl", "webp", "avif", "png", "jpg", "heif"
# Can be: "image/jxl", "image/webp", "image/avif", "image/png", "image/jpeg", "image/heif", "image/gif"
# JXL support will likely not work
convert_to = "webp"
convert_to = "image/webp"
# Also convert SVG images?
convert_vectors = false
# [s3]
# Can be left blank if you don't use the S3 media backend
# endpoint = "https://s3-us-west-2.amazonaws.com"
# access_key = ""
# secret_access_key = ""
# region = "us-west-2"
# Can be left commented if you don't use the S3 media backend
# endpoint = "https://s3.example.com"
# Sensitive value
# access_key = "XXXXX"
# Sensitive value
# secret_access_key = "XXX"
# region = "us-east-1"
# bucket_name = "versia"
# public_url = "https://cdn.example.com"
[validation]
# Self explanatory
max_displayname_size = 50
max_bio_size = 160
max_note_size = 5000
max_avatar_size = 5_000_000
max_header_size = 5_000_000
max_media_size = 40_000_000
max_media_attachments = 10
max_media_description_size = 1000
max_poll_options = 20
max_poll_option_size = 500
min_poll_duration = 60
max_poll_duration = 1893456000
max_username_size = 30
# An array of strings, defaults are from Akkoma
username_blacklist = [
".well-known",
"~",
# Checks user data
# Does not retroactively apply to previously entered data
[validation.accounts]
max_displayname_characters = 50
max_username_characters = 30
max_bio_characters = 5000
max_avatar_bytes = 5_000_000
max_header_bytes = 5_000_000
# Regex is allowed here
disallowed_usernames = [
"well-known",
"about",
"activities",
"api",
@ -120,12 +191,14 @@ username_blacklist = [
"search",
"mfa",
]
# Whether to blacklist known temporary email providers
blacklist_tempmail = false
# Additional email providers to blacklist
email_blacklist = []
# Valid URL schemes, otherwise the URL is parsed as text
url_scheme_whitelist = [
max_field_count = 10
max_field_name_characters = 1000
max_field_value_characters = 1000
max_pinned_notes = 20
[validation.notes]
max_characters = 5000
allowed_url_schemes = [
"http",
"https",
"ftp",
@ -143,62 +216,122 @@ url_scheme_whitelist = [
"mumble",
"ssb",
"gemini",
] # NOT IMPLEMENTED
enforce_mime_types = false
allowed_mime_types = [
"image/jpeg",
"image/png",
"image/gif",
"image/heic",
"image/heif",
"image/webp",
"image/avif",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/vnd.wave",
"audio/ogg",
"audio/vorbis",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf",
]
max_attachments = 16
[validation.media]
max_bytes = 40_000_000
max_description_characters = 1000
# An empty array allows all MIME types
allowed_mime_types = []
[validation.emojis]
max_bytes = 1_000_000
max_shortcode_characters = 100
max_description_characters = 1000
[validation.polls]
max_options = 20
max_option_characters = 500
min_duration_seconds = 60
# 100 days
max_duration_seconds = 8_640_000
[validation.emails]
# Blocks over 10,000 common tempmail domains
disallow_tempmail = false
# Regex is allowed here
disallowed_domains = []
[validation.challenges]
# "Challenges" (aka captchas) are a way to verify that a user is human
# Versia Server's challenges use no external services, and are Proof of Work based
# Versia Server's challenges use no external services, and are proof-of-work based
# This means that they do not require any user interaction, instead
# they require the user's computer to do a small amount of work
enabled = true
# The difficulty of the challenge, higher is harder
# The difficulty of the challenge, higher is will take more time to solve
difficulty = 50000
# Challenge expiration time in seconds
expiration = 300 # 5 minutes
# Leave this empty to generate a new key
# Sensitive value
key = "YBpAV0KZOeM/MZ4kOb2E9moH9gCUr00Co9V7ncGRJ3wbd/a9tLDKKFdI0BtOcnlpfx0ZBh0+w3WSvsl0TsesTg=="
# Block content that matches these regular expressions
[validation.filters]
note_content = [
# "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+",
# "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+",
]
emoji_shortcode = []
username = []
displayname = []
bio = []
[notifications]
# Web Push Notifications configuration.
# Leave out to disable.
[notifications.push]
# Subject field embedded in the push notification
# subject = "mailto:joe@example.com"
#
[notifications.push.vapid_keys]
# VAPID keys for push notifications
# Run Versia Server with those values missing to generate new keys
# Sensitive value
public = "BBanhyj2_xWwbTsWld3T49VcAoKZHrVJTzF1f6Av2JwQY_wUi3CF9vZ0WeEcACRj6EEqQ7N35CkUh5epF7n4P_s"
# Sensitive value
private = "Eujaz7NsF0rKZOVrAFL7mMpFdl96f591ERsRn81unq0"
[defaults]
# Default visibility for new notes
# Can be public, unlisted, private or direct
# Private only sends to followers, unlisted doesn't show up in timelines
visibility = "public"
# Default language for new notes
# Default language for new notes (ISO code)
language = "en"
# Default avatar, must be a valid URL or ""
# Default avatar, must be a valid URL or left out for a placeholder avatar
# avatar = ""
# Default header, must be a valid URL or ""
# Default header, must be a valid URL or left out for none
# header = ""
# A style name from https://www.dicebear.com/styles
placeholder_style = "thumbs"
[queues]
# Controls the delivery queue (for outbound federation)
[queues.delivery]
# Time in seconds to remove completed jobs
remove_after_complete_seconds = 31536000
# Time in seconds to remove failed jobs
remove_after_failure_seconds = 31536000
# Controls the inbox processing queue (for inbound federation)
[queues.inbox]
# Time in seconds to remove completed jobs
remove_after_complete_seconds = 31536000
# Time in seconds to remove failed jobs
remove_after_failure_seconds = 31536000
# Controls the fetch queue (for remote data refreshes)
[queues.fetch]
# Time in seconds to remove completed jobs
remove_after_complete_seconds = 31536000
# Time in seconds to remove failed jobs
remove_after_failure_seconds = 31536000
# Controls the push queue (for push notification delivery)
[queues.push]
# Time in seconds to remove completed jobs
remove_after_complete_seconds = 31536000
# Time in seconds to remove failed jobs
remove_after_failure_seconds = 31536000
# Controls the media queue (for media processing)
[queues.media]
# Time in seconds to remove completed jobs
remove_after_complete_seconds = 31536000
# Time in seconds to remove failed jobs
remove_after_failure_seconds = 31536000
[federation]
# This is a list of domain names, such as "mastodon.social" or "pleroma.site"
@ -223,57 +356,119 @@ reactions = []
banners = []
avatars = []
# For bridge software, such as versia-pub/activitypub
# Bridges must be hosted separately from the main Versia Server process
# [federation.bridge]
# Only versia-ap exists for now
# software = "versia-ap"
# If this is empty, any bridge with the correct token
# will be able to send data to your instance
# v4, v6, ranges and wildcards are supported
# allowed_ips = ["192.168.1.0/24"]
# Token for the bridge software
# Bridge must have the same token!
# Sensitive value
# token = "mycooltoken"
# url = "https://ap.versia.social"
[instance]
name = "Versia"
description = "A test instance of Versia Server"
# URL to your instance logo (jpg files should be renamed to jpeg)
# logo = ""
# URL to your instance banner (jpg files should be renamed to jpeg)
# banner = ""
description = "A Versia Server instance"
# Paths to instance long description, terms of service, and privacy policy
# These will be parsed as Markdown
#
# extended_description_path = "config/extended_description.md"
# tos_path = "config/tos.md"
# privacy_policy_path = "config/privacy_policy.md"
[filters]
# Regex filters for federated and local data
# Drops data matching the filters
# Does not apply retroactively to existing data
# Primary instance languages. ISO 639-1 codes.
languages = ["en"]
# Note contents
note_content = [
# "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+",
# "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+",
]
emoji = []
# These will drop users matching the filters
username = []
displayname = []
bio = []
[instance.contact]
email = "staff@yourinstance.com"
[instance.branding]
# logo = "https://cdn.example.com/logo.png"
# banner = "https://cdn.example.com/banner.png"
# Used for federation. If left empty or missing, the server will generate one for you.
[instance.keys]
# Sensitive value
public = "MCowBQYDK2VwAyEASN0V5OWRbhRCnuhxfRLqpUOfszHozvrLLVhlIYLNTZM="
# Sensitive value
private = "MC4CAQAwBQYDK2VwBCIEIKaxDGMaW71OcCGMY+GKTZPtLPNlTvMFe3G5qXVHPhQM"
[[instance.rules]]
# Short description of the rule
text = "No hate speech"
# Longer version of the rule with additional information
hint = "Hate speech includes slurs, threats, and harassment."
[[instance.rules]]
text = "No spam"
# [[instance.rules]]
# ...etc
[permissions]
# Control default permissions for users
# Note that an anonymous user having a permission will not allow them
# to do things that require authentication (e.g. 'owner:notes' -> posting a note will need
# auth, but viewing a note will not)
# See https://server.versia.pub/api/roles#list-of-permissions for a list of all permissions
# Defaults to being able to login and manage their own content
# anonymous = []
# Defaults to identical to anonymous
# default = []
# Defaults to being able to manage all instance data, content, and users
# admin = []
[logging]
# Log all requests (warning: this is a lot of data)
log_requests = true
# Log request and their contents (warning: this is a lot of data)
log_requests_verbose = false
# For GDPR compliance, you can disable logging of IPs
log_ip = false
# Log all filtered objects
log_filters = true
# Available levels: trace, debug, info, warning, error, fatal
log_level = "info" # For console output
[ratelimits]
# These settings apply to every route at once
# Amount to multiply every route's duration by
duration_coeff = 1.0
# Amount to multiply every route's max requests per [duration] by
max_coeff = 1.0
# [logging.file]
# path = "logs/versia.log"
# log_level = "info"
#
# [logging.file.rotation]
# max_size = 10_000_000 # 10 MB
# max_files = 10 # Keep 10 rotated files
#
# https://sentry.io support
# [logging.sentry]
# dsn = "https://example.com"
# debug = false
# sample_rate = 1.0
# traces_sample_rate = 1.0
# Can also be regex
# trace_propagation_targets = []
# max_breadcrumbs = 100
# environment = "production"
# log_level = "info"
[ratelimits.custom]
# Add in any API route in this style here
# Applies before the global ratelimit changes
# "/api/v1/accounts/:id/block" = { duration = 30, max = 60 }
# "/api/v1/timelines/public" = { duration = 60, max = 200 }
[authentication]
# Run Versia Server with this value missing to generate a new key
key = "ZWcwanRaQAqY3ChUro/Jey9XGQjzsxEed5iqTp4yFr8W6vEnXdz91F/Pu/uf7HBMbNeIK7V6aHsM0lq9onrO8Q=="
[plugins]
[plugins.config."@versia/openid".keys]
private = "MC4CAQAwBQYDK2VwBCIEID+H5n9PY3zVKZQcq4jrnE1IiRd2EWWr8ApuHUXmuOzl"
public = "MCowBQYDK2VwAyEAzenliNkgpXYsh3gXTnAoUWzlCPjIOppmAVx2DBlLsC8="
# The provider MUST support OpenID Connect with .well-known discovery
# Most notably, GitHub does not support this
# Redirect URLs in your OpenID provider can be set to this:
# <base_url>/oauth/sso/<provider_id>/callback*
# The asterisk is important, as it allows for any query parameters to be passed
# Authentik for example uses regex so it can be set to (regex):
# <base_url>/oauth/sso/<provider_id>/callback.*
# [[authentication.openid_providers]]
# name = "CPlusPatch ID"
# id = "cpluspatch-id"
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)
# url = "https://id.cpluspatch.com/application/o/versia-testing/"
# client_id = "XXXX"
# Sensitive value
# client_secret = "XXXXX"
# icon = "https://cpluspatch.com/images/icons/logo.svg"

22
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,22 @@
We use full TypeScript and ESM with Bun for our codebase. Please include relevant and detailed JSDoc comments for all functions and classes. Use explicit type annotations for all variables and function return values, such as:
```typescript
/**
* Adds two numbers together.
*
* @param {number} a
* @param {number} b
* @returns {number}
*/
const add = (a: number, b: number): number => a + b;
```
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
Our codebase uses Drizzle as an ORM, which is exposed in the `@versia-server/kit/db` and `@versia-server/kit/tables` packages. This project uses a monorepo structure with Bun as the package manager.
The app has two modes: worker and API. The worker mode is used for background tasks, while the API mode serves HTTP requests. The entry point for the worker is `worker.ts`, and for the API, it is `api.ts`.
Run the typechecker with `bun run typecheck` to ensure that all TypeScript code is type-checked correctly. Run tests with `bun test` to ensure that all tests pass. Run the linter and formatter with `bun lint` to ensure that the code adheres to our style guidelines, and `bun lint --write` to automatically fix minor/formatting issues.
Cover all new functionality with tests, and ensure that all tests pass before submitting your code.

View file

@ -1,31 +1,27 @@
name: Check Types
name: Check Types
on:
push:
branches: ["*"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
on:
workflow_call:
jobs:
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Install NPM packages
run: |
bun install
- name: Run typechecks
run: |
bun run check
- name: Run typechecks
run: |
bun run typecheck

27
.github/workflows/circular-imports.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Check Circular Imports
on:
workflow_call:
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Run typechecks
run: |
bun run detect-circular

View file

@ -1,74 +0,0 @@
name: Docker Build
on:
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # v3.0.0
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Get the commit hash
run: echo "GIT_COMMIT=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT=${{ env.GIT_COMMIT }}
provenance: mode=max
sbom: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max

98
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,98 @@
name: Build Docker Images
on:
push:
branches: ["*"]
# Publish semver tags as releases.
tags: ["v*.*.*"]
pull_request:
branches: ["main"]
jobs:
lint:
uses: ./.github/workflows/lint.yml
check:
uses: ./.github/workflows/check.yml
tests:
uses: ./.github/workflows/tests.yml
detect-circular:
uses: ./.github/workflows/circular-imports.yml
build:
if: ${{ success() }}
needs: [lint, check, tests, detect-circular]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
strategy:
matrix:
include:
- container: worker
image_name: ${{ github.repository_owner }}/worker
dockerfile: Worker.Dockerfile
- container: server
image_name: ${{ github.repository_owner }}/server
dockerfile: Dockerfile
env:
REGISTRY: ghcr.io
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ matrix.image_name }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=sha
- name: Get the commit hash
run: echo "GIT_COMMIT=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT=${{ env.GIT_COMMIT }}
file: ${{ matrix.dockerfile }}
provenance: mode=max
sbom: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,56 +1,56 @@
name: Deploy Docs to GitHub Pages
on:
push:
branches: [main]
push:
branches: [main]
workflow_dispatch:
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- name: Setup Pages
- uses: oven-sh/setup-bun@v2
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: bun install
uses: actions/configure-pages@v4
- name: Install dependencies
run: bun install
- name: Build with VitePress
run: bun run docs:build
- name: Build with VitePress
run: bun run --filter="@versia-server/api" docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: packages/api/docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -1,31 +1,27 @@
name: Lint & Format
on:
push:
branches: ["*"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
workflow_call:
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Install NPM packages
run: |
bun install
- name: Run linting
run: |
bunx @biomejs/biome ci .
- name: Run linting
run: |
bunx @biomejs/biome ci .

View file

@ -2,7 +2,7 @@ name: Mirror to Codeberg
on: [push]
jobs:
mirror:
name: Mirror
uses: versia-pub/.github/.github/workflows/mirror.yml@main
secrets: inherit
mirror:
name: Mirror
uses: versia-pub/.github/.github/workflows/mirror.yml@main
secrets: inherit

View file

@ -1,25 +1,25 @@
name: Nix Build
name: Nix Build
on:
pull_request:
push:
branches: ["*"]
workflow_dispatch:
pull_request:
push:
branches: ["*"]
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
permissions:
id-token: "write"
contents: "read"
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: accept-flake-config = true
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Build default package
run: nix build .
- name: Check flakes
run: nix flake check --allow-import-from-derivation
check:
runs-on: ubuntu-latest
permissions:
id-token: "write"
contents: "read"
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: accept-flake-config = true
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Build default package
run: nix build .
- name: Check flakes
run: nix flake check --allow-import-from-derivation

48
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Build & Publish Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to publish"
required: true
type: choice
options:
- client
- sdk
tag:
description: "NPM tag to use"
required: true
type: choice
default: nightly
options:
- latest
- nightly
permissions:
contents: read
# For provenance generation
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
environment: NPM Deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install
run: bun install --frozen-lockfile
- name: Publish to NPM
working-directory: packages/${{ inputs.package }}
run: bun publish --provenance --tag ${{ inputs.tag }} --access public
env:
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to JSR
working-directory: packages/${{ inputs.package }}
run: bunx jsr publish --allow-slow-types --allow-dirty

View file

@ -1,50 +0,0 @@
name: Staging build bundle
on:
push:
branches: ["staging"]
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Build dist
run: |
bun run build
- name: Bundle
run: |
mkdir bundle
cp -r dist bundle/
cp -r config bundle/
cp -r docs bundle/
cp -r CODE_OF_CONDUCT.md bundle/
cp -r CONTRIBUTING.md bundle/
cp -r README.md bundle/
cp -r flake.nix bundle/
cp -r shell.nix bundle/
cp -r flake.lock bundle/
cp -r LICENSE bundle/
cp -r SECURITY.md bundle/
tar cfJ archive.tar.xz bundle/
- name: Upload
uses: actions/upload-artifact@v4
with:
name: staging-dist
path: archive.tar.xz

36
.github/workflows/test-publish.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Test Publish
on:
push:
permissions:
contents: read
# For provenance generation
id-token: write
jobs:
# Build job
build:
runs-on: ubuntu-latest
environment: NPM Deploy
strategy:
matrix:
package: ["sdk", "client"]
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install
run: bun install --frozen-lockfile
- name: Publish to NPM
working-directory: packages/${{ matrix.package }}
env:
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun publish --dry-run --access public
- name: Publish to JSR
working-directory: packages/${{ matrix.package }}
run: bunx jsr publish --allow-slow-types --allow-dirty --dry-run

View file

@ -1,51 +1,53 @@
name: Tests
on:
push:
branches: ["*"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
workflow_call:
jobs:
tests:
runs-on: ubuntu-latest
services:
postgres:
image: ghcr.io/versia-pub/postgres:main
ports:
- 5432:5432
env:
POSTGRES_DB: versia
POSTGRES_USER: versia
POSTGRES_PASSWORD: versia
volumes:
- versia-data:/var/lib/postgresql/data
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
permissions:
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
ports:
- 5432:5432
env:
POSTGRES_DB: versia
POSTGRES_USER: versia
POSTGRES_PASSWORD: versia
volumes:
- versia-data:/var/lib/postgresql/data
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:latest
ports:
- 6379:6379
options: --health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Install NPM packages
run: |
bun install
- name: Move workflow config to config folder
run: |
mv .github/config.workflow.toml config/config.toml
- name: Move workflow config to config folder
run: |
mv .github/config.workflow.toml config/config.toml
- name: Run tests
run: |
bun run test
- name: Run tests
run: |
bun run test

7
.madgerc Normal file
View file

@ -0,0 +1,7 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

48
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,48 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/",
"stopOnEntry": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run index.ts",
"program": "${workspaceFolder}/index.ts",
"cwd": "${workspaceFolder}",
"watchMode": true
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run tests",
"program": "test"
}
]
}

View file

@ -6,7 +6,10 @@
"cli",
"federation",
"config",
"plugin"
"worker",
"media",
"packages/client",
"packages/sdk"
],
"languageToolLinter.languageTool.ignoredWordsInWorkspace": ["versia"]
}

View file

@ -1,4 +1,93 @@
# `0.7.0` (unreleased)
# `0.9.0` (upcoming)
## Features
### API
- [x] 🥺 Emoji Reactions are now available! You can react to any note with custom emojis.
- [x] 🔎 Added support for [batch account data API](https://docs.joinmastodon.org/methods/accounts/#index).
### Backend
- [x] 🚀 Upgraded Bun to `1.3.2`
# `0.8.0` • Federation 2: Electric Boogaloo
## Backwards Compatibility
Versia Server `0.8.0` is **not** backwards-compatible with `0.7.0`. This release includes some breaking changes to the database schema and configuration file.
Please see [Database Changes](#database-changes) and [New Configuration](#new-configuration) for more information.
## Features
### Federation
- [x] 🦄 Updated to [`Versia 0.5`](https://versia.pub/changelog).
- [x] 📦 Added support for new Versia features:
- [x] [**Instance Messaging Extension**](https://versia.pub/extensions/instance-messaging)
- [x] [**Shared Inboxes**](https://versia.pub/federation#inboxes)
- [x] 🔗 Changed entity URIs to be more readable (`example.org/objects/:id` → `example.org/{notes,likes,...}/:id`)
### API
- [x] 📲 Added [Push Notifications](https://docs.joinmastodon.org/methods/push) support.
- [x] 📖 Overhauled OpenAPI schemas to match [Mastodon API docs](https://docs.joinmastodon.org)
- [x] 👷 Improved [**Roles API**](https://server.versia.pub/api/roles) to allow for full role control (create, update, delete, assign).
- [x] ✏️ `<div>` and `<span>` tags are now allowed in Markdown.
- [x] 🔥 Removed nonstandard `/api/v1/accounts/id` endpoint (the same functionality was already possible with other endpoints).
- [x] ✨️ Implemented rate limiting support for API endpoints.
- [x] 🔒 Implemented `is_indexable` and `is_hiding_collections` fields to the [**Accounts API**](https://docs.joinmastodon.org/methods/accounts/#update_credentials).
- [x] ✨️ Muting other users now lets you specify a duration, after which the mute will be automatically removed.
- [x] 📰 All accounts now have an RSS/Atom feed attached to them.
### CLI
- [x] ⌨️ New commands!
- [x] ✨️ `cli user token` to generate API tokens.
- [x] 👷 Error messages are now prettier!
### Frontend
The way frontend is built and served has been changed. In the past, it was required to have a second process serving a frontend, which `versia-server` would proxy requests to. This is no longer the case.
Versia Server now serves static files directly from a configurable path, and `versia-fe` has been updated to support this.
### Backend
- [x] 🚀 Upgraded Bun to `1.2.13`
- [x] 🔥 Removed dependency on the `pg_uuidv7` extension. Versia Server can now be used with "vanilla" PostgreSQL.
- [x] 🖼️ Simplified media pipeline: this will improve S3 performance
- [x] 📈 It is now possible to disable media proxying for your CDN (offloading considerable bandwidth to your more optimized CDN).
- [x] 👷 Outbound federation, inbox processing, data fetching and media processing are now handled by a queue system.
- [x] 🌐 An administration panel is available at `/admin/queues` to monitor and manage queues.
- [x] 🔥 Removed support for **from-source** installations, as Versia Server is designed around containerization and maintaining support was a large burden.
- [x] ❄️ A [**Nix**](https://nixos.org/) package is now available for this project, packaged as a [Flake](https://wiki.nixos.org/wiki/Flakes). A **NixOS** module is also provided.
## New Configuration
Configuration parsing and validation has been overhauled. Unfortunately, this means that since a bunch of options have been renamed, you'll need to redownload [the default configuration file](config/config.example.toml) and reapply your changes.
## Database Changes
Various media-related attributes have been merged into a single `Medias` table. This will require a migration in order to preserve the old data.
Since very few instances are running `0.7.0`, we have decided to "rawdog it" instead of making a proper migration script (as that would take a ton of time that we don't have).
In the case that you've been running secret instances in the shadows, let us know and we'll help you out.
## Bug Fixes
- 🐛 All URIs in custom Markdown text are now correctly proxied.
- 🐛 Fixed several issues with the [ActivityPub Federation Bridge](https://github.com/versia-pub/activitypub) preventing it from operating properly.
- 🐛 Fixed incorrect content-type on some media when using S3.
- 🐛 All media content-type is now correctly fetched, instead of guessed from the file extension as before.
- 🐛 Fixed OpenAPI schema generation and `/docs` endpoint.
- 🐛 Logs folder is now automatically created if it doesn't exist.
- 🐛 Media hosted on the configured S3 bucket and on the local filesystem is no longer unnecessarily proxied.
- 🐛 Likes and Shares now federate properly.
# `0.7.0` • The Auth and APIs Update
> [!WARNING]
> This release marks the rename of the project from `Lysand` to `Versia`.
@ -9,87 +98,87 @@ Versia Server `0.7.0` is backwards compatible with `0.6.0`. However, some new fe
## Features
- Upgraded Bun to `1.1.34`. This brings performance upgrades and better stability.
- Added support for the [ActivityPub Federation Bridge](https://github.com/versia-pub/activitypub).
- Added support for the [Sonic](https://github.com/valeriansaliou/sonic) search indexer.
- Note deletions are now federated.
- Note edits are now federated.
- Added support for [Sentry](https://sentry.io).
- Added option for more federation debug logging.
- Added [**Roles API**](https://server.versia.pub/api/roles).
- Added [**Permissions API**](https://server.versia.pub/api/roles) and enabled it for every route.
- Added [**TOS and Privacy Policy**](https://server.versia.pub/api/mastodon) endpoints.
- Added [**Challenge API**](https://server.versia.pub/api/challenges). (basically CAPTCHAS). This can be enabled/disabled by administrators. No `versia-fe` support yet.
- Added ability to refetch user data from remote instances.
- Added ability to change the `username` of a user. ([Mastodon API extension](https://server.versia.pub/api/mastodon#api-v1-accounts-update-credentials)).
- Added an endpoint to get a user by its username.
- Add OpenID Connect registration support. Admins can now disable username/password registration entirely and still allow users to sign up via OpenID Connect.
- Add option to never convert vector images to a raster format.
- Refactor logging system to be more robust and easier to use. Log files are now automatically rotated.
- Add support for HTTP proxies.
- Add support for serving Versia over a Tor hidden service.
- Add global server error handler, to properly return 500 error messages to clients.
- Sign all federation HTTP requests.
- Add JSON schema for configuration file.
- Rewrite federation stack
- Updated federation to Versia 0.4
- Implement OAuth2 token revocation
- Add new **Plugin API**
- Upgraded Bun to `1.1.34`. This brings performance upgrades and better stability.
- Added support for the [ActivityPub Federation Bridge](https://github.com/versia-pub/activitypub).
- Added support for the [Sonic](https://github.com/valeriansaliou/sonic) search indexer.
- Note deletions are now federated.
- Note edits are now federated.
- Added support for [Sentry](https://sentry.io).
- Added option for more federation debug logging.
- Added [**Roles API**](https://server.versia.pub/api/roles).
- Added [**Permissions API**](https://server.versia.pub/api/roles) and enabled it for every route.
- Added [**TOS and Privacy Policy**](https://server.versia.pub/api/mastodon) endpoints.
- Added [**Challenge API**](https://server.versia.pub/api/challenges). (basically CAPTCHAS). This can be enabled/disabled by administrators. No `versia-fe` support yet.
- Added ability to refetch user data from remote instances.
- Added ability to change the `username` of a user. ([Mastodon API extension](https://server.versia.pub/api/mastodon#api-v1-accounts-update-credentials)).
- Added an endpoint to get a user by its username.
- Add OpenID Connect registration support. Admins can now disable username/password registration entirely and still allow users to sign up via OpenID Connect.
- Add option to never convert vector images to a raster format.
- Refactor logging system to be more robust and easier to use. Log files are now automatically rotated.
- Add support for HTTP proxies.
- Add support for serving Versia over a Tor hidden service.
- Add global server error handler, to properly return 500 error messages to clients.
- Sign all federation HTTP requests.
- Add JSON schema for configuration file.
- Rewrite federation stack
- Updated federation to Versia 0.4
- Implement OAuth2 token revocation
- Add new **Plugin API**
## Plugin System
A new plugin system for extending Versia Server has been added in this release!
> [!NOTE]
>
>
> This is an internal feature and is not documented. Support for third-party plugins will be given on a "if we have time" basis, until the API is fully stabilized and documented
Plugins using this framework support:
- [x] Plugin hotswapping and hotreloading
- [x] Manifest files (JSON, JSON5, JSONC supported) with metadata (JSON schema provided)
- [x] Installation by dropping a folder into the plugins/ directory
- [x] Support for plugins having their own NPM dependencies
- [x] Support for storing plugins' configuration in the main config.toml (single source of truth)
- [x] Schema-based strict config validation (plugins can specify their own schemas)
- [x] Full type-safety
- [x] Custom hooks
- [x] FFI compatibility (with `bun:ffi` or Node's FFI)
- [x] Custom API route registration or overriding or middlewaring
- [x] Automatic OpenAPI schema generation for all installed plugins
- [x] End-to-end and unit testing supported
- [x] Automatic user input validation for API routes with schemas (specify a schema for the route and the server will take care of validating everything)
- [x] Access to internal database abstractions
- [x] Support for sending raw SQL to database (type-safe!)
- [x] Plugin autoload on startup with override controls (enable/disable)
- [x] Plugin hotswapping and hotreloading
- [x] Manifest files (JSON, JSON5, JSONC supported) with metadata (JSON schema provided)
- [x] Installation by dropping a folder into the plugins/ directory
- [x] Support for plugins having their own NPM dependencies
- [x] Support for storing plugins' configuration in the main config.toml (single source of truth)
- [x] Schema-based strict config validation (plugins can specify their own schemas)
- [x] Full type-safety
- [x] Custom hooks
- [x] FFI compatibility (with `bun:ffi` or Node's FFI)
- [x] Custom API route registration or overriding or middlewaring
- [x] Automatic OpenAPI schema generation for all installed plugins
- [x] End-to-end and unit testing supported
- [x] Automatic user input validation for API routes with schemas (specify a schema for the route and the server will take care of validating everything)
- [x] Access to internal database abstractions
- [x] Support for sending raw SQL to database (type-safe!)
- [x] Plugin autoload on startup with override controls (enable/disable)
As a demonstration of the power of this system and an effort to modularize the codebase further, OpenID functionality has been moved to a plugin. This plugin is required for login.
## Bug Fixes
- Fix favouriting/unfavouriting sometimes returning negative counts.
- Non-images will now properly be uploaded to object storage.
- Make account searches case-insensitive
- Fix image decoding error when passing media through proxy.
- OpenID Connect now correctly remembers and passes `state` parameter.
- OpenID Connect will not reject some correct but weird redirect URIs.
- Markdown posts will not have invisible anchor tags anymore (this messed up accessibility).
- Reverse proxies incorrectly reporting an HTTPS request as HTTP will now be handled correctly during OpenID Connect flows.
- API Relationships will now correctly return `requested_by`.
- Make process wait for Ctrl+C to exit on error, instead of exiting immediately. This fixes some issues with Docker restarting endlessly.
- Animated media will now stay animated when uploaded.
- Some instance metadata will no longer be missing from `/api/v2/instabnce` endpoint. In fact, it will now be more complete than Mastodon's implementation.
- The Origin HTTP header will no longer be used to determine the origin of a request. This was a security issue.
- New notes will no longer incorrectly be federated to *all* remote users at once.
- Fix [Elk Client](https://elk.zone/) not being able to log in.
- Fix favouriting/unfavouriting sometimes returning negative counts.
- Non-images will now properly be uploaded to object storage.
- Make account searches case-insensitive
- Fix image decoding error when passing media through proxy.
- OpenID Connect now correctly remembers and passes `state` parameter.
- OpenID Connect will not reject some correct but weird redirect URIs.
- Markdown posts will not have invisible anchor tags anymore (this messed up accessibility).
- Reverse proxies incorrectly reporting an HTTPS request as HTTP will now be handled correctly during OpenID Connect flows.
- API Relationships will now correctly return `requested_by`.
- Make process wait for Ctrl+C to exit on error, instead of exiting immediately. This fixes some issues with Docker restarting endlessly.
- Animated media will now stay animated when uploaded.
- Some instance metadata will no longer be missing from `/api/v2/instabnce` endpoint. In fact, it will now be more complete than Mastodon's implementation.
- The Origin HTTP header will no longer be used to determine the origin of a request. This was a security issue.
- New notes will no longer incorrectly be federated to _all_ remote users at once.
- Fix [Elk Client](https://elk.zone/) not being able to log in.
## Removals
- Remove old logging system, to be replaced by a new one.
- Removed Meilisearch support, in favor of Sonic. Follow instructions in the [installation guide](https://server.versia.pub/setup/installation) to set up Sonic.
- Removed explicit Glitch-FE support. Glitch-FE will still work, but must be hosted separately like any other frontend.
- Remove old logging system, to be replaced by a new one.
- Removed Meilisearch support, in favor of Sonic. Follow instructions in the [installation guide](https://server.versia.pub/setup/installation) to set up Sonic.
- Removed explicit Glitch-FE support. Glitch-FE will still work, but must be hosted separately like any other frontend.
## Miscellaneous
- Remove Node.js from Docker build.
- Update all dependencies.
- Remove Node.js from Docker build.
- Update all dependencies.

View file

@ -112,7 +112,7 @@ TypeScript errors should be ignored with `// @ts-expect-error` comments, as well
To scan for all TypeScript errors, run:
```sh
bun check
bun typecheck
```
### Commit messages
@ -153,4 +153,4 @@ If you find a bug, please open an issue on GitHub. Please make sure to include t
# License
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.

View file

@ -1,7 +1,5 @@
# Node is required for building the project
FROM imbios/bun-node:1-20-alpine AS base
RUN apk add --no-cache libstdc++
FROM imbios/bun-node:latest-23-alpine AS base
# Install dependencies into temp directory
# This will cache them and speed up future builds
@ -22,20 +20,19 @@ COPY --from=install /temp/node_modules /temp/node_modules
# Build the project
WORKDIR /temp
RUN bun run build
RUN bun run build api
WORKDIR /temp/dist
# Copy production dependencies and source code into final image
FROM oven/bun:1.1.34-alpine
FROM oven/bun:1.3.2-alpine
# Install libstdc++ for Bun and create app directory
RUN apk add --no-cache libstdc++ && \
mkdir -p /app
RUN mkdir -p /app
COPY --from=build /temp/dist /app/dist
COPY entrypoint.sh /app
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
LABEL org.opencontainers.image.vendor="Versia Pub"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
@ -51,4 +48,4 @@ WORKDIR /app
ENV NODE_ENV=production
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
# Run migrations and start the server
CMD [ "cli", "start" ]
CMD [ "bun", "run", "api.js" ]

View file

@ -1,8 +1,30 @@
<p align="center">
<a href="https://versia.pub"><img src="https://cdn.versia.pub/branding/logo-dark.svg" alt="Versia Logo" height="110"></a>
</p>
<div align="center">
<a href="https://versia.pub">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.versia.pub/branding/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://cdn.versia.pub/branding/logo-light.svg">
<img src="https://cdn.versia.pub/branding/logo-dark.svg" alt="Versia Logo" height="110" />
</picture>
</a>
</div>
![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white) ![VS Code Insiders](https://img.shields.io/badge/VS%20Code%20Insiders-35b393.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=for-the-badge)](code_of_conduct.md)
<h2 align="center">
<strong><code>Versia Server</code></strong>
</h2>
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/typescript/typescript-original.svg" height="42" width="52" alt="TypeScript logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/postgresql/postgresql-original.svg" height="42" width="52" alt="PostgreSQL logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/docker/docker-original.svg" height="42" width="52" alt="Docker logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/bun/bun-original.svg" height="42" width="52" alt="Bun logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/vscode/vscode-original.svg" height="42" width="52" alt="VSCode logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/sentry/sentry-original.svg" height="42" width="52" alt="Sentry logo">
<img src="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linux/linux-original.svg" height="42" width="52" alt="Linux logo">
</div>
<br/>
## What is this?
@ -70,8 +92,10 @@ Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) fil
The following extensions are currently supported or being worked on:
- `pub.versia:custom_emojis`: Custom emojis
- `pub.versia:polls`: Polls
- `pub.versia:instance_messaging`: Instance Messaging
- `pub.versia:likes`: Likes
- `pub.versia:share`: Share
- `pub.versia:reactions`: Reactions
## API
@ -188,7 +212,7 @@ Working endpoints are:
- [x] `/oauth/authorize`
- [x] `/oauth/token`
- [x] `/oauth/revoke`
- Admin API
- Admin API
### Main work to do for API
@ -213,3 +237,21 @@ For Versia Server's own custom API, please see the [API documentation](https://s
## License
This project is licensed under the [AGPL-3.0-or-later](LICENSE).
All Versia assets (icon, logo, banners, etc) are licensed under [CC-BY-NC-SA-4.0](https://creativecommons.org/licenses/by-nc-sa/4.0)
## Thanks!
Thanks to [**Fastly**](https://fastly.com) for providing us with support and resources to build Versia!
<br />
<p align="center">
<a href="https://fastly.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/fastly-red.svg">
<source media="(prefers-color-scheme: light)" srcset="assets/fastly-red.svg">
<img src="assets/fastly-red.svg" alt="Fastly Logo" height="110" />
</picture>
</a>
</p>

51
Worker.Dockerfile Normal file
View file

@ -0,0 +1,51 @@
# Node is required for building the project
FROM imbios/bun-node:latest-23-alpine AS base
# Install dependencies into temp directory
# This will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp
COPY . /temp
WORKDIR /temp
RUN bun install --production
FROM base AS build
# Copy the project
RUN mkdir -p /temp
COPY . /temp
# Copy dependencies
COPY --from=install /temp/node_modules /temp/node_modules
# Build the project
WORKDIR /temp
RUN bun run build worker
WORKDIR /temp/dist
# Copy production dependencies and source code into final image
FROM oven/bun:1.3.2-alpine
# Install libstdc++ for Bun and create app directory
RUN mkdir -p /app
COPY --from=build /temp/dist /app/dist
COPY entrypoint.sh /app
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
LABEL org.opencontainers.image.vendor="Versia Pub"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
LABEL org.opencontainers.image.title="Versia Server Worker"
LABEL org.opencontainers.image.description="Versia Server Worker Docker image"
# Set current Git commit hash as an environment variable
ARG GIT_COMMIT
ENV GIT_COMMIT=$GIT_COMMIT
# CD to app
WORKDIR /app
ENV NODE_ENV=production
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
# Run migrations and start the server
CMD [ "bun", "run", "worker.js" ]

19
api.ts Normal file
View file

@ -0,0 +1,19 @@
import process from "node:process";
import { appFactory } from "@versia-server/api";
import { config } from "@versia-server/config";
import { Youch } from "youch";
import { createServer } from "@/server.ts";
process.on("SIGINT", () => {
process.exit();
});
process.on("uncaughtException", async (error) => {
const youch = new Youch();
console.error(await youch.toANSI(error));
});
await import("@versia-server/api/setup");
createServer(config, await appFactory());

View file

@ -1,228 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "@/math";
import { Application } from "@versia/kit/db";
import { config } from "~/packages/config-manager";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, deleteUsers, passwords } = await getTestUsers(1);
// Create application
const application = await Application.insert({
name: "Test Application",
clientId: randomString(32, "hex"),
secret: "test",
redirectUri: "https://example.com",
scopes: "read write",
});
afterAll(async () => {
await deleteUsers();
await application.delete();
});
// /api/auth/login
describe(meta.route, () => {
test("should get a JWT with email", async () => {
const formData = new FormData();
formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", passwords[0]);
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
config.http.base_url,
);
expect(locationHeader.pathname).toBe("/oauth/consent");
expect(locationHeader.searchParams.get("client_id")).toBe(
application.data.clientId,
);
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
"https://example.com",
);
expect(locationHeader.searchParams.get("response_type")).toBe("code");
expect(locationHeader.searchParams.get("scope")).toBe("read write");
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
});
test("should get a JWT with username", async () => {
const formData = new FormData();
formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]);
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
config.http.base_url,
);
expect(locationHeader.pathname).toBe("/oauth/consent");
expect(locationHeader.searchParams.get("client_id")).toBe(
application.data.clientId,
);
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
"https://example.com",
);
expect(locationHeader.searchParams.get("response_type")).toBe("code");
expect(locationHeader.searchParams.get("scope")).toBe("read write");
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
});
test("should have state in the URL", async () => {
const formData = new FormData();
formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", passwords[0]);
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
config.http.base_url,
);
expect(locationHeader.pathname).toBe("/oauth/consent");
expect(locationHeader.searchParams.get("client_id")).toBe(
application.data.clientId,
);
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
"https://example.com",
);
expect(locationHeader.searchParams.get("response_type")).toBe("code");
expect(locationHeader.searchParams.get("scope")).toBe("read write");
expect(locationHeader.searchParams.get("state")).toBe("abc");
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
});
describe("should reject invalid credentials", () => {
// Redirects to /oauth/authorize on invalid
test("invalid email", async () => {
const formData = new FormData();
formData.append("identifier", "ababa@gmail.com");
formData.append("password", "password");
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
"",
);
expect(locationHeader.pathname).toBe("/oauth/authorize");
expect(locationHeader.searchParams.get("error")).toBe(
"invalid_grant",
);
expect(locationHeader.searchParams.get("error_description")).toBe(
"Invalid identifier or password",
);
expect(response.headers.get("Set-Cookie")).toBeNull();
});
test("invalid username", async () => {
const formData = new FormData();
formData.append("identifier", "ababa");
formData.append("password", "password");
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
"",
);
expect(locationHeader.pathname).toBe("/oauth/authorize");
expect(locationHeader.searchParams.get("error")).toBe(
"invalid_grant",
);
expect(locationHeader.searchParams.get("error_description")).toBe(
"Invalid identifier or password",
);
expect(response.headers.get("Set-Cookie")).toBeNull();
});
test("invalid password", async () => {
const formData = new FormData();
formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", "password");
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
"",
);
expect(locationHeader.pathname).toBe("/oauth/authorize");
expect(locationHeader.searchParams.get("error")).toBe(
"invalid_grant",
);
expect(locationHeader.searchParams.get("error_description")).toBe(
"Invalid identifier or password",
);
expect(response.headers.get("Set-Cookie")).toBeNull();
});
});
});

View file

@ -1,236 +0,0 @@
import { apiRoute, applyConfig } from "@/api";
import type { Context } from "@hono/hono";
import { setCookie } from "@hono/hono/cookie";
import { createRoute } from "@hono/zod-openapi";
import { Application, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq, or } from "drizzle-orm";
import { SignJWT } from "jose";
import { z } from "zod";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
ratelimits: {
max: 4,
duration: 60,
},
route: "/api/auth/login",
auth: {
required: false,
},
});
export const schemas = {
form: z.object({
identifier: z
.string()
.email()
.toLowerCase()
.or(z.string().toLowerCase()),
password: z.string().min(2).max(100),
}),
query: z.object({
scope: z.string().optional(),
redirect_uri: z.string().url().optional(),
response_type: z.enum([
"code",
"token",
"none",
"id_token",
"code id_token",
"code token",
"token id_token",
"code token id_token",
]),
client_id: z.string(),
state: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.enum(["plain", "S256"]).optional(),
prompt: z
.enum(["none", "login", "consent", "select_account"])
.optional()
.default("none"),
max_age: z
.number()
.int()
.optional()
.default(60 * 60 * 24 * 7),
}),
};
const route = createRoute({
method: "post",
path: "/api/auth/login",
summary: "Login",
description: "Login to the application",
request: {
body: {
content: {
"multipart/form-data": {
schema: schemas.form,
},
},
},
query: schemas.query,
},
responses: {
302: {
description: "Redirect to OAuth authorize, or error",
headers: {
"Set-Cookie": {
description: "JWT cookie",
required: false,
},
},
},
},
});
const returnError = (
context: Context,
error: string,
description: string,
): Response => {
const searchParams = new URLSearchParams();
// Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(context.req.query())) {
if (key !== "email" && key !== "password" && value !== undefined) {
searchParams.append(key, value);
}
}
searchParams.append("error", error);
searchParams.append("error_description", description);
return context.redirect(
new URL(
`${config.frontend.routes.login}?${searchParams.toString()}`,
config.http.base_url,
).toString(),
);
};
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
| {
forced: boolean;
providers: {
id: string;
name: string;
icon: string;
}[];
keys: {
private: string;
public: string;
};
}
| undefined;
if (!oidcConfig) {
return returnError(
context,
"invalid_request",
"The OpenID Connect plugin is not enabled on this instance. Cannot process login request.",
);
}
if (oidcConfig?.forced) {
return returnError(
context,
"invalid_request",
"Logging in with a password is disabled by the administrator. Please use a valid OpenID Connect provider.",
);
}
const { identifier, password } = context.req.valid("form");
const { client_id } = context.req.valid("query");
// Find user
const user = await User.fromSql(
or(
eq(Users.email, identifier.toLowerCase()),
eq(Users.username, identifier.toLowerCase()),
),
);
if (
!(
user &&
(await Bun.password.verify(password, user.data.password || ""))
)
) {
return returnError(
context,
"invalid_grant",
"Invalid identifier or password",
);
}
if (user.data.passwordResetToken) {
return context.redirect(
`${config.frontend.routes.password_reset}?${new URLSearchParams(
{
token: user.data.passwordResetToken ?? "",
login_reset: "true",
},
).toString()}`,
);
}
// Try and import the key
const privateKey = await crypto.subtle.importKey(
"pkcs8",
Buffer.from(oidcConfig?.keys?.private ?? "", "base64"),
"Ed25519",
false,
["sign"],
);
// Generate JWT
const jwt = await new SignJWT({
sub: user.id,
iss: new URL(config.http.base_url).origin,
aud: client_id,
exp: Math.floor(Date.now() / 1000) + 60 * 60,
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000),
})
.setProtectedHeader({ alg: "EdDSA" })
.sign(privateKey);
const application = await Application.fromClientId(client_id);
if (!application) {
return context.json({ error: "Invalid application" }, 400);
}
const searchParams = new URLSearchParams({
application: application.data.name,
});
if (application.data.website) {
searchParams.append("website", application.data.website);
}
// Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(context.req.query())) {
if (key !== "email" && key !== "password" && value !== undefined) {
searchParams.append(key, String(value));
}
}
// Redirect to OAuth authorize with JWT
setCookie(context, "jwt", jwt, {
httpOnly: true,
secure: true,
sameSite: "Strict",
path: "/",
maxAge: 60 * 60,
});
return context.redirect(
`${config.frontend.routes.consent}?${searchParams.toString()}`,
);
}),
);

View file

@ -1,83 +0,0 @@
import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { db } from "@versia/kit/db";
import { Applications, Tokens } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
ratelimits: {
max: 4,
duration: 60,
},
route: "/api/auth/redirect",
auth: {
required: false,
},
});
export const schemas = {
query: z.object({
redirect_uri: z.string().url(),
client_id: z.string(),
code: z.string(),
}),
};
const route = createRoute({
method: "get",
path: "/api/auth/redirect",
summary: "OAuth Code flow",
description:
"Redirects to the application, or back to login if the code is invalid",
responses: {
302: {
description:
"Redirects to the application, or back to login if the code is invalid",
},
},
request: {
query: schemas.query,
},
});
/**
* OAuth Code flow
*/
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { redirect_uri, client_id, code } = context.req.valid("query");
const redirectToLogin = (error: string): Response =>
context.redirect(
`${config.frontend.routes.login}?${new URLSearchParams({
...context.req.query,
error: encodeURIComponent(error),
}).toString()}`,
);
const foundToken = await db
.select()
.from(Tokens)
.leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
.where(
and(
eq(Tokens.code, code),
eq(Applications.clientId, client_id),
),
)
.limit(1);
if (!foundToken || foundToken.length <= 0) {
return redirectToLogin("Invalid code");
}
// Redirect back to application
return context.redirect(
`${redirect_uri}?${new URLSearchParams({
code,
}).toString()}`,
);
}),
);

View file

@ -1,123 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "@/math";
import { Application } from "@versia/kit/db";
import { config } from "~/packages/config-manager";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, deleteUsers, passwords } = await getTestUsers(1);
const token = randomString(32, "hex");
const newPassword = randomString(16, "hex");
// Create application
const application = await Application.insert({
name: "Test Application",
clientId: randomString(32, "hex"),
secret: "test",
redirectUri: "https://example.com",
scopes: "read write",
});
afterAll(async () => {
await deleteUsers();
await application.delete();
});
// /api/auth/reset
describe(meta.route, () => {
test("should login with normal password", async () => {
const formData = new FormData();
formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]);
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
});
test("should reset password and refuse login with old password", async () => {
await users[0]?.update({
passwordResetToken: token,
});
const formData = new FormData();
formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]);
const response = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: formData,
},
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const locationHeader = new URL(
response.headers.get("Location") ?? "",
config.http.base_url,
);
expect(locationHeader.pathname).toBe("/oauth/reset");
expect(locationHeader.searchParams.get("token")).toBe(token);
});
test("should reset password and login with new password", async () => {
const formData = new FormData();
formData.append("token", token);
formData.append("password", newPassword);
formData.append("password2", newPassword);
const response = await fakeRequest("/api/auth/reset", {
method: "POST",
body: formData,
});
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBeDefined();
const loginFormData = new FormData();
loginFormData.append("identifier", users[0]?.data.username ?? "");
loginFormData.append("password", newPassword);
const loginResponse = await fakeRequest(
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
{
method: "POST",
body: loginFormData,
},
);
expect(loginResponse.status).toBe(302);
expect(loginResponse.headers.get("location")).toBeDefined();
const locationHeader = new URL(
loginResponse.headers.get("Location") ?? "",
config.http.base_url,
);
expect(locationHeader.pathname).toBe("/oauth/consent");
expect(locationHeader.searchParams.get("client_id")).toBe(
application.data.clientId,
);
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
"https://example.com",
);
expect(locationHeader.searchParams.get("response_type")).toBe("code");
expect(locationHeader.searchParams.get("scope")).toBe("read write");
expect(loginResponse.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
});
});

View file

@ -1,98 +0,0 @@
import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import type { Context } from "hono";
import { z } from "zod";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
ratelimits: {
max: 4,
duration: 60,
},
route: "/api/auth/reset",
auth: {
required: false,
},
});
export const schemas = {
form: z.object({
token: z.string().min(1),
password: z.string().min(3).max(100),
}),
};
const route = createRoute({
method: "post",
path: "/api/auth/reset",
summary: "Reset password",
description: "Reset password",
responses: {
302: {
description: "Redirect to the password reset page with a message",
},
},
request: {
body: {
content: {
"application/x-www-form-urlencoded": {
schema: schemas.form,
},
"multipart/form-data": {
schema: schemas.form,
},
},
},
},
});
const returnError = (
context: Context,
token: string,
error: string,
description: string,
): Response => {
const searchParams = new URLSearchParams();
searchParams.append("error", error);
searchParams.append("error_description", description);
searchParams.append("token", token);
return context.redirect(
new URL(
`${
config.frontend.routes.password_reset
}?${searchParams.toString()}`,
config.http.base_url,
).toString(),
);
};
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { token, password } = context.req.valid("form");
const user = await User.fromSql(eq(Users.passwordResetToken, token));
if (!user) {
return returnError(
context,
token,
"invalid_token",
"Invalid token",
);
}
await user.update({
password: await Bun.password.hash(password),
passwordResetToken: null,
});
return context.redirect(
`${config.frontend.routes.password_reset}?success=true`,
);
}),
);

View file

@ -1,68 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Relationship as ApiRelationship } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./block.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/:id/block
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
},
);
expect(response.status).toBe(401);
});
test("should return 404 if user not found", async () => {
const response = await fakeRequest(
meta.route.replace(":id", "00000000-0000-0000-0000-000000000000"),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(404);
});
test("should block user", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.blocking).toBe(true);
});
test("should return 200 if user already blocked", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.blocking).toBe(true);
});
});

View file

@ -1,97 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/block",
auth: {
required: true,
oauthPermissions: ["write:blocks"],
},
permissions: {
required: [
RolePermissions.ManageOwnBlocks,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/block",
summary: "Block user",
description: "Block a user",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
request: {
params: schemas.param,
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!foundRelationship.data.blocking) {
await foundRelationship.update({
blocking: true,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,78 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Relationship as ApiRelationship } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./follow.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/:id/follow
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(401);
});
test("should return 404 if user not found", async () => {
const response = await fakeRequest(
meta.route.replace(":id", "00000000-0000-0000-0000-000000000000"),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(404);
});
test("should follow user", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.following).toBe(true);
});
test("should return 200 if user already followed", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.following).toBe(true);
});
});

View file

@ -1,118 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/follow",
auth: {
required: true,
oauthPermissions: ["write:follows"],
},
permissions: {
required: [
RolePermissions.ManageOwnFollows,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
json: z
.object({
reblogs: z.coerce.boolean().optional(),
notify: z.coerce.boolean().optional(),
languages: z
.array(z.enum(ISO6391.getAllCodes() as [string, ...string[]]))
.optional(),
})
.optional()
.default({ reblogs: true, notify: false, languages: [] }),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/follow",
summary: "Follow user",
description: "Follow a user",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
request: {
params: schemas.param,
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const { reblogs, notify, languages } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
let relationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!relationship.data.following) {
relationship = await user.followRequest(otherUser, {
reblogs,
notify,
languages,
});
}
return context.json(relationship.toApi(), 200);
}),
);

View file

@ -1,80 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./followers.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
// Follow user
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
});
// /api/v1/accounts/:id/followers
describe(meta.route, () => {
test("should return 200 with followers", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiAccount[];
expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(1);
expect(data[0].id).toBe(users[0].id);
});
test("should return no followers after unfollowing", async () => {
// Unfollow user
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/unfollow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const response2 = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response2.status).toBe(200);
const data = (await response2.json()) as ApiAccount[];
expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(0);
});
});

View file

@ -1,107 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 60,
duration: 60,
},
route: "/api/v1/accounts/:id/followers",
auth: {
required: false,
oauthPermissions: ["read:accounts"],
},
permissions: {
required: [
RolePermissions.ViewAccountFollows,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
}),
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}/followers",
summary: "Get account followers",
description:
"Gets an paginated list of accounts that follow the specified account",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
query: schemas.query,
},
responses: {
200: {
description: "A list of accounts that follow the specified account",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
headers: {
Link: {
description: "Links to the next and previous pages",
},
},
},
404: {
description: "The specified account was not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id, limit } = context.req.valid("query");
const otherUser = await User.fromId(id);
// TODO: Add follower/following privacy settings
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
),
limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,80 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./following.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
// Follow user
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
});
// /api/v1/accounts/:id/following
describe(meta.route, () => {
test("should return 200 with following", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[0].id),
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiAccount[];
expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(1);
expect(data[0].id).toBe(users[1].id);
});
test("should return no following after unfollowing", async () => {
// Unfollow user
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/unfollow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const response2 = await fakeRequest(
meta.route.replace(":id", users[0].id),
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response2.status).toBe(200);
const data = (await response2.json()) as ApiAccount[];
expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(0);
});
});

View file

@ -1,108 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 60,
duration: 60,
},
route: "/api/v1/accounts/:id/following",
auth: {
required: false,
oauthPermissions: ["read:accounts"],
},
permissions: {
required: [
RolePermissions.ViewAccountFollows,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
}),
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}/following",
summary: "Get account following",
description:
"Gets an paginated list of accounts that the specified account follows",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
query: schemas.query,
},
responses: {
200: {
description:
"A list of accounts that the specified account follows",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
headers: {
Link: {
description: "Link to the next page of results",
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id } = context.req.valid("query");
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
// TODO: Add follower/following privacy settings
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
),
context.req.valid("query").limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,71 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id",
auth: {
required: false,
oauthPermissions: [],
},
permissions: {
required: [RolePermissions.ViewAccounts],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}",
summary: "Get account data",
description: "Gets the specified account data",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Account data",
content: {
"application/json": {
schema: User.schema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const foundUser = await User.fromId(id);
if (!foundUser) {
return context.json({ error: "User not found" }, 404);
}
return context.json(foundUser.toApi(user?.id === foundUser.id), 200);
}),
);

View file

@ -1,78 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Relationship as ApiRelationship } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./mute.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/:id/mute
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(401);
});
test("should return 404 if user not found", async () => {
const response = await fakeRequest(
meta.route.replace(":id", "00000000-0000-0000-0000-000000000000"),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(404);
});
test("should mute user", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.muting).toBe(true);
});
test("should return 200 if user already muted", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.muting).toBe(true);
});
});

View file

@ -1,115 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/mute",
auth: {
required: true,
oauthPermissions: ["write:mutes"],
},
permissions: {
required: [
RolePermissions.ManageOwnMutes,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
json: z.object({
notifications: z.boolean().optional(),
duration: z
.number()
.int()
.min(60)
.max(60 * 60 * 24 * 365 * 5)
.optional(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/mute",
summary: "Mute user",
description: "Mute a user",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
// TODO: Add duration support
const { notifications } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
// TODO: Implement duration
await foundRelationship.update({
muting: true,
mutingNotifications: notifications ?? true,
});
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,106 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/note",
auth: {
required: true,
oauthPermissions: ["write:accounts"],
},
permissions: {
required: [
RolePermissions.ManageOwnAccount,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
json: z.object({
comment: z.string().min(0).max(5000).trim().optional(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/note",
summary: "Set note",
description: "Set a note on a user's profile, visible only to you",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const { comment } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
note: comment,
});
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,95 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/pin",
auth: {
required: true,
oauthPermissions: ["write:accounts"],
},
permissions: {
required: [
RolePermissions.ManageOwnAccount,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/pin",
summary: "Pin user",
description: "Pin a user to your profile",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
endorsed: true,
});
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,97 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 4,
duration: 60,
},
route: "/api/v1/accounts/:id/refetch",
auth: {
required: true,
oauthPermissions: ["write:accounts"],
},
permissions: {
required: [RolePermissions.ViewAccounts],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/refetch",
summary: "Refetch user",
description: "Refetch a user's profile from the remote server",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated user data",
content: {
"application/json": {
schema: User.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
400: {
description: "User is local",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
if (otherUser.isLocal()) {
return context.json({ error: "Cannot refetch a local user" }, 400);
}
const newUser = await otherUser.updateFromRemote();
return context.json(newUser.toApi(false), 200);
}),
);

View file

@ -1,102 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/remove_from_followers",
auth: {
required: true,
oauthPermissions: ["write:follows"],
},
permissions: {
required: [
RolePermissions.ManageOwnFollows,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/remove_from_followers",
summary: "Remove user from followers",
description: "Remove a user from your followers",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
otherUser,
self,
);
if (oppositeRelationship.data.following) {
await oppositeRelationship.update({
following: false,
});
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,152 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils.ts";
import { meta } from "./statuses.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[1])).toReversed();
const timeline2 = (await getTestStatuses(40, users[2])).toReversed();
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
const response = await fakeRequest(
`/api/v1/statuses/${timeline2[0].id}/reblog`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(201);
});
// /api/v1/accounts/:id/statuses
describe(meta.route, () => {
test("should return 200 with statuses", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiStatus[];
expect(data.length).toBe(20);
// Should have reblogs
expect(data[0].reblog).toBeDefined();
});
test("should exclude reblogs", async () => {
const response = await fakeRequest(
`${meta.route.replace(":id", users[1].id)}?exclude_reblogs=true`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiStatus[];
expect(data.length).toBe(20);
// Should not have reblogs
expect(data[0].reblog).toBeNull();
});
test("should exclude replies", async () => {
// Create reply
const replyResponse = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: new URLSearchParams({
status: "Reply",
in_reply_to_id: timeline[0].id,
local_only: "true",
}),
});
expect(replyResponse.status).toBe(201);
const response = await fakeRequest(
`${meta.route.replace(":id", users[1].id)}?exclude_replies=true`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiStatus[];
expect(data.length).toBe(20);
// Should not have replies
expect(data[0].in_reply_to_id).toBeNull();
});
test("should only include pins", async () => {
const response = await fakeRequest(
`${meta.route.replace(":id", users[1].id)}?pinned=true`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiStatus[];
expect(data.length).toBe(0);
// Create pin
const pinResponse = await fakeRequest(
`/api/v1/statuses/${timeline[3].id}/pin`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(pinResponse.status).toBe(200);
const response2 = await fakeRequest(
`${meta.route.replace(":id", users[1].id)}?pinned=true`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response2.status).toBe(200);
const data2 = (await response2.json()) as ApiStatus[];
expect(data2.length).toBe(1);
});
});

View file

@ -1,150 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Note, Timeline, User } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/statuses",
auth: {
required: false,
oauthPermissions: ["read:statuses"],
},
permissions: {
required: [RolePermissions.ViewNotes, RolePermissions.ViewAccounts],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
only_media: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
exclude_replies: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
exclude_reblogs: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
pinned: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
tagged: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}/statuses",
summary: "Get account statuses",
description: "Gets an paginated list of statuses by the specified account",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
query: schemas.query,
},
responses: {
200: {
description: "A list of statuses by the specified account",
content: {
"application/json": {
schema: z.array(Note.schema),
},
},
headers: {
Link: {
description: "Links to the next and previous pages",
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const {
max_id,
min_id,
since_id,
limit,
exclude_reblogs,
only_media,
exclude_replies,
pinned,
} = context.req.valid("query");
const { objects, link } = await Timeline.getNoteTimeline(
and(
max_id ? lt(Notes.id, max_id) : undefined,
since_id ? gte(Notes.id, since_id) : undefined,
min_id ? gt(Notes.id, min_id) : undefined,
eq(Notes.authorId, id),
only_media
? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})`
: undefined,
pinned
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`
: undefined,
// Visibility check
or(
sql`EXISTS (SELECT 1 FROM "NoteToMentions" WHERE "NoteToMentions"."noteId" = ${Notes.id} AND "NoteToMentions"."userId" = ${otherUser.id})`,
and(
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Notes.authorId} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
inArray(Notes.visibility, ["public", "private"]),
),
inArray(Notes.visibility, ["public", "unlisted"]),
),
exclude_reblogs ? isNull(Notes.reblogId) : undefined,
exclude_replies ? isNull(Notes.replyId) : undefined,
),
limit,
context.req.url,
user?.id,
);
return context.json(
await Promise.all(objects.map((note) => note.toApi(otherUser))),
200,
{
link,
},
);
}),
);

View file

@ -1,97 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/unblock",
auth: {
required: true,
oauthPermissions: ["write:blocks"],
},
permissions: {
required: [
RolePermissions.ManageOwnBlocks,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/unblock",
summary: "Unblock user",
description: "Unblock a user",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (foundRelationship.data.blocking) {
await foundRelationship.update({
blocking: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,103 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/unfollow",
auth: {
required: true,
oauthPermissions: ["write:follows"],
},
permissions: {
required: [
RolePermissions.ManageOwnFollows,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/unfollow",
summary: "Unfollow user",
description: "Unfollow a user",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
500: {
description: "Failed to unfollow user during federation",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (!(await self.unfollow(otherUser, foundRelationship))) {
return context.json({ error: "Failed to unfollow user" }, 500);
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,77 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Relationship as ApiRelationship } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./unmute.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/mute`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
});
// /api/v1/accounts/:id/unmute
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
},
);
expect(response.status).toBe(401);
});
test("should return 404 if user not found", async () => {
const response = await fakeRequest(
meta.route.replace(":id", "00000000-0000-0000-0000-000000000000"),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(404);
});
test("should unmute user", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.muting).toBe(false);
});
test("should return 200 if user already unmuted", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const relationship = (await response.json()) as ApiRelationship;
expect(relationship.muting).toBe(false);
});
});

View file

@ -1,98 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/unmute",
auth: {
required: true,
oauthPermissions: ["write:mutes"],
},
permissions: {
required: [
RolePermissions.ManageOwnMutes,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/unmute",
summary: "Unmute user",
description: "Unmute a user",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const user = await User.fromId(id);
if (!user) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
user,
);
if (foundRelationship.data.muting) {
await foundRelationship.update({
muting: false,
mutingNotifications: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,97 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/:id/unpin",
auth: {
required: true,
oauthPermissions: ["write:accounts"],
},
permissions: {
required: [
RolePermissions.ManageOwnAccount,
RolePermissions.ViewAccounts,
],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/unpin",
summary: "Unpin user",
description: "Unpin a user from your profile",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (foundRelationship.data.endorsed) {
await foundRelationship.update({
endorsed: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,156 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestUsers } from "~/tests/utils.ts";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
beforeAll(async () => {
// Create followers relationships
const result1 = await fakeRequest(
`/api/v1/accounts/${users[1].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(result1.status).toBe(200);
const result2 = await fakeRequest(
`/api/v1/accounts/${users[2].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(result2.status).toBe(200);
const result3 = await fakeRequest(
`/api/v1/accounts/${users[3].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(result3.status).toBe(200);
const result4 = await fakeRequest(
`/api/v1/accounts/${users[2].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(result4.status).toBe(200);
const result5 = await fakeRequest(
`/api/v1/accounts/${users[3].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(result5.status).toBe(200);
const result6 = await fakeRequest(
`/api/v1/accounts/${users[3].id}/follow`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[2].data.accessToken}`,
},
},
);
expect(result6.status).toBe(200);
});
afterAll(async () => {
await deleteUsers();
});
describe(meta.route, () => {
test("should return 0 familiar followers", async () => {
const response = await fakeRequest(`${meta.route}?id=${users[4].id}`, {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.length).toBe(1);
expect(data[0].id).toBe(users[4].id);
expect(data[0].accounts).toBeArrayOfSize(0);
});
test("should return 1 familiar follower", async () => {
const response = await fakeRequest(`${meta.route}?id=${users[2].id}`, {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.length).toBe(1);
expect(data[0].id).toBe(users[2].id);
expect(data[0].accounts[0].id).toBe(users[1].id);
});
test("should return 2 familiar followers", async () => {
const response = await fakeRequest(`${meta.route}?id=${users[3].id}`, {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.length).toBe(1);
expect(data[0].id).toBe(users[3].id);
expect(data[0].accounts).toBeArrayOfSize(2);
expect(data[0].accounts[0].id).toBe(users[2].id);
expect(data[0].accounts[1].id).toBe(users[1].id);
});
test("should work with multiple ids", async () => {
const response = await fakeRequest(
`${meta.route}?id[]=${users[2].id}&id[]=${users[3].id}&id[]=${users[4].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.length).toBe(3);
expect(data[0].id).toBe(users[2].id);
expect(data[0].accounts[0].id).toBe(users[1].id);
expect(data[1].id).toBe(users[3].id);
expect(data[1].accounts[0].id).toBe(users[2].id);
expect(data[1].accounts[1].id).toBe(users[1].id);
expect(data[2].id).toBe(users[4].id);
expect(data[2].accounts).toBeArrayOfSize(0);
});
});

View file

@ -1,111 +0,0 @@
import { apiRoute, applyConfig, auth, qsQuery } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User, db } from "@versia/kit/db";
import { RolePermissions, type Users } from "@versia/kit/tables";
import { type InferSelectModel, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/accounts/familiar_followers",
ratelimits: {
max: 5,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:follows"],
},
permissions: {
required: [RolePermissions.ManageOwnFollows],
},
});
export const schemas = {
query: z.object({
id: z
.array(z.string().uuid())
.min(1)
.max(10)
.or(z.string().uuid())
.transform((v) => (Array.isArray(v) ? v : [v])),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/familiar_followers",
summary: "Get familiar followers",
description:
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
middleware: [auth(meta.auth, meta.permissions), qsQuery()],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Familiar followers",
content: {
"application/json": {
schema: z.array(
z.object({
id: z.string().uuid(),
accounts: z.array(User.schema),
}),
),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user: self } = context.get("auth");
const { id: ids } = context.req.valid("query");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
// Find followers of the accounts in "ids", that you also follow
const finalUsers = await Promise.all(
ids.map(async (id) => ({
id,
accounts: await User.fromIds(
(
await db.execute(sql<InferSelectModel<typeof Users>>`
SELECT "Users"."id" FROM "Users"
INNER JOIN "Relationships" AS "SelfFollowing"
ON "SelfFollowing"."subjectId" = "Users"."id"
WHERE "SelfFollowing"."ownerId" = ${self.id}
AND "SelfFollowing"."following" = true
AND EXISTS (
SELECT 1 FROM "Relationships" AS "IdsFollowers"
WHERE "IdsFollowers"."subjectId" = ${id}
AND "IdsFollowers"."ownerId" = "Users"."id"
AND "IdsFollowers"."following" = true
)
`)
).rows.map((u) => u.id as string),
),
})),
);
return context.json(
finalUsers.map((u) => ({
...u,
accounts: u.accounts.map((a) => a.toApi()),
})),
200,
);
}),
);

View file

@ -1,33 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/id
describe(meta.route, () => {
test("should correctly get user from username", async () => {
const response = await fakeRequest(
`${meta.route}?username=${users[0].data.username}`,
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiAccount;
expect(data.id).toBe(users[0].id);
});
test("should return 404 for non-existent user", async () => {
const response = await fakeRequest(
`${meta.route}?username=${users[0].data.username}-nonexistent`,
);
expect(response.status).toBe(404);
});
});

View file

@ -1,73 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/id",
auth: {
required: false,
oauthPermissions: [],
},
permissions: {
required: [RolePermissions.Search],
},
});
export const schemas = {
query: z.object({
username: z.string().min(1).max(512).toLowerCase(),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/id",
summary: "Get account by username",
description: "Get an account by username",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
404: {
description: "Not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { username } = context.req.valid("query");
const user = await User.fromSql(
and(eq(Users.username, username), isNull(Users.instanceId)),
);
if (!user) {
return context.json({ error: "User not found" }, 404);
}
return context.json(user.toApi(), 200);
}),
);

View file

@ -1,223 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { randomString } from "@/math";
import { db } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { fakeRequest, getSolvedChallenge } from "~/tests/utils";
import { meta } from "./index.ts";
const username = randomString(10, "hex");
const username2 = randomString(10, "hex");
afterEach(async () => {
await db.delete(Users).where(eq(Users.username, username));
await db.delete(Users).where(eq(Users.username, username2));
});
// /api/v1/statuses
describe(meta.route, () => {
test("should create a new account", async () => {
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username,
email: "bob@gamer.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response.ok).toBe(true);
});
test("should refuse invalid emails", async () => {
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username,
email: "bob",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response.status).toBe(422);
});
test("should require a password", async () => {
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username,
email: "contatc@bob.com",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response.status).toBe(422);
});
test("should not allow a previously registered email", async () => {
await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username,
email: "contact@george.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: username2,
email: "contact@george.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response.status).toBe(422);
});
test("should not allow a previously registered email (case insensitive)", async () => {
await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username,
email: "contact@george.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: username2,
email: "CONTACT@george.CoM",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response.status).toBe(422);
});
test("should not allow invalid usernames (not a-z_0-9)", async () => {
const response1 = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: "bob$",
email: "contact@bob.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response1.status).toBe(422);
const response2 = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: "bob-markey",
email: "contact@bob.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response2.status).toBe(422);
const response3 = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: "bob markey",
email: "contact@bob.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response3.status).toBe(422);
const response4 = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Challenge-Solution": await getSolvedChallenge(),
},
body: JSON.stringify({
username: "BOB",
email: "contact@bob.com",
password: "password",
agreement: "true",
locale: "en",
reason: "testing",
}),
});
expect(response4.status).toBe(422);
});
});

View file

@ -1,342 +0,0 @@
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
import { tempmailDomains } from "@/tempmail";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/accounts",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: false,
oauthPermissions: ["write:accounts"],
},
challenge: {
required: true,
},
});
export const schemas = {
json: z.object({
username: z.string(),
email: z.string().toLowerCase(),
password: z.string().optional(),
agreement: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.or(z.boolean()),
locale: z.string(),
reason: z.string(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/accounts",
summary: "Create account",
description: "Register a new account",
middleware: [
auth(meta.auth, meta.permissions, meta.challenge),
jsonOrForm(),
],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Account created",
},
422: {
description: "Validation failed",
content: {
"application/json": {
schema: z.object({
error: z.string(),
details: z.object({
username: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_TOO_LONG",
"ERR_TOO_SHORT",
"ERR_BLOCKED",
"ERR_TAKEN",
"ERR_RESERVED",
"ERR_ACCEPTED",
"ERR_INCLUSION",
]),
description: z.string(),
}),
),
email: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_BLOCKED",
"ERR_TAKEN",
]),
description: z.string(),
}),
),
password: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_TOO_LONG",
"ERR_TOO_SHORT",
]),
description: z.string(),
}),
),
agreement: z.array(
z.object({
error: z.enum(["ERR_ACCEPTED"]),
description: z.string(),
}),
),
locale: z.array(
z.object({
error: z.enum(["ERR_BLANK", "ERR_INVALID"]),
description: z.string(),
}),
),
reason: z.array(
z.object({
error: z.enum(["ERR_BLANK"]),
description: z.string(),
}),
),
}),
}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const form = context.req.valid("json");
const { username, email, password, agreement, locale } =
context.req.valid("json");
if (!config.signups.registration) {
return context.json(
{
error: "Registration is disabled",
},
422,
);
}
const errors: {
details: Record<
string,
{
error:
| "ERR_BLANK"
| "ERR_INVALID"
| "ERR_TOO_LONG"
| "ERR_TOO_SHORT"
| "ERR_BLOCKED"
| "ERR_TAKEN"
| "ERR_RESERVED"
| "ERR_ACCEPTED"
| "ERR_INCLUSION";
description: string;
}[]
>;
} = {
details: {
password: [],
username: [],
email: [],
agreement: [],
locale: [],
reason: [],
},
};
// Check if fields are blank
for (const value of [
"username",
"email",
"password",
"agreement",
"locale",
"reason",
]) {
// @ts-expect-error We don't care about the type here
if (!form[value]) {
errors.details[value].push({
error: "ERR_BLANK",
description: "can't be blank",
});
}
}
// Check if username is valid
if (!username?.match(/^[a-z0-9_]+$/)) {
errors.details.username.push({
error: "ERR_INVALID",
description:
"must only contain lowercase letters, numbers, and underscores",
});
}
// Check if username doesnt match filters
if (config.filters.username.some((filter) => username?.match(filter))) {
errors.details.username.push({
error: "ERR_INVALID",
description: "contains blocked words",
});
}
// Check if username is too long
if ((username?.length ?? 0) > config.validation.max_username_size) {
errors.details.username.push({
error: "ERR_TOO_LONG",
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
});
}
// Check if username is too short
if ((username?.length ?? 0) < 3) {
errors.details.username.push({
error: "ERR_TOO_SHORT",
description: "is too short (minimum is 3 characters)",
});
}
// Check if username is reserved
if (config.validation.username_blacklist.includes(username ?? "")) {
errors.details.username.push({
error: "ERR_RESERVED",
description: "is reserved",
});
}
// Check if username is taken
if (
await User.fromSql(
and(eq(Users.username, username)),
isNull(Users.instanceId),
)
) {
errors.details.username.push({
error: "ERR_TAKEN",
description: "is already taken",
});
}
// Check if email is valid
if (
!email?.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
)
) {
errors.details.email.push({
error: "ERR_INVALID",
description: "must be a valid email address",
});
}
// Check if email is blocked
if (
config.validation.email_blacklist.includes(email) ||
(config.validation.blacklist_tempmail &&
tempmailDomains.domains.includes((email ?? "").split("@")[1]))
) {
errors.details.email.push({
error: "ERR_BLOCKED",
description: "is from a blocked email provider",
});
}
// Check if email is taken
if (await User.fromSql(eq(Users.email, email))) {
errors.details.email.push({
error: "ERR_TAKEN",
description: "is already taken",
});
}
// Check if agreement is accepted
if (!agreement) {
errors.details.agreement.push({
error: "ERR_ACCEPTED",
description: "must be accepted",
});
}
if (!locale) {
errors.details.locale.push({
error: "ERR_BLANK",
description: "can't be blank",
});
}
if (!ISO6391.validate(locale ?? "")) {
errors.details.locale.push({
error: "ERR_INVALID",
description: "must be a valid ISO 639-1 code",
});
}
// If any errors are present, return them
if (Object.values(errors.details).some((value) => value.length > 0)) {
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
const errorsText = Object.entries(errors.details)
.filter(([_, errors]) => errors.length > 0)
.map(
([name, errors]) =>
`${name} ${errors
.map((error) => error.description)
.join(", ")}`,
)
.join(", ");
return context.json(
{
error: `Validation failed: ${errorsText}`,
details: Object.fromEntries(
Object.entries(errors.details).filter(
([_, errors]) => errors.length > 0,
),
),
},
422,
);
}
await User.fromDataLocal({
username: username ?? "",
password: password ?? "",
email: email ?? "",
});
return context.newResponse(null, 200);
}),
);

View file

@ -1,37 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/lookup
describe(meta.route, () => {
test("should return 200 with users", async () => {
const response = await fakeRequest(
`${meta.route}?acct=${users[0].data.username}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiAccount[];
expect(data).toEqual(
expect.objectContaining({
id: users[0].id,
username: users[0].data.username,
display_name: users[0].data.displayName,
avatar: expect.any(String),
header: expect.any(String),
}),
);
});
});

View file

@ -1,128 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import {
anyOf,
charIn,
createRegExp,
digit,
exactly,
global,
letter,
maybe,
oneOrMore,
} from "magic-regexp";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 30,
duration: 60,
},
route: "/api/v1/accounts/lookup",
auth: {
required: false,
oauthPermissions: [],
},
permissions: {
required: [RolePermissions.Search],
},
});
export const schemas = {
query: z.object({
acct: z.string().min(1).max(512),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/lookup",
summary: "Lookup account",
description: "Lookup an account by acct",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
404: {
description: "Not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { acct } = context.req.valid("query");
const { user } = context.get("auth");
// Check if acct is matching format username@domain.com or @username@domain.com
const accountMatches = acct?.trim().match(
createRegExp(
maybe("@"),
oneOrMore(
anyOf(letter.lowercase, digit, charIn("-")),
).groupedAs("username"),
exactly("@"),
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs(
"domain",
),
[global],
),
);
if (accountMatches) {
// Remove leading @ if it exists
if (accountMatches[0].startsWith("@")) {
accountMatches[0] = accountMatches[0].slice(1);
}
const [username, domain] = accountMatches[0].split("@");
const manager = await (user ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, domain);
const foundAccount = await User.resolve(uri);
if (foundAccount) {
return context.json(foundAccount.toApi(), 200);
}
return context.json({ error: "Account not found" }, 404);
}
let username = acct;
if (username.startsWith("@")) {
username = username.slice(1);
}
const account = await User.fromSql(eq(Users.username, username));
if (account) {
return context.json(account.toApi(), 200);
}
return context.json(
{ error: `Account with username ${username} not found` },
404,
);
}),
);

View file

@ -1,112 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
beforeAll(async () => {
// user0 should be `locked`
// user1 should follow user0
// user0 should follow user2
await db
.update(Users)
.set({ isLocked: true })
.where(eq(Users.id, users[0].id));
const res1 = await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
expect(res1.ok).toBe(true);
const res2 = await fakeRequest(`/api/v1/accounts/${users[2].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
expect(res2.ok).toBe(true);
});
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/relationships
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`${meta.route}?id[]=${users[2].id}`);
expect(response.status).toBe(401);
});
test("should return relationships", async () => {
const response = await fakeRequest(
`${meta.route}?id[]=${users[2].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: users[2].id,
following: true,
followed_by: false,
blocking: false,
muting: false,
muting_notifications: false,
requested: false,
domain_blocking: false,
endorsed: false,
}),
]),
);
});
test("should be requested_by user1", async () => {
const response = await fakeRequest(
`${meta.route}?id[]=${users[1].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
following: false,
followed_by: false,
blocking: false,
muting: false,
muting_notifications: false,
requested_by: true,
domain_blocking: false,
endorsed: false,
}),
]),
);
});
});

View file

@ -1,84 +0,0 @@
import { apiRoute, applyConfig, auth, qsQuery } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/accounts/relationships",
ratelimits: {
max: 30,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:follows"],
},
permissions: {
required: [RolePermissions.ManageOwnFollows],
},
});
export const schemas = {
query: z.object({
id: z.array(z.string().uuid()).min(1).max(10).or(z.string().uuid()),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/accounts/relationships",
summary: "Get relationships",
description: "Get relationships by account ID",
middleware: [auth(meta.auth, meta.permissions), qsQuery()],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Relationships",
content: {
"application/json": {
schema: z.array(Relationship.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user: self } = context.get("auth");
const { id } = context.req.valid("query");
const ids = Array.isArray(id) ? id : [id];
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const relationships = await Relationship.fromOwnerAndSubjects(
self,
ids,
);
relationships.sort(
(a, b) =>
ids.indexOf(a.data.subjectId) - ids.indexOf(b.data.subjectId),
);
return context.json(
relationships.map((r) => r.toApi()),
200,
);
}),
);

View file

@ -1,39 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/search
describe(meta.route, () => {
test("should return 200 with users", async () => {
const response = await fakeRequest(
`${meta.route}?q=${users[0].data.username}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const data = (await response.json()) as ApiAccount[];
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: users[0].id,
username: users[0].data.username,
display_name: users[0].data.displayName,
avatar: expect.any(String),
header: expect.any(String),
}),
]),
);
});
});

View file

@ -1,155 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { eq, ilike, not, or, sql } from "drizzle-orm";
import {
anyOf,
charIn,
createRegExp,
digit,
exactly,
global,
letter,
maybe,
oneOrMore,
} from "magic-regexp";
import stringComparison from "string-comparison";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/accounts/search",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: false,
oauthPermissions: ["read:accounts"],
},
permissions: {
required: [RolePermissions.Search, RolePermissions.ViewAccounts],
},
});
export const schemas = {
query: z.object({
q: z
.string()
.min(1)
.max(512)
.regex(
createRegExp(
maybe("@"),
oneOrMore(
anyOf(letter.lowercase, digit, charIn("-")),
).groupedAs("username"),
maybe(
exactly("@"),
oneOrMore(
anyOf(letter, digit, charIn("_-.:")),
).groupedAs("domain"),
),
[global],
),
),
limit: z.coerce.number().int().min(1).max(80).default(40),
offset: z.coerce.number().int().optional(),
resolve: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
following: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
}),
};
export const route = createRoute({
method: "get",
path: "/api/v1/accounts/search",
summary: "Search accounts",
description: "Search for accounts",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Accounts",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { q, limit, offset, resolve, following } =
context.req.valid("query");
const { user: self } = context.get("auth");
if (!self && following) {
return context.json({ error: "Unauthorized" }, 401);
}
const [username, host] = q.replace(/^@/, "").split("@");
const accounts: User[] = [];
if (resolve && username && host) {
const manager = await (self ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, host);
const resolvedUser = await User.resolve(uri);
if (resolvedUser) {
accounts.push(resolvedUser);
}
} else {
accounts.push(
...(await User.manyFromSql(
or(
ilike(Users.displayName, `%${q}%`),
ilike(Users.username, `%${q}%`),
following && self
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${self.id} AND "Relationships"."following" = true)`
: undefined,
self ? not(eq(Users.id, self.id)) : undefined,
),
undefined,
limit,
offset,
)),
);
}
const indexOfCorrectSort = stringComparison.jaccardIndex
.sortMatch(
q,
accounts.map((acct) => acct.getAcct()),
)
.map((sort) => sort.index);
const result = indexOfCorrectSort.map((index) => accounts[index]);
return context.json(
result.map((acct) => acct.toApi()),
200,
);
}),
);

View file

@ -1,388 +0,0 @@
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
import { sanitizedHtmlStrip } from "@/sanitization";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji, User, db } from "@versia/kit/db";
import { EmojiToUser, RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { contentToHtml } from "~/classes/functions/status";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/accounts/update_credentials",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["write:accounts"],
},
permissions: {
required: [RolePermissions.ManageOwnAccount],
},
});
export const schemas = {
json: z.object({
display_name: z
.string()
.min(3)
.trim()
.max(config.validation.max_displayname_size)
.refine(
(s) =>
!config.filters.displayname.some((filter) =>
s.match(filter),
),
"Display name contains blocked words",
)
.optional(),
username: z
.string()
.min(3)
.trim()
.max(config.validation.max_username_size)
.toLowerCase()
.regex(
/^[a-z0-9_-]+$/,
"Username can only contain letters, numbers, underscores and hyphens",
)
.refine(
(s) =>
!config.filters.username.some((filter) => s.match(filter)),
"Username contains blocked words",
)
.optional(),
note: z
.string()
.min(0)
.max(config.validation.max_bio_size)
.trim()
.refine(
(s) => !config.filters.bio.some((filter) => s.match(filter)),
"Bio contains blocked words",
)
.optional(),
avatar: z
.instanceof(File)
.refine(
(v) => v.size <= config.validation.max_avatar_size,
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
)
.optional(),
header: z
.instanceof(File)
.refine(
(v) => v.size <= config.validation.max_header_size,
`Header must be less than ${config.validation.max_header_size} bytes`,
)
.optional(),
locked: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
bot: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
discoverable: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
source: z
.object({
privacy: z
.enum(["public", "unlisted", "private", "direct"])
.optional(),
sensitive: z
.string()
.transform((v) =>
["true", "1", "on"].includes(v.toLowerCase()),
)
.optional(),
language: z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.optional(),
})
.optional(),
fields_attributes: z
.array(
z.object({
name: z
.string()
.trim()
.max(config.validation.max_field_name_size),
value: z
.string()
.trim()
.max(config.validation.max_field_value_size),
}),
)
.max(config.validation.max_field_count)
.optional(),
}),
};
const route = createRoute({
method: "patch",
path: "/api/v1/accounts/update_credentials",
summary: "Update credentials",
description: "Update user credentials",
middleware: [auth(meta.auth, meta.permissions), jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Updated user",
content: {
"application/json": {
schema: User.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
422: {
description: "Validation error",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
500: {
description: "Couldn't edit user",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
const {
display_name,
username,
note,
avatar,
header,
locked,
bot,
discoverable,
source,
fields_attributes,
} = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const self = user.data;
const sanitizedDisplayName = await sanitizedHtmlStrip(
display_name ?? "",
);
const mediaManager = new MediaManager(config);
if (display_name) {
self.displayName = sanitizedDisplayName;
}
if (note && self.source) {
self.source.note = note;
self.note = await contentToHtml({
"text/markdown": {
content: note,
remote: false,
},
});
}
if (source?.privacy) {
self.source.privacy = source.privacy;
}
if (source?.sensitive) {
self.source.sensitive = source.sensitive;
}
if (source?.language) {
self.source.language = source.language;
}
if (username) {
// Check if username is already taken
const existingUser = await User.fromSql(
and(isNull(Users.instanceId), eq(Users.username, username)),
);
if (existingUser) {
return context.json(
{ error: "Username is already taken" },
422,
);
}
self.username = username;
}
if (avatar) {
const { path } = await mediaManager.addFile(avatar);
self.avatar = Attachment.getUrl(path);
}
if (header) {
const { path } = await mediaManager.addFile(header);
self.header = Attachment.getUrl(path);
}
if (locked) {
self.isLocked = locked;
}
if (bot) {
self.isBot = bot;
}
if (discoverable) {
self.isDiscoverable = discoverable;
}
const fieldEmojis: Emoji[] = [];
if (fields_attributes) {
self.fields = [];
self.source.fields = [];
for (const field of fields_attributes) {
// Can be Markdown or plaintext, also has emojis
const parsedName = await contentToHtml(
{
"text/markdown": {
content: field.name,
remote: false,
},
},
undefined,
true,
);
const parsedValue = await contentToHtml(
{
"text/markdown": {
content: field.value,
remote: false,
},
},
undefined,
true,
);
// Parse emojis
const nameEmojis = await Emoji.parseFromText(parsedName);
const valueEmojis = await Emoji.parseFromText(parsedValue);
fieldEmojis.push(...nameEmojis, ...valueEmojis);
// Replace fields
self.fields.push({
key: {
"text/html": {
content: parsedName,
remote: false,
},
},
value: {
"text/html": {
content: parsedValue,
remote: false,
},
},
});
self.source.fields.push({
name: field.name,
value: field.value,
});
}
}
// Parse emojis
const displaynameEmojis =
await Emoji.parseFromText(sanitizedDisplayName);
const noteEmojis = await Emoji.parseFromText(self.note);
self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis]
.map((e) => e.data)
.filter(
// Deduplicate emojis
(emoji, index, self) =>
self.findIndex((e) => e.id === emoji.id) === index,
);
// Connect emojis, if any
// Do it before updating user, so that federation takes that into account
for (const emoji of self.emojis) {
await db
.delete(EmojiToUser)
.where(
and(
eq(EmojiToUser.emojiId, emoji.id),
eq(EmojiToUser.userId, self.id),
),
)
.execute();
await db
.insert(EmojiToUser)
.values({
emojiId: emoji.id,
userId: self.id,
})
.execute();
}
await user.update({
displayName: self.displayName,
username: self.username,
note: self.note,
avatar: self.avatar,
header: self.header,
fields: self.fields,
isLocked: self.isLocked,
isBot: self.isBot,
isDiscoverable: self.isDiscoverable,
source: self.source || undefined,
});
const output = await User.fromId(self.id);
if (!output) {
return context.json({ error: "Couldn't edit user" }, 500);
}
return context.json(output.toApi(), 200);
}),
);

View file

@ -1,55 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/accounts/verify_credentials",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:accounts"],
},
});
const route = createRoute({
method: "get",
path: "/api/v1/accounts/verify_credentials",
summary: "Verify credentials",
description: "Get your own account information",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, (context) => {
// TODO: Add checks for disabled/unverified accounts
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
return context.json(user.toApi(true), 200);
}),
);

View file

@ -1,106 +0,0 @@
import { apiRoute, applyConfig, jsonOrForm } from "@/api";
import { randomString } from "@/math";
import { createRoute } from "@hono/zod-openapi";
import { Application } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
export const meta = applyConfig({
route: "/api/v1/apps",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: false,
},
permissions: {
required: [RolePermissions.ManageOwnApps],
},
});
export const schemas = {
json: z.object({
client_name: z.string().trim().min(1).max(100),
redirect_uris: z
.string()
.min(0)
.max(2000)
.url()
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
scopes: z.string().min(1).max(200),
website: z
.string()
.min(0)
.max(2000)
.url()
.optional()
// Allow empty websites because Traewelling decides to give an empty
// value instead of not providing anything at all
.or(z.literal("").transform(() => undefined)),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/apps",
summary: "Create app",
description: "Create an OAuth2 app",
middleware: [jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "App",
content: {
"application/json": {
schema: z.object({
id: z.string().uuid(),
name: z.string(),
website: z.string().nullable(),
client_id: z.string(),
client_secret: z.string(),
redirect_uri: z.string(),
vapid_link: z.string().nullable(),
}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { client_name, redirect_uris, scopes, website } =
context.req.valid("json");
const app = await Application.insert({
name: client_name || "",
redirectUri: decodeURI(redirect_uris) || "",
scopes: scopes || "read",
website: website || null,
clientId: randomString(32, "base64url"),
secret: randomString(64, "base64url"),
});
return context.json(
{
id: app.id,
name: app.data.name,
website: app.data.website,
client_id: app.data.clientId,
client_secret: app.data.secret,
redirect_uri: app.data.redirectUri,
vapid_link: app.data.vapidKey,
},
200,
);
}),
);

View file

@ -1,75 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Application } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/apps/verify_credentials",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnApps],
},
});
const route = createRoute({
method: "get",
path: "/api/v1/apps/verify_credentials",
summary: "Verify credentials",
description: "Get your own application information",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Application",
content: {
"application/json": {
schema: Application.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user, token } = context.get("auth");
if (!token) {
return context.json({ error: "Unauthorized" }, 401);
}
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const application = await Application.getFromToken(
token.data.accessToken,
);
if (!application) {
return context.json({ error: "Unauthorized" }, 401);
}
return context.json(
{
...application.toApi(),
redirect_uris: application.data.redirectUri,
scopes: application.data.scopes,
},
200,
);
}),
);

View file

@ -1,91 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/blocks",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:blocks"],
},
permissions: {
required: [RolePermissions.ManageOwnBlocks],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(80).default(40),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/blocks",
summary: "Get blocks",
description: "Get users you have blocked",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Blocks",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { max_id, since_id, min_id, limit } = context.req.valid("query");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { objects: blocks, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`,
),
limit,
context.req.url,
);
return context.json(
blocks.map((u) => u.toApi()),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,73 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { generateChallenge } from "@/challenges";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/challenges",
ratelimits: {
max: 10,
duration: 60,
},
auth: {
required: false,
},
permissions: {
required: [],
},
});
const route = createRoute({
method: "post",
path: "/api/v1/challenges",
summary: "Generate a challenge",
description: "Generate a challenge to solve",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Challenge",
content: {
"application/json": {
schema: z.object({
id: z.string(),
algorithm: z.enum(["SHA-1", "SHA-256", "SHA-512"]),
challenge: z.string(),
maxnumber: z.number().optional(),
salt: z.string(),
signature: z.string(),
}),
},
},
},
400: {
description: "Challenges are disabled",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
if (!config.validation.challenges.enabled) {
return context.json(
{ error: "Challenges are disabled in config" },
400,
);
}
const result = await generateChallenge();
return context.json(
{
id: result.id,
...result.challenge,
},
200,
);
}),
);

View file

@ -1,155 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
// Make user 2 an admin
beforeAll(async () => {
await users[1].update({ isAdmin: true });
// Upload one emoji as admin, then one as each user
const response = await fakeRequest("/api/v1/emojis", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
shortcode: "test1",
element: "https://cdn.versia.social/logo.webp",
global: true,
}),
});
expect(response.status).toBe(201);
await fakeRequest("/api/v1/emojis", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
shortcode: "test2",
element: "https://cdn.versia.social/logo.webp",
}),
});
await fakeRequest("/api/v1/emojis", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
shortcode: "test3",
element: "https://cdn.versia.social/logo.webp",
}),
});
});
afterAll(async () => {
await deleteUsers();
await db
.delete(Emojis)
.where(inArray(Emojis.shortcode, ["test1", "test2", "test3"]));
});
describe(meta.route, () => {
test("should return all global emojis", async () => {
const response = await fakeRequest(meta.route, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const emojis = await response.json();
// Should contain test1 and test2, but not test2
expect(emojis).toContainEqual(
expect.objectContaining({
shortcode: "test1",
}),
);
expect(emojis).not.toContainEqual(
expect.objectContaining({
shortcode: "test2",
}),
);
expect(emojis).toContainEqual(
expect.objectContaining({
shortcode: "test3",
}),
);
});
test("should return all user emojis", async () => {
const response = await fakeRequest(meta.route, {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const emojis = await response.json();
// Should contain test1 and test2, but not test3
expect(emojis).toContainEqual(
expect.objectContaining({
shortcode: "test1",
}),
);
expect(emojis).toContainEqual(
expect.objectContaining({
shortcode: "test2",
}),
);
expect(emojis).not.toContainEqual(
expect.objectContaining({
shortcode: "test3",
}),
);
});
test("should return all global emojis when signed out", async () => {
const response = await fakeRequest(meta.route);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const emojis = await response.json();
// Should contain test1, but not test2 or test3
expect(emojis).toContainEqual(
expect.objectContaining({
shortcode: "test1",
}),
);
expect(emojis).not.toContainEqual(
expect.objectContaining({
shortcode: "test2",
}),
);
expect(emojis).not.toContainEqual(
expect.objectContaining({
shortcode: "test3",
}),
);
});
});

View file

@ -1,58 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { Emoji } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
export const meta = applyConfig({
route: "/api/v1/custom_emojis",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: false,
},
permissions: {
required: [RolePermissions.ViewEmojis],
},
});
const route = createRoute({
method: "get",
path: "/api/v1/custom_emojis",
summary: "Get custom emojis",
description: "Get custom emojis",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Emojis",
content: {
"application/json": {
schema: z.array(Emoji.schema),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
const emojis = await Emoji.manyFromSql(
and(
isNull(Emojis.instanceId),
or(
isNull(Emojis.ownerId),
user ? eq(Emojis.ownerId, user.id) : undefined,
),
),
);
return context.json(
emojis.map((emoji) => emoji.toApi()),
200,
);
}),
);

View file

@ -1,166 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
let id = "";
// Make user 2 an admin
beforeAll(async () => {
await users[1].update({ isAdmin: true });
// Create an emoji
const response = await fakeRequest("/api/v1/emojis", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
global: true,
}),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
id = emoji.id;
});
afterAll(async () => {
await deleteUsers();
await db
.delete(Emojis)
.where(inArray(Emojis.shortcode, ["test", "test2", "test3", "test4"]));
});
// /api/v1/emojis/:id (PATCH, DELETE, GET)
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
method: "GET",
});
expect(response.status).toBe(401);
});
test("should return 404 if emoji does not exist", async () => {
const response = await fakeRequest(
meta.route.replace(":id", "00000000-0000-0000-0000-000000000000"),
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
},
);
expect(response.status).toBe(404);
});
test("should not work if the user is trying to update an emoji they don't own", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
shortcode: "test2",
}),
});
expect(response.status).toBe(403);
});
test("should return the emoji", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test");
});
test("should update the emoji", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
shortcode: "test2",
}),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test2");
});
test("should update the emoji with another url, but keep the shortcode", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
element: "https://avatars.githubusercontent.com/u/30842467?v=4",
}),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test2");
});
test("should update the emoji to be non-global", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
global: false,
}),
});
expect(response.ok).toBe(true);
// Check if the other user can see it
const response2 = await fakeRequest("/api/v1/custom_emojis", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
method: "GET",
});
expect(response2.ok).toBe(true);
const emojis = await response2.json();
expect(emojis).not.toContainEqual(expect.objectContaining({ id }));
});
test("should delete the emoji", async () => {
const response = await fakeRequest(meta.route.replace(":id", id), {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "DELETE",
});
expect(response.status).toBe(204);
});
});

View file

@ -1,354 +0,0 @@
import { apiRoute, applyConfig, auth, emojiValidator, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji, db } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/emojis/:id",
ratelimits: {
max: 30,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnEmojis, RolePermissions.ViewEmojis],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
json: z
.object({
shortcode: z
.string()
.trim()
.min(1)
.max(64)
.regex(
emojiValidator,
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
),
element: z
.string()
.trim()
.min(1)
.max(2000)
.url()
.or(z.instanceof(File)),
category: z.string().max(64).optional(),
alt: z.string().max(1000).optional(),
global: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.or(z.boolean())
.optional(),
})
.partial(),
};
const routeGet = createRoute({
method: "get",
path: "/api/v1/emojis/{id}",
summary: "Get emoji data",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Emoji",
content: {
"application/json": {
schema: Emoji.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
403: {
description: "Insufficient credentials",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Emoji not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
const routePatch = createRoute({
method: "patch",
path: "/api/v1/emojis/{id}",
summary: "Modify emoji",
middleware: [auth(meta.auth, meta.permissions), jsonOrForm()],
request: {
params: schemas.param,
body: {
content: {
"application/json": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Emoji modified",
content: {
"application/json": {
schema: Emoji.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
403: {
description: "Insufficient credentials",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Emoji not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
422: {
description: "Invalid form data",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
const routeDelete = createRoute({
method: "delete",
path: "/api/v1/emojis/{id}",
summary: "Delete emoji",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
204: {
description: "Emoji deleted",
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Emoji not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => {
app.openapi(routeGet, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
}
// Check if user is admin
if (
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403,
);
}
return context.json(emoji.toApi(), 200);
});
app.openapi(routePatch, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
}
// Check if user is admin
if (
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403,
);
}
const mediaManager = new MediaManager(config);
const {
global: emojiGlobal,
alt,
category,
element,
shortcode,
} = context.req.valid("json");
if (!user.hasPermission(RolePermissions.ManageEmojis) && emojiGlobal) {
return context.json(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can make an emoji global or not`,
},
401,
);
}
const modified = structuredClone(emoji.data);
if (element) {
// Check of emoji is an image
let contentType =
element instanceof File
? element.type
: await mimeLookup(element);
if (!contentType.startsWith("image/")) {
return context.json(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
422,
);
}
let url = "";
if (element instanceof File) {
const uploaded = await mediaManager.addFile(element);
url = uploaded.path;
contentType = uploaded.uploadedFile.type;
} else {
url = element;
}
modified.url = Attachment.getUrl(url);
modified.contentType = contentType;
}
modified.shortcode = shortcode ?? modified.shortcode;
modified.alt = alt ?? modified.alt;
modified.category = category ?? modified.category;
modified.ownerId = emojiGlobal ? null : user.data.id;
await emoji.update(modified);
return context.json(emoji.toApi(), 200);
});
app.openapi(routeDelete, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
}
// Check if user is admin
if (
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
error: `You cannot delete this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403,
);
}
const mediaManager = new MediaManager(config);
await mediaManager.deleteFileByUrl(emoji.data.url);
await db.delete(Emojis).where(eq(Emojis.id, id));
return context.newResponse(null, 204);
});
});

View file

@ -1,185 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import sharp from "sharp";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(3);
// Make user 2 an admin
beforeAll(async () => {
await users[1].update({ isAdmin: true });
});
afterAll(async () => {
await deleteUsers();
await db
.delete(Emojis)
.where(inArray(Emojis.shortcode, ["test1", "test2", "test3", "test4"]));
});
const createImage = async (name: string): Promise<File> => {
const inputBuffer = await sharp({
create: {
width: 100,
height: 100,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.png()
.toBuffer();
return new File([inputBuffer], name, {
type: "image/png",
});
};
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
}),
});
expect(response.status).toBe(401);
});
describe("Admin tests", () => {
test("should upload a file and create an emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test.png"));
formData.append("global", "true");
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: formData,
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test1");
expect(emoji.url).toContain("/media/proxy");
});
test("should try to upload a non-image", async () => {
const formData = new FormData();
formData.append("shortcode", "test2");
formData.append("element", new File(["test"], "test.txt"));
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: formData,
});
expect(response.status).toBe(422);
});
test("should upload an emoji by url", async () => {
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
shortcode: "test3",
element: "https://cdn.versia.social/logo.webp",
}),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test3");
expect(emoji.url).toContain("/media/proxy/");
});
test("should fail when uploading an already existing emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: formData,
});
expect(response.status).toBe(422);
});
});
describe("User tests", () => {
test("should upload a file and create an emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: formData,
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test4");
expect(emoji.url).toContain("/media/proxy/");
});
test("should fail when uploading an already existing global emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: formData,
});
expect(response.status).toBe(422);
});
test("should create an emoji as another user with the same shortcode", async () => {
const formData = new FormData();
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest(meta.route, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[2].data.accessToken}`,
},
body: formData,
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test4");
expect(emoji.url).toContain("/media/proxy/");
});
});
});

View file

@ -1,178 +0,0 @@
import { apiRoute, applyConfig, auth, emojiValidator, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
import { z } from "zod";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/emojis",
ratelimits: {
max: 30,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnEmojis, RolePermissions.ViewEmojis],
},
});
export const schemas = {
json: z.object({
shortcode: z
.string()
.trim()
.min(1)
.max(64)
.regex(
emojiValidator,
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
),
element: z
.string()
.trim()
.min(1)
.max(2000)
.url()
.or(z.instanceof(File)),
category: z.string().max(64).optional(),
alt: z.string().max(1000).optional(),
global: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.or(z.boolean())
.optional(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/emojis",
summary: "Upload emoji",
description: "Upload an emoji",
middleware: [auth(meta.auth, meta.permissions), jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
},
},
},
responses: {
201: {
description: "Uploaded emoji",
content: {
"application/json": {
schema: Emoji.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
422: {
description: "Invalid data",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { shortcode, element, alt, global, category } =
context.req.valid("json");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
if (!user.hasPermission(RolePermissions.ManageEmojis) && global) {
return context.json(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
},
401,
);
}
// Check if emoji already exists
const existing = await Emoji.fromSql(
and(
eq(Emojis.shortcode, shortcode),
isNull(Emojis.instanceId),
or(eq(Emojis.ownerId, user.id), isNull(Emojis.ownerId)),
),
);
if (existing) {
return context.json(
{
error: `An emoji with the shortcode ${shortcode} already exists, either owned by you or global.`,
},
422,
);
}
let url = "";
// Check of emoji is an image
let contentType =
element instanceof File ? element.type : await mimeLookup(element);
if (!contentType.startsWith("image/")) {
return context.json(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
422,
);
}
if (element instanceof File) {
const mediaManager = new MediaManager(config);
const uploaded = await mediaManager.addFile(element);
url = uploaded.path;
contentType = uploaded.uploadedFile.type;
} else {
url = element;
}
const emoji = await Emoji.insert({
shortcode,
url: Attachment.getUrl(url),
visibleInPicker: true,
ownerId: global ? null : user.id,
category,
contentType,
alt,
});
return context.json(emoji.toApi(), 201);
}),
);

View file

@ -1,90 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Note, Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/favourites",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnLikes],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(80).default(40),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/favourites",
summary: "Get favourites",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Favourites",
content: {
"application/json": {
schema: z.array(Note.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { max_id, since_id, min_id, limit } = context.req.valid("query");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { objects: favourites, link } = await Timeline.getNoteTimeline(
and(
max_id ? lt(Notes.id, max_id) : undefined,
since_id ? gte(Notes.id, since_id) : undefined,
min_id ? gt(Notes.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${Notes.id} AND "Likes"."likerId" = ${user.id})`,
),
limit,
context.req.url,
user?.id,
);
return context.json(
await Promise.all(favourites.map((note) => note.toApi(user))),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,103 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/follow_requests/:account_id/authorize",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnFollows],
},
});
export const schemas = {
param: z.object({
account_id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/follow_requests/{account_id}/authorize",
summary: "Authorize follow request",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Account not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { account_id } = context.req.valid("param");
const account = await User.fromId(account_id);
if (!account) {
return context.json({ error: "Account not found" }, 404);
}
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
account,
user,
);
await oppositeRelationship.update({
requested: false,
following: true,
});
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
account,
);
// Check if accepting remote follow
if (account.isRemote()) {
// Federate follow accept
await user.sendFollowAccept(account);
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,103 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/follow_requests/:account_id/reject",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnFollows],
},
});
export const schemas = {
param: z.object({
account_id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/follow_requests/{account_id}/reject",
summary: "Reject follow request",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Account not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { account_id } = context.req.valid("param");
const account = await User.fromId(account_id);
if (!account) {
return context.json({ error: "Account not found" }, 404);
}
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
account,
user,
);
await oppositeRelationship.update({
requested: false,
following: false,
});
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
account,
);
// Check if rejecting remote follow
if (account.isRemote()) {
// Federate follow reject
await user.sendFollowReject(account);
}
return context.json(foundRelationship.toApi(), 200);
}),
);

View file

@ -1,90 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/follow_requests",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
permissions: {
required: [RolePermissions.ManageOwnFollows],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(80).default(40),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/follow_requests",
summary: "Get follow requests",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Follow requests",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { max_id, since_id, min_id, limit } = context.req.valid("query");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { objects: followRequests, link } =
await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."requested" = true)`,
),
limit,
context.req.url,
);
return context.json(
followRequests.map((u) => u.toApi()),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,36 +0,0 @@
import { apiRoute, applyConfig } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
auth: {
required: false,
},
ratelimits: {
duration: 60,
max: 120,
},
route: "/api/v1/frontend/config",
});
const route = createRoute({
method: "get",
path: "/api/v1/frontend/config",
summary: "Get frontend config",
responses: {
200: {
description: "Frontend config",
content: {
"application/json": {
schema: z.record(z.string(), z.any()).default({}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, (context) => {
return context.json(config.frontend.settings, 200);
}),
);

View file

@ -1,19 +0,0 @@
import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils";
import { meta } from "./extended_description.ts";
// /api/v1/instance/extended_description
describe(meta.route, () => {
test("should return extended description", async () => {
const response = await fakeRequest(meta.route);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
content:
'<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n',
});
});
});

View file

@ -1,51 +0,0 @@
import { apiRoute, applyConfig } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/instance/extended_description",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
const route = createRoute({
method: "get",
path: "/api/v1/instance/extended_description",
summary: "Get extended description",
responses: {
200: {
description: "Extended description",
content: {
"application/json": {
schema: z.object({
updated_at: z.string(),
content: z.string(),
}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.extended_description_path ?? "",
"This is a [Versia](https://versia.pub) server with the default extended description.",
);
return context.json(
{
updated_at: lastModified.toISOString(),
content,
},
200,
);
}),
);

View file

@ -1,130 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { proxyUrl } from "@/response";
import { createRoute, z } from "@hono/zod-openapi";
import { Instance, Note, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import manifest from "~/package.json";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/instance",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
const route = createRoute({
method: "get",
path: "/api/v1/instance",
summary: "Get instance information",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Instance information",
content: {
// TODO: Add schemas for this response
"application/json": {
schema: z.any(),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
// Get software version from package.json
const version = manifest.version;
const statusCount = await Note.getCount();
const userCount = await User.getCount();
const contactAccount = await User.fromSql(
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
);
const knownDomainsCount = await Instance.getCount();
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
| {
forced?: boolean;
providers?: {
id: string;
name: string;
icon: string;
}[];
}
| undefined;
// TODO: fill in more values
return context.json({
approval_required: false,
configuration: {
polls: {
max_characters_per_option:
config.validation.max_poll_option_size,
max_expiration: config.validation.max_poll_duration,
max_options: config.validation.max_poll_options,
min_expiration: config.validation.min_poll_duration,
},
statuses: {
characters_reserved_per_url: 0,
max_characters: config.validation.max_note_size,
max_media_attachments:
config.validation.max_media_attachments,
},
},
description: config.instance.description,
email: "",
invites_enabled: false,
registrations: config.signups.registration,
languages: ["en"],
rules: config.signups.rules.map((r, index) => ({
id: String(index),
text: r,
})),
stats: {
domain_count: knownDomainsCount,
status_count: statusCount,
user_count: userCount,
},
thumbnail: proxyUrl(config.instance.logo),
banner: proxyUrl(config.instance.banner),
title: config.instance.name,
uri: config.http.base_url,
urls: {
streaming_api: "",
},
version: "4.3.0-alpha.3+glitch",
versia_version: version,
// TODO: Put into plugin directly
sso: {
forced: oidcConfig?.forced ?? false,
providers:
oidcConfig?.providers?.map((p) => ({
name: p.name,
icon: proxyUrl(p.icon) || undefined,
id: p.id,
})) ?? [],
},
contact_account: contactAccount?.toApi() || undefined,
} satisfies Record<string, unknown> & {
banner: string | null;
versia_version: string;
sso: {
forced: boolean;
providers: {
id: string;
name: string;
icon?: string;
}[];
};
});
}),
);

View file

@ -1,20 +0,0 @@
import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils";
import { meta } from "./privacy_policy.ts";
// /api/v1/instance/privacy_policy
describe(meta.route, () => {
test("should return privacy policy", async () => {
const response = await fakeRequest(meta.route);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
// This instance has not provided any privacy policy.
content:
"<p>This instance has not provided any privacy policy.</p>\n",
});
});
});

View file

@ -1,49 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/instance/privacy_policy",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
const route = createRoute({
method: "get",
path: "/api/v1/instance/privacy_policy",
summary: "Get instance privacy policy",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Instance privacy policy",
content: {
"application/json": {
schema: z.object({
updated_at: z.string(),
content: z.string(),
}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.privacy_policy_path ?? "",
"This instance has not provided any privacy policy.",
);
return context.json({
updated_at: lastModified.toISOString(),
content,
});
}),
);

View file

@ -1,22 +0,0 @@
import { describe, expect, test } from "bun:test";
import { config } from "~/packages/config-manager/index.ts";
import { fakeRequest } from "~/tests/utils";
import { meta } from "./rules.ts";
// /api/v1/instance/rules
describe(meta.route, () => {
test("should return rules", async () => {
const response = await fakeRequest(meta.route);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual(
config.signups.rules.map((rule, index) => ({
id: String(index),
text: rule,
hint: "",
})),
);
});
});

View file

@ -1,49 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/instance/rules",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
const route = createRoute({
method: "get",
path: "/api/v1/instance/rules",
summary: "Get instance rules",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Instance rules",
content: {
"application/json": {
schema: z.array(
z.object({
id: z.string(),
text: z.string(),
hint: z.string(),
}),
),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, (context) => {
return context.json(
config.signups.rules.map((rule, index) => ({
id: String(index),
text: rule,
hint: "",
})),
);
}),
);

View file

@ -1,20 +0,0 @@
import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils";
import { meta } from "./tos.ts";
// /api/v1/instance/tos
describe(meta.route, () => {
test("should return terms of service", async () => {
const response = await fakeRequest(meta.route);
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
// This instance has not provided any terms of service.
content:
"<p>This instance has not provided any terms of service.</p>\n",
});
});
});

View file

@ -1,49 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
export const meta = applyConfig({
route: "/api/v1/instance/tos",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
const route = createRoute({
method: "get",
path: "/api/v1/instance/tos",
summary: "Get instance terms of service",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Instance terms of service",
content: {
"application/json": {
schema: z.object({
updated_at: z.string(),
content: z.string(),
}),
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.tos_path ?? "",
"This instance has not provided any terms of service.",
);
return context.json({
updated_at: lastModified.toISOString(),
content,
});
}),
);

View file

@ -1,86 +0,0 @@
import { afterAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(1);
const timeline = await getTestStatuses(10, users[0]);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/markers
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(meta.route, {
method: "GET",
});
expect(response.status).toBe(401);
});
test("should return empty markers", async () => {
const response = await fakeRequest(
`${meta.route}?${new URLSearchParams([
["timeline[]", "home"],
["timeline[]", "notifications"],
])}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({});
});
test("should create markers", async () => {
const response = await fakeRequest(
`${meta.route}?${new URLSearchParams({
"home[last_read_id]": timeline[0].id,
})}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
home: {
last_read_id: timeline[0].id,
updated_at: expect.any(String),
version: 1,
},
});
});
test("should return markers", async () => {
const response = await fakeRequest(
`${meta.route}?${new URLSearchParams([
["timeline[]", "home"],
["timeline[]", "notifications"],
])}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
home: {
last_read_id: timeline[0].id,
updated_at: expect.any(String),
version: 1,
},
});
});
});

View file

@ -1,255 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import type { Marker as ApiMarker } from "@versia/client/types";
import { db } from "@versia/kit/db";
import { Markers, RolePermissions } from "@versia/kit/tables";
import { type SQL, and, eq } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/markers",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:blocks"],
},
permissions: {
required: [RolePermissions.ManageOwnAccount],
},
});
export const schemas = {
markers: z.object({
home: z
.object({
last_read_id: z.string().regex(idValidator),
version: z.number(),
updated_at: z.string(),
})
.nullable()
.optional(),
notifications: z
.object({
last_read_id: z.string().regex(idValidator),
version: z.number(),
updated_at: z.string(),
})
.nullable()
.optional(),
}),
};
const routeGet = createRoute({
method: "get",
path: "/api/v1/markers",
summary: "Get markers",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: z.object({
"timeline[]": z
.array(z.enum(["home", "notifications"]))
.max(2)
.or(z.enum(["home", "notifications"]))
.optional(),
}),
},
responses: {
200: {
description: "Markers",
content: {
"application/json": {
schema: schemas.markers,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
const routePost = createRoute({
method: "post",
path: "/api/v1/markers",
summary: "Update markers",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: z.object({
"home[last_read_id]": z.string().regex(idValidator).optional(),
"notifications[last_read_id]": z
.string()
.regex(idValidator)
.optional(),
}),
},
responses: {
200: {
description: "Markers",
content: {
"application/json": {
schema: schemas.markers,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => {
app.openapi(routeGet, async (context) => {
const { "timeline[]": timelines } = context.req.valid("query");
const { user } = context.get("auth");
const timeline = Array.isArray(timelines) ? timelines : [];
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
if (!timeline) {
return context.json({}, 200);
}
const markers: ApiMarker = {
home: undefined,
notifications: undefined,
};
if (timeline.includes("home")) {
const found = await db.query.Markers.findFirst({
where: (marker, { and, eq }): SQL | undefined =>
and(
eq(marker.userId, user.id),
eq(marker.timeline, "home"),
),
});
const totalCount = await db.$count(
Markers,
and(eq(Markers.userId, user.id), eq(Markers.timeline, "home")),
);
if (found?.noteId) {
markers.home = {
last_read_id: found.noteId,
version: totalCount,
updated_at: new Date(found.createdAt).toISOString(),
};
}
}
if (timeline.includes("notifications")) {
const found = await db.query.Markers.findFirst({
where: (marker, { and, eq }): SQL | undefined =>
and(
eq(marker.userId, user.id),
eq(marker.timeline, "notifications"),
),
});
const totalCount = await db.$count(
Markers,
and(
eq(Markers.userId, user.id),
eq(Markers.timeline, "notifications"),
),
);
if (found?.notificationId) {
markers.notifications = {
last_read_id: found.notificationId,
version: totalCount,
updated_at: new Date(found.createdAt).toISOString(),
};
}
}
return context.json(markers, 200);
});
app.openapi(routePost, async (context) => {
const {
"home[last_read_id]": homeId,
"notifications[last_read_id]": notificationsId,
} = context.req.valid("query");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const markers: ApiMarker = {
home: undefined,
notifications: undefined,
};
if (homeId) {
const insertedMarker = (
await db
.insert(Markers)
.values({
userId: user.id,
timeline: "home",
noteId: homeId,
})
.returning()
)[0];
const totalCount = await db.$count(
Markers,
and(eq(Markers.userId, user.id), eq(Markers.timeline, "home")),
);
markers.home = {
last_read_id: homeId,
version: totalCount,
updated_at: new Date(insertedMarker.createdAt).toISOString(),
};
}
if (notificationsId) {
const insertedMarker = (
await db
.insert(Markers)
.values({
userId: user.id,
timeline: "notifications",
notificationId: notificationsId,
})
.returning()
)[0];
const totalCount = await db.$count(
Markers,
and(
eq(Markers.userId, user.id),
eq(Markers.timeline, "notifications"),
),
);
markers.notifications = {
last_read_id: notificationsId,
version: totalCount,
updated_at: new Date(insertedMarker.createdAt).toISOString(),
};
}
return context.json(markers, 200);
});
});

View file

@ -1,167 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 10,
duration: 60,
},
route: "/api/v1/media/:id",
auth: {
required: true,
oauthPermissions: ["write:media"],
},
permissions: {
required: [RolePermissions.ManageOwnMedia],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
form: z.object({
thumbnail: z.instanceof(File).optional(),
description: z
.string()
.max(config.validation.max_media_description_size)
.optional(),
focus: z.string().optional(),
}),
};
const routePut = createRoute({
method: "put",
path: "/api/v1/media/{id}",
summary: "Update media",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
body: {
content: {
"multipart/form-data": {
schema: schemas.form,
},
},
},
},
responses: {
204: {
description: "Media updated",
content: {
"application/json": {
schema: Attachment.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "Media not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
const routeGet = createRoute({
method: "get",
path: "/api/v1/media/{id}",
summary: "Get media",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Media",
content: {
"application/json": {
schema: Attachment.schema,
},
},
},
404: {
description: "Media not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => {
app.openapi(routePut, async (context) => {
const { id } = context.req.valid("param");
const attachment = await Attachment.fromId(id);
if (!attachment) {
return context.json({ error: "Media not found" }, 404);
}
const { description, thumbnail } = context.req.valid("form");
let thumbnailUrl = attachment.data.thumbnailUrl;
const mediaManager = new MediaManager(config);
if (thumbnail) {
const { path } = await mediaManager.addFile(thumbnail);
thumbnailUrl = Attachment.getUrl(path);
}
const descriptionText = description || attachment.data.description;
if (
descriptionText !== attachment.data.description ||
thumbnailUrl !== attachment.data.thumbnailUrl
) {
await attachment.update({
description: descriptionText,
thumbnailUrl,
});
return context.json(attachment.toApi(), 204);
}
return context.json(attachment.toApi(), 204);
});
app.openapi(routeGet, async (context) => {
const { id } = context.req.valid("param");
const attachment = await Attachment.fromId(id);
if (!attachment) {
return context.json({ error: "Media not found" }, 404);
}
return context.json(attachment.toApi(), 200);
});
});

View file

@ -1,146 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import sharp from "sharp";
import { z } from "zod";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
ratelimits: {
max: 10,
duration: 60,
},
route: "/api/v1/media",
auth: {
required: true,
oauthPermissions: ["write:media"],
},
permissions: {
required: [RolePermissions.ManageOwnMedia],
},
});
export const schemas = {
form: z.object({
file: z.instanceof(File),
thumbnail: z.instanceof(File).optional(),
description: z
.string()
.max(config.validation.max_media_description_size)
.optional(),
focus: z.string().optional(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/media",
summary: "Upload media",
middleware: [auth(meta.auth, meta.permissions)],
request: {
body: {
content: {
"multipart/form-data": {
schema: schemas.form,
},
},
},
},
responses: {
200: {
description: "Attachment",
content: {
"application/json": {
schema: Attachment.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
413: {
description: "File too large",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
415: {
description: "Disallowed file type",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { file, thumbnail, description } = context.req.valid("form");
if (file.size > config.validation.max_media_size) {
return context.json(
{
error: `File too large, max size is ${config.validation.max_media_size} bytes`,
},
413,
);
}
if (
config.validation.enforce_mime_types &&
!config.validation.allowed_mime_types.includes(file.type)
) {
return context.json({ error: "Disallowed file type" }, 415);
}
const sha256 = new Bun.SHA256();
const isImage = file.type.startsWith("image/");
const metadata = isImage
? await sharp(await file.arrayBuffer()).metadata()
: null;
const mediaManager = new MediaManager(config);
const { path, blurhash } = await mediaManager.addFile(file);
const url = Attachment.getUrl(path);
let thumbnailUrl = "";
if (thumbnail) {
const { path } = await mediaManager.addFile(thumbnail);
thumbnailUrl = Attachment.getUrl(path);
}
const newAttachment = await Attachment.insert({
url,
thumbnailUrl,
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
mimeType: file.type,
description: description ?? "",
size: file.size,
blurhash: blurhash ?? undefined,
width: metadata?.width ?? undefined,
height: metadata?.height ?? undefined,
});
// TODO: Add job to process videos and other media
return context.json(newAttachment.toApi(), 200);
}),
);

View file

@ -1,83 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { users, tokens, deleteUsers } = await getTestUsers(3);
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/mute`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
});
// /api/v1/mutes
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", users[1].id),
{
method: "GET",
},
);
expect(response.status).toBe(401);
});
test("should return mutes", async () => {
const response = await fakeRequest(meta.route, {
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: users[1].id,
}),
]),
);
});
test("should return mutes after unmute", async () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/unmute`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const response2 = await fakeRequest(meta.route, {
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response2.status).toBe(200);
const body = await response2.json();
expect(body).toEqual([]);
});
});

View file

@ -1,89 +0,0 @@
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/mutes",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:mutes"],
},
permissions: {
required: [RolePermissions.ManageOwnMutes],
},
});
export const schemas = {
query: z.object({
max_id: z.string().regex(idValidator).optional(),
since_id: z.string().regex(idValidator).optional(),
min_id: z.string().regex(idValidator).optional(),
limit: z.coerce.number().int().min(1).max(80).default(40),
}),
};
const route = createRoute({
method: "get",
path: "/api/v1/mutes",
summary: "Get muted users",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Muted users",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { max_id, since_id, limit, min_id } = context.req.valid("query");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const { objects: mutes, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`,
),
limit,
context.req.url,
);
return context.json(
mutes.map((u) => u.toApi()),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,73 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./dismiss.ts";
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = [];
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
notifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(notifications.length).toBe(1);
});
afterAll(async () => {
await deleteUsers();
});
// /api/v1/notifications/:id/dismiss
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
meta.route.replace(":id", notifications[0].id),
{
method: "POST",
},
);
expect(response.status).toBe(401);
});
test("should dismiss notification", async () => {
const response = await fakeRequest(
meta.route.replace(":id", notifications[0].id),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
});
test("should not display dismissed notification", async () => {
const response = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200);
const output = await response.json();
expect(output.length).toBe(0);
});
});

View file

@ -1,73 +0,0 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Notification } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
route: "/api/v1/notifications/:id/dismiss",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["write:notifications"],
},
permissions: {
required: [RolePermissions.ManageOwnNotifications],
},
});
export const schemas = {
param: z.object({
id: z.string().uuid(),
}),
};
const route = createRoute({
method: "post",
path: "/api/v1/notifications/{id}/dismiss",
summary: "Dismiss notification",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
},
responses: {
200: {
description: "Notification dismissed",
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const notification = await Notification.fromId(id);
if (!notification) {
return context.json({ error: "Notification not found" }, 404);
}
await notification.update({
dismissed: true,
});
return context.newResponse(null, 200);
}),
);

Some files were not shown because too many files have changed in this diff Show more