Compare commits

...

208 commits
0.1.0 ... main

Author SHA1 Message Date
April John bcff0a4eec argh
Some checks failed
Docker / build (push) Failing after 5s
Nix Flake actions / check (push) Failing after 5s
2025-02-11 14:51:18 +01:00
April John d12d618da4 fix scanner 2025-02-11 14:33:33 +01:00
April John 6752a93d2f change to flakehub cache 2025-02-11 14:30:13 +01:00
April John 3dadef436e add docker image scanning 2025-02-11 14:21:33 +01:00
April John 93d4b7af0d update nix flake 2025-02-11 14:11:20 +01:00
April John e5851e2c9b format files 2025-02-11 14:03:32 +01:00
April John 23bd522ade w 2025-02-03 15:42:25 +01:00
April John 33475b5bb9 aaa 2025-02-03 15:25:07 +01:00
April John 4510f31dea add debug print 2025-02-03 15:19:49 +01:00
April John d1dfa00c6b fix? 2025-02-03 15:15:43 +01:00
April John eb5374967c fix? 2025-02-03 15:02:58 +01:00
April John 98cf521ae0 fix links 2025-02-03 14:54:10 +01:00
April John 14e4402e7a add favicon 2025-02-03 14:15:19 +01:00
April John c47575eb8d aaa 2025-02-03 14:00:28 +01:00
April John 9543fe939f arson 2025-02-03 13:56:29 +01:00
April John 375f4467b2 debug print authr 2025-02-03 13:39:29 +01:00
April John 3aec94e528 fix duplicate insert 2025-02-03 12:49:10 +01:00
April John 2401a6f42d this is how easy it is to implement post federation? 2025-02-03 11:34:22 +01:00
April John 44108cb2d3 make category enum lowercase 2024-11-24 17:09:03 +01:00
April John d683edafb8 serialise better 2024-11-24 17:06:05 +01:00
April John 3d7dffde5a add auth 2024-11-24 16:48:45 +01:00
April John f7de3bc3e1 thing 2024-11-24 16:44:43 +01:00
April John 9c33416cec misspell discovered 2024-11-24 16:24:07 +01:00
April John 227b6c8a44 test 2024-11-24 16:05:15 +01:00
April John a5051fd252 @CPlusPatch needs to fix for this to work 2024-11-23 23:56:23 +01:00
April John 3f26d5d731 oh god this code 2024-11-23 23:53:38 +01:00
April John 6fe3509718 what happens if i just 2024-11-23 23:40:52 +01:00
April John 6f4451c3a9 kurwa header 2024-11-23 22:40:58 +01:00
April John 366c2b7e81 aaa 2024-11-23 22:28:13 +01:00
April John 35905c8b12 awa 2024-11-23 22:04:40 +01:00
April John 5a53851cb1 awa 2024-11-23 21:35:37 +01:00
April John a9401f4d8d aa 2024-11-23 21:17:15 +01:00
April John 592f0cecbd fix collectuins 2024-11-23 14:31:02 +01:00
April John 1a9eca3e90 aaw 2024-11-22 22:46:06 +01:00
April John f405137ef6 stoopd 2024-11-22 22:40:15 +01:00
April John 4d11fd5738 aaa 2024-11-22 22:36:35 +01:00
April John 44e9e0409d wicked 2024-11-22 22:36:32 +01:00
April John ffa14e2881 log fed 2024-11-22 22:30:15 +01:00
April John 8a61240d3b fix emojis 2024-11-22 22:19:52 +01:00
April John bf2377a68d dedub array first 2024-11-22 22:09:58 +01:00
April John d275165b97 AP -> Versia note fed 2024-11-22 22:08:25 +01:00
April John 012079cd9b fix doublicate check 2024-11-22 19:02:28 +01:00
aprilthepink 21364237c5 fix: make likes opt 2024-11-19 23:28:29 +01:00
aprilthepink c72763c4b1 feat: initial port to WD4 2024-11-19 22:15:19 +01:00
aprilthepink 084e85e111 flake: update sources 2024-11-19 20:57:49 +01:00
April John 20eede0c08 fix: change some thing to point to API_DOMAIN 2024-08-28 16:24:39 +02:00
April John 0b4574b2d1 feat: rename to Versia 2024-08-28 15:24:22 +02:00
emily 29d8fc718a
ci: accept-flake-config in workflows 2024-08-25 19:45:24 +02:00
emily 8015abaeb2
feat: Add nix binary cache 2024-08-25 19:27:20 +02:00
emily 4e277ecbb3
flake.lock: Update
Flake lock file updates:

• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/9227223f6d922fee3c7b190b2cc238a99527bbb7?narHash=sha256-pQMhCCHyQGRzdfAkdJ4cIWiw%2BJNuWsTX7f0ZYSyz0VY%3D' (2024-07-03)
  → 'github:hercules-ci/flake-parts/8471fe90ad337a8074e957b69ca4d0089218391d?narHash=sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC%2Bx4%3D' (2024-08-01)
• Updated input 'flake-parts/nixpkgs-lib':
    '5daf051448.tar.gz?narHash=sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI%3D' (2024-07-01)
  → 'a5d394176e.tar.gz?narHash=sha256-uFf2QeW7eAHlYXuDktm9c25OxOyCoUOQmh5SZ9amE5Q%3D' (2024-08-01)
• Updated input 'naersk/nixpkgs':
    'github:NixOS/nixpkgs/48bacf585a51d953def8bff32087970f273052e2?narHash=sha256-NMDotPxtCNvmRnUo/YuxNOpN8%2BUMONBlNBnRFsGHADQ%3D' (2024-07-27)
  → 'github:NixOS/nixpkgs/ae815cee91b417be55d43781eb4b73ae1ecc396c?narHash=sha256-zRkDV/nbrnp3Y8oCADf5ETl1sDrdmAW6/bBVJ8EbIdQ%3D' (2024-08-23)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/5ad6a14c6bf098e98800b091668718c336effc95?narHash=sha256-Sb1jlyRO%2BN8jBXEX9Pg9Z1Qb8Bw9QyOgLDNMEpmjZ2M%3D' (2024-07-25)
  → 'github:nixos/nixpkgs/c374d94f1536013ca8e92341b540eba4c22f9c62?narHash=sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh%2BaRKoCdaAv5fiO0%3D' (2024-08-21)
• Updated input 'treefmt-nix':
    'github:numtide/treefmt-nix/8db8970be1fb8be9c845af7ebec53b699fe7e009?narHash=sha256-6Pqa0bi5nV74IZcENKYRToRNM5obo1EQ%2B3ihtunJ014%3D' (2024-07-23)
  → 'github:numtide/treefmt-nix/070f834771efa715f3e74cd8ab93ecc96fabc951?narHash=sha256-kKJtaiU5Ou%2Be/0Qs7SICXF22DLx4V/WhG1P6%2Bk4yeOE%3D' (2024-08-22)
2024-08-25 19:23:21 +02:00
emily f74855ce4c
refactor: move nixos module to its own repo 2024-08-25 19:19:12 +02:00
aprilthepink faa9ca4270 fix: fmt 2024-08-03 15:30:34 +02:00
aprilthepink 42abaa0779 feat: sync follow to lysand 2024-08-03 15:30:25 +02:00
aprilthepink 86c6df24a7 awa? 2024-08-03 12:09:02 +02:00
aprilthepink 88c80a9045 eeee 2024-08-03 11:49:16 +02:00
aprilthepink 5cc9b91a15 aaaa 2024-08-03 11:43:04 +02:00
aprilthepink 12581254d2 awa 2024-08-03 11:34:46 +02:00
aprilthepink 8ee5eb52bd awawaw 2024-08-03 11:34:43 +02:00
aprilthepink 1f44d79f2b fix: legacy code check 2024-08-03 11:00:02 +02:00
aprilthepink 6997e4d994 10h of debugging, one issue found 2024-08-03 10:19:20 +02:00
aprilthepink 42c9c19509 awa? 2024-08-03 08:56:05 +02:00
aprilthepink bbb613f6de debug msg 2024-08-03 08:31:21 +02:00
aprilthepink 92c9ad80b4 fawa 2024-08-03 08:23:46 +02:00
aprilthepink dde8692a5d fix: accept any domain cuz bridge 2024-08-03 08:15:54 +02:00
aprilthepink 2910702c19 fix: actor on follow 2024-08-03 08:10:22 +02:00
aprilthepink f7be000097 fix: awa 2024-08-03 08:05:17 +02:00
aprilthepink 01ba54328e bump activitypub_federation 2024-08-03 07:46:09 +02:00
aprilthepink 8ba0dfe2dc fix: println instead of print 2024-08-03 07:37:18 +02:00
aprilthepink 32c3dad71e baaaa 2024-08-03 07:18:45 +02:00
aprilthepink 8831275097 fix: pls work 2024-08-03 07:04:31 +02:00
aprilthepink 566c444aed fix: correct method call in lysand/http 2024-08-03 06:50:03 +02:00
aprilthepink 05c8acc71e fix: add lysand inbox to actix 2024-08-03 06:41:38 +02:00
aprilthepink f480bc068c feat: follow lysand -> ap 2024-08-03 06:25:16 +02:00
aprilthepink b666d339f2 fmt: codebase 2024-08-03 04:14:38 +02:00
aprilthepink 12dc5e89d0 fix: change prometheus namespace to activitypub_bridge 2024-08-03 04:14:12 +02:00
April John ba901981f0 update 2024-08-03 03:49:07 +02:00
April John b495fbe80e
update: flake deps 2024-07-27 16:27:08 +02:00
April John 5949bb9bf3
fix: formatting & DB 2024-07-27 16:24:04 +02:00
April John 2ffc66f412 fix 2024-07-26 18:11:08 +02:00
April John 65f1f543a6 fix 2024-07-26 18:01:24 +02:00
April John 5d3fa040cf fix 2024-07-26 17:33:07 +02:00
April John 60338e8a1c fix 2024-07-26 17:24:33 +02:00
April John 483212c7fa
fix: AP user federation behavior 2024-07-26 00:26:13 +02:00
April John c2a79b128b
feat: follow rels 2024-07-26 00:25:49 +02:00
aprilthepink e8b78e4d8d fix: migration issues 2024-07-21 21:48:49 +02:00
aprilthepink 9ce95c4b7a fix: ap json for default user 2024-07-21 21:27:30 +02:00
aprilthepink 451cf8941a fix: migration stuff 2024-07-21 21:04:13 +02:00
aprilthepink 4429aa380a oopsie i was silly 2024-07-21 20:56:53 +02:00
aprilthepink 1a741c6420 feat: add missing fields on AP users 2024-07-21 19:37:08 +02:00
aprilthepink 692e4bff22 fix: resolve webfinger correctly 2024-07-17 14:21:32 +02:00
aprilthepink 071f6dcbd8 fix: correct urls in AP user 2024-07-17 13:17:16 +02:00
aprilthepink d33de465bf fix: add AP context 2024-07-17 02:45:01 +02:00
aprilthepink 572e86624e fix: im having a stroke it works 2024-07-17 02:38:49 +02:00
aprilthepink 3f9fb9e67a fix awa 2024-07-17 02:34:18 +02:00
aprilthepink 6dc2597ad9 fix aaa 2024-07-17 02:29:51 +02:00
aprilthepink de402bf3c7 awa 2024-07-17 02:25:33 +02:00
aprilthepink d99d47a9d0 fix: debug print 2024-07-17 02:14:53 +02:00
aprilthepink b1c78822de feat: AP webfinger 2024-07-17 02:08:51 +02:00
aprilthepink f22cae919f fix: allow post parsing again 2024-07-17 01:24:04 +02:00
aprilthepink 81ef6f8c5f fix: awa 2024-07-17 01:13:10 +02:00
aprilthepink 45a7555db3 fix: lysand ap fetch UUID change and wrong URI 2024-07-17 01:07:23 +02:00
aprilthepink b5b4144f39 fix: replace lysand user key with dummy 2024-07-17 00:09:31 +02:00
aprilthepink d86abbb97d fix: add indexable to Lysand user and default it to false 2024-07-16 23:22:52 +02:00
aprilthepink 474b57652b fix: make sensitive in AP optional bc AP 2024-07-16 20:04:53 +02:00
aprilthepink f5e4092bca feat: debug script 2024-07-16 19:56:40 +02:00
aprilthepink a840feecb0 feat: basic lysand -> ap fetching api 2024-07-16 19:35:04 +02:00
aprilthepink b1af17c5d2 fix: add mentions to TO on note import 2024-06-27 17:29:55 +02:00
aprilthepink b29ca0cadc fix: add mentioned user to TO in test 2024-06-27 17:14:37 +02:00
aprilthepink 25d857c0f3 format 2024-06-27 09:48:47 +02:00
aprilthepink b278cd8a27 fix: shitcode 2024-06-27 05:55:43 +02:00
aprilthepink 29ba9a8b7a fix: allow api domain to have users 2024-06-27 05:49:35 +02:00
aprilthepink 505809b117 fix: me please 2024-06-27 05:44:40 +02:00
aprilthepink 02c1c4f9f8 fix: ruse only one UUID in test instead of two different 2024-06-27 05:31:58 +02:00
aprilthepink b52024d726 Merge branch 'test1' 2024-06-27 05:17:03 +02:00
aprilthepink dc4afd8411 feat: meow meow akkoma? 2024-06-27 05:13:38 +02:00
April John 6b6d36b30b
feat: basic AP <-> lysand conversion (#4)
* feat: Update API domain variable name

* save changes, fake commit

* feat: function to receive lysand note

* [feat]: lysand to ap for posts and users

* feat: Add .env file to gitignore and update dependencies

The commit adds the `.env` file to the `.gitignore` and updates the dependencies in the `Cargo.toml` and `Cargo.lock` files. This change ensures that sensitive environment variables are not committed to the repository and keeps the dependencies up to date.

* feat: Add db_post_from_url function

The commit adds the `db_post_from_url` function to the `conversion.rs` file. This function retrieves a post from the database based on a given URL. If the post is not found in the database, it fetches the post from the URL, saves it to the database, and returns the post. This change enhances the functionality of the codebase by providing a convenient way to retrieve and store posts.

* feat: Update dependencies and add async-recursion crate

* fix: Refactor lysand note handling in conversion.rs

The commit refactors the lysand note handling in the `conversion.rs` file. It updates the `receive_lysand_note` function to properly handle quoting and replying to notes. This change improves the functionality and readability of the codebase.

* fix: use example as default user

* me oopid

* fix: Refactor lysand note handling in conversion.rs

* fix: fix post printing

* fix: Refactor lysand note handling in conversion.rs

* fix: Refactor lysand note handling in conversion.rs

* fix: make nix-bootstrap executable

* fix: remove unused linux only import

* fix: make person stop screaming at me :(

* feat: remove test code

* fix: update deps

* fix: rm build.rs fully

* feat: standard user to apservice

* fix: format files
2024-06-18 01:56:25 +00:00
aprilthepink cd6ff024e4 fix: format files 2024-06-18 03:43:59 +02:00
aprilthepink 489a216aca feat: standard user to apservice 2024-06-18 03:34:37 +02:00
aprilthepink c601fb03eb fix: rm build.rs fully 2024-06-18 01:52:47 +02:00
aprilthepink 3359da6139 fix: update deps 2024-06-18 01:48:17 +02:00
aprilthepink b6ca94b809 feat: remove test code 2024-06-18 01:46:24 +02:00
aprilthepink a90a2695a9 fix: make person stop screaming at me :( 2024-06-17 23:50:31 +02:00
aprilthepink ef89602d6f fix: remove unused linux only import 2024-06-17 23:49:40 +02:00
aprilthepink b08e3459b9 fix: make nix-bootstrap executable 2024-06-17 22:47:21 +02:00
aprilthepink 766819346f fix: Refactor lysand note handling in conversion.rs 2024-06-17 22:39:03 +02:00
aprilthepink 03f6663768 fix: Refactor lysand note handling in conversion.rs 2024-06-17 22:33:30 +02:00
aprilthepink 56b008fcae fix: fix post printing 2024-06-17 21:58:00 +02:00
aprilthepink da40e5b5e6 fix: Refactor lysand note handling in conversion.rs 2024-06-17 21:55:42 +02:00
aprilthepink e8d716c7a5 me oopid 2024-06-17 21:50:33 +02:00
aprilthepink a6c0fc4755 fix: use example as default user 2024-06-17 21:47:48 +02:00
aprilthepink 21a3447a84 fix: Refactor lysand note handling in conversion.rs
The commit refactors the lysand note handling in the `conversion.rs` file. It updates the `receive_lysand_note` function to properly handle quoting and replying to notes. This change improves the functionality and readability of the codebase.
2024-06-17 21:44:29 +02:00
aprilthepink 9e148fe77f feat: Update dependencies and add async-recursion crate 2024-06-17 21:40:25 +02:00
aprilthepink 1588b2e46c feat: Add db_post_from_url function
The commit adds the `db_post_from_url` function to the `conversion.rs` file. This function retrieves a post from the database based on a given URL. If the post is not found in the database, it fetches the post from the URL, saves it to the database, and returns the post. This change enhances the functionality of the codebase by providing a convenient way to retrieve and store posts.
2024-06-17 21:35:21 +02:00
aprilthepink 15bba70d2a feat: Add .env file to gitignore and update dependencies
The commit adds the `.env` file to the `.gitignore` and updates the dependencies in the `Cargo.toml` and `Cargo.lock` files. This change ensures that sensitive environment variables are not committed to the repository and keeps the dependencies up to date.
2024-06-17 19:52:51 +02:00
aprilthepink 1174f92915 [feat]: lysand to ap for posts and users 2024-06-15 02:06:01 +02:00
April John bcab516a1f
feat: function to receive lysand note 2024-05-21 04:59:47 +02:00
April John e42baf51e4
save changes, fake commit 2024-05-19 07:17:13 +02:00
aprilthepink b65a51a1ef feat: Update API domain variable name 2024-05-17 13:24:41 +02:00
aprilthepink 14322c961f feat: Update API domain variable name
The commit updates the variable name for the API domain from `FEDERATED_DOMAIN` to `API_DOMAIN` in the `src/main.rs` file. This change improves clarity and consistency in the codebase.
2024-05-17 11:30:10 +02:00
aprilthepink c7798ac5e4 fix: docker build 2024-05-17 11:06:16 +02:00
Gaspard Wierzbinski b8bac014a2
feat(build): Add automatic Dockerbuilds 2024-05-15 15:36:22 -10:00
aprilthepink 15c30d2f30 feat: lysand extension types 2024-05-13 22:08:23 +02:00
aprilthepink eec3a037bb feat: lysand user 2024-05-13 21:31:33 +02:00
April John 9e6445d192
fix: update gh actions 2024-05-13 07:57:52 +02:00
April John 470f1f7eae
fix: format 2024-05-09 23:10:36 +02:00
April John 946804968e
feat: Lysand User struct 2024-05-09 23:09:57 +02:00
April John 5db0f6a37a
feat: oci image 2024-05-09 23:09:56 +02:00
Jesse Wierzbinski 42133b257b
docs: 📝 Update README and Docker instructions 2024-05-06 09:01:21 -10:00
April John 6af5ac94cd
awawwwa 2024-05-06 16:25:49 +02:00
April John 1340f00167
fix: simplify nix module 2024-05-06 16:12:40 +02:00
aprilthepink 4c3d06b686 fix: post::Model's insert method to remove test code 2024-05-05 18:51:45 +02:00
aprilthepink 503e433af3 Refactor import statements in person.rs 2024-05-05 18:24:59 +02:00
aprilthepink 36d10f774a feat: Add generate_follow_accept_id function to utils.rs and follow_manually endpoint to main.rs 2024-05-05 18:18:39 +02:00
aprilthepink 3ebe83c52f fix: Todo into_json method in post::Model 2024-05-05 17:17:28 +02:00
aprilthepink ce5f97ac33 feat: Refactor migration and follow code 2024-05-05 17:05:09 +02:00
aprilthepink 468371d43d Add user follow relation 2024-05-05 16:26:48 +02:00
aprilthepink 9acf6578bf format files 2024-05-04 19:07:34 +02:00
aprilthepink 07337ffe2f Update post_manually function to include path in content 2024-05-04 18:04:26 +02:00
aprilthepink d115bdda0f Add FEDERATION_CONFIG.set(data.clone()) in main.rs 2024-05-04 18:02:02 +02:00
aprilthepink 9529ac26d1 Refactor post_manually function to use GET instead of POST in main.rs 2024-05-04 17:59:26 +02:00
aprilthepink 8ac9f7bd4b Add a post fuct 2024-05-04 17:54:11 +02:00
aprilthepink 4c020229e4 Add User entity to person.rs 2024-05-04 01:26:10 +02:00
aprilthepink a06414f0bd Refactor Note struct in post.rs to remove url and published fields 2024-05-04 00:04:34 +02:00
aprilthepink 19291bc107 Add url, published, and sensitive fields to Note struct in post.rs 2024-05-04 00:00:26 +02:00
aprilthepink 7266790889 Fix formatting issues and add signed fetch actor in main.rs 2024-05-03 23:42:42 +02:00
aprilthepink e59bcfbc60 Update database.rs to include LOCAL_USER_NAME in filter 2024-05-03 20:50:43 +02:00
aprilthepink 89e537be64 update error msg 2024-05-03 20:12:56 +02:00
aprilthepink 571f65e2fa fix: let eepy 2024-05-03 19:29:12 +02:00
aprilthepink 09874fce92 fix: let module setup be correct 2024-05-03 19:23:17 +02:00
aprilthepink 9db98ab6d9 fix: let module setup be correct 2024-05-03 19:17:52 +02:00
aprilthepink 02c5d7770d fix: let module setup be correct 2024-05-03 19:16:31 +02:00
aprilthepink f7dae1cf54 fix 2024-05-03 18:06:53 +02:00
aprilthepink 32ba6d0d1e
chore: format 2024-05-03 02:19:10 +02:00
aprilthepink 22547e64ce
feat: nix module 2024-05-03 02:06:40 +02:00
aprilthepink 0a8d3439d4
chore: update gh actions 2024-05-03 01:00:04 +02:00
Gaspard Wierzbinski afe7212aff
chore: Update nix-flake.yml to use latest checkout action 2024-05-02 12:33:41 -10:00
aprilthepink dcaba02c21
unfinished nix module setup 2024-05-03 00:22:08 +02:00
aprilthepink 1207102332 [fix]: update nixpkgs & naersk inputs 2024-04-20 17:38:35 +02:00
aprilthepink d9f6123942 fix formating 2024-04-20 17:35:40 +02:00
aprilthepink c8077e5f29 Add created_at field to user and person models 2024-04-18 19:14:06 +02:00
aprilthepink 8a4f5b795c Add follower and following count fields to user and person models 2024-04-18 19:09:10 +02:00
aprilthepink b5c9c2b9e6 awa 2024-04-18 19:05:22 +02:00
aprilthepink 98732a5c06 [feat]diagnostic messages 2024-04-18 19:01:14 +02:00
aprilthepink 775c1198c3 awa 2024-04-18 18:30:15 +02:00
aprilthepink 831473c940 add logging 2024-04-18 18:26:41 +02:00
aprilthepink 8afd6a9468 bug found? 2024-04-18 04:27:22 +02:00
aprilthepink c160218de6 eee 2024-04-18 04:16:33 +02:00
aprilthepink 7c144f6e60 eee 2024-04-18 04:14:19 +02:00
aprilthepink fa3e4634cb fomat 2024-04-18 04:03:52 +02:00
aprilthepink 4d4e3ed794 [feat] db code? 2024-04-18 03:41:52 +02:00
aprilthepink 8469b236a7 [feat] first entities 2024-04-18 02:07:19 +02:00
aprilthepink ff728ef6f3 [fix] make third mig sqlite compatible 2024-04-18 01:59:16 +02:00
aprilthepink 99304aa53a [feat]: initial migration 2024-04-18 01:47:18 +02:00
aprilthepink ed912004b8 [fix]: update workflow 2024-04-17 22:03:00 +02:00
aprilthepink db996b56a9 [fix]: format tree 2024-04-17 22:00:40 +02:00
aprilthepink 36af08918e [fix]: update lock 2024-04-17 21:49:19 +02:00
April John 3333c4133b
[fix] flake 2024-04-17 18:45:48 +00:00
April John 889f3e68ba
[feat] sample seaorm code 2024-04-17 18:25:22 +00:00
April John e239863cdb
[feat] use naersk to build package in flake 2024-04-17 18:12:27 +00:00
April John 6ec80a7d6d
meow 2024-04-16 20:44:08 +00:00
April John f3e010a321
[refactor]: remove redis code 2024-04-16 17:53:24 +00:00
April John 10495a070c
[fix]: rm nix from devc 2024-04-16 17:39:43 +00:00
April John 484c5d9bf6
[fix] use latest feature versions 2024-04-16 17:06:52 +00:00
April John 14f21c997e
[feat] dev container config 2024-04-16 17:01:16 +00:00
April John 9dd37f483a [dependency]: serde_json added 2024-04-15 01:20:44 +02:00
April John 1bbb512932 big oof no msg 2024-04-15 01:17:11 +02:00
April John 22d34cf504 feat: redis caching 2024-04-15 01:07:15 +02:00
April John cc629c1414 make markdown changelog 2024-04-15 00:06:13 +02:00
53 changed files with 8453 additions and 729 deletions

View file

@ -0,0 +1,6 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/rust:latest": {}
}
}

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
DATABASE_URL="sqlite:///home/aprl/Documents/versia-ap-layer/db.sqlite?mode=rwc"
LYSAND_DOMAIN="versia.social"
API_DOMAIN="ap.versia.social"
RUST_LOG="debug"

72
.github/workflows/docker-publish.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Docker
on:
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
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
- 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 }}
- uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: accept-flake-config = true
- uses: DeterminateSystems/flakehub-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Build docker package
run: nix build .#ociImage
- name: Load Docker image
run: docker load < result
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'ghcr.io/${{ env.IMAGE_NAME }}:main'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
- name: Push image to registry
if: github.event_name != 'pull_request'
run: docker push ghcr.io/$IMAGE_NAME -a

View file

@ -10,14 +10,21 @@ on:
jobs:
check:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
id-token: "write"
contents: "read"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
extra-conf: accept-flake-config = true
- uses: DeterminateSystems/flakehub-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Run `nix build`
- name: Build default package
run: nix build .
- name: Check flakes
run: nix flake check
- name: Build migrations
run: nix build .#ls-ap-migration

3
.gitignore vendored
View file

@ -4,3 +4,6 @@
/result
/result-lib
.direnv
migration/target
db.sqlite
.env

2366
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,31 +1,63 @@
[package]
name = "lysand-ap-layer"
name = "versia-ap-layer"
version = "0.1.0"
edition = "2021"
build = "build.rs"
authors = ["April John <aprl@acab.dev>"]
license = "AGPL-3.0-or-later"
repository = "https://github.com/lysand-org/lysand-ap-layer"
description = "A compatibility layer between lysands official server and activitypub"
repository = "https://github.com/versia-pub/versia-ap-layer"
description = "A compatibility layer between versias official server and activitypub"
[dependencies]
tokio = { version = "1.20.0", features = ["rt", "macros"] }
serde = { version = "1.0.130", features = ["derive"] }
serde = { version = "1.0.130", features = ["derive", "rc"] }
actix-web = "4"
env_logger = "0.11.0"
clap = { version = "4.3.14", features = ["derive"] }
activitypub_federation = "0.5.2"
activitypub_federation = "0.5.8"
anyhow = "1.0.81"
url = "2.5.0"
rand = "0.8.5"
tracing = "0.1.40"
async-trait = "0.1.79"
enum_delegate = "0.2.0"
chrono = "0.4.37"
activitystreams-kinds = "0.3.0"
thiserror = "1.0.58"
num_cpus = "1.16.0"
actix-web-prom = { version = "0.8.0", features = ["process"] }
serde_json = "1.0.115"
chrono = "0.4.38"
lazy_static = "1.4.0"
async_once = "0.2.6"
reqwest = { version = "0.12.4", features = ["blocking", "json", "multipart"] }
time = { version = "0.3.36", features = ["serde"] }
serde_derive = "1.0.201"
dotenv = "0.15.0"
async-recursion = "1.1.1"
base64-url = "3.0.0"
webfinger = "0.5.1"
regex = "1.10.6"
once_cell = "1.19.0"
actix-files = "0.6.6"
[dependencies.sea-orm]
version = "0.12.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite","sqlx-mysql","with-chrono"
]
[dependencies.uuid]
version = "1.8.0"
features = [
"v4",
"v7",
"fast-rng", # Use a faster (but still sufficiently random) RNG
"serde",
]
[build-dependencies]
vcpkg = "0.2.15"

View file

@ -1,22 +1,22 @@
## 2024-04-14, Version 0.1.0
### Commits
- [[`51a708a1c3`](https://github.com/lysand-org/lysand-ap-layer/commit/51a708a1c3d1aa974deb148156c07dfe7e775a8c)] feat! AGPL3 + Contributor Covenant (April John)
- [[`cc663ffc9b`](https://github.com/lysand-org/lysand-ap-layer/commit/cc663ffc9b56b4b1a93ceaa2e488fc554e597790)] arbeiter (aprilthepink)
- [[`1e6c005698`](https://github.com/lysand-org/lysand-ap-layer/commit/1e6c005698df8c781c60abfb1bab94d0b968c4d9)] enable ttp sign (aprilthepink)
- [[`0060d8baa8`](https://github.com/lysand-org/lysand-ap-layer/commit/0060d8baa8b356d61d0aabfcd6b6f5143a47e8fa)] boop (aprilthepink)
- [[`522ecabd07`](https://github.com/lysand-org/lysand-ap-layer/commit/522ecabd078b7b35276f2aa4f255f967f0b958bf)] aaaa (aprilthepink)
- [[`e75b523bb3`](https://github.com/lysand-org/lysand-ap-layer/commit/e75b523bb3d74bcf696313a320c35caa6f78e225)] awa (aprilthepink)
- [[`76d20cb2ef`](https://github.com/lysand-org/lysand-ap-layer/commit/76d20cb2ef8b3ef3ebe68e5a58c6f7588597ad4e)] awa (aprilthepink)
- [[`4d3657132d`](https://github.com/lysand-org/lysand-ap-layer/commit/4d3657132d764e71f70878de1ee1a284427eb7bb)] mew (aprilthepink)
- [[`643dc59b4b`](https://github.com/lysand-org/lysand-ap-layer/commit/643dc59b4bb0d7da13daf8d6ef47f3e0f31d1500)] Use Action badge on readme (aprilthepink)
- [[`3f4618d4d2`](https://github.com/lysand-org/lysand-ap-layer/commit/3f4618d4d2291e16369fbb4ecddd35d6a841833e)] [feat] GH nix builds (aprilthepink)
- [[`9bc640aadc`](https://github.com/lysand-org/lysand-ap-layer/commit/9bc640aadcd2c02374fa195aed09452b4a3bb3ea)] [fix] format files (aprilthepink)
- [[`b90a332b3c`](https://github.com/lysand-org/lysand-ap-layer/commit/b90a332b3c1e23198d1fad3caddcc8b69dee6a4b)] basic AP (aprilthepink)
- [[`1c09eb793d`](https://github.com/lysand-org/lysand-ap-layer/commit/1c09eb793db0e9b29f3d45b044e269311420ed8b)] Rework readme (aprilthepink)
- [[`091b8efe8e`](https://github.com/lysand-org/lysand-ap-layer/commit/091b8efe8e0578304a81722d11e12db906c4edbc)] [feat]: basic nix dev enviroment (aprilthepink)
- [[`51c7a6d6a2`](https://github.com/lysand-org/lysand-ap-layer/commit/51c7a6d6a2054f6dafbbac70c6b41239e56e2fe9)] Removed mod entities from main.rs (Helba)
- [[`9a021e768d`](https://github.com/lysand-org/lysand-ap-layer/commit/9a021e768d62d4355324f9137e63ae279358ebed)] Create LICENSE (Helba)
- [[`9609c7ab83`](https://github.com/lysand-org/lysand-ap-layer/commit/9609c7ab83251ca31ded4f0589a7dac04ceca874)] initial commit (Helba)
- [[`51a708a1c3`](https://github.com/versia-pub/versia-ap-layer/commit/51a708a1c3d1aa974deb148156c07dfe7e775a8c)] feat! AGPL3 + Contributor Covenant (April John)
- [[`cc663ffc9b`](https://github.com/versia-pub/versia-ap-layer/commit/cc663ffc9b56b4b1a93ceaa2e488fc554e597790)] arbeiter (aprilthepink)
- [[`1e6c005698`](https://github.com/versia-pub/versia-ap-layer/commit/1e6c005698df8c781c60abfb1bab94d0b968c4d9)] enable ttp sign (aprilthepink)
- [[`0060d8baa8`](https://github.com/versia-pub/versia-ap-layer/commit/0060d8baa8b356d61d0aabfcd6b6f5143a47e8fa)] boop (aprilthepink)
- [[`522ecabd07`](https://github.com/versia-pub/versia-ap-layer/commit/522ecabd078b7b35276f2aa4f255f967f0b958bf)] aaaa (aprilthepink)
- [[`e75b523bb3`](https://github.com/versia-pub/versia-ap-layer/commit/e75b523bb3d74bcf696313a320c35caa6f78e225)] awa (aprilthepink)
- [[`76d20cb2ef`](https://github.com/versia-pub/versia-ap-layer/commit/76d20cb2ef8b3ef3ebe68e5a58c6f7588597ad4e)] awa (aprilthepink)
- [[`4d3657132d`](https://github.com/versia-pub/versia-ap-layer/commit/4d3657132d764e71f70878de1ee1a284427eb7bb)] mew (aprilthepink)
- [[`643dc59b4b`](https://github.com/versia-pub/versia-ap-layer/commit/643dc59b4bb0d7da13daf8d6ef47f3e0f31d1500)] Use Action badge on readme (aprilthepink)
- [[`3f4618d4d2`](https://github.com/versia-pub/versia-ap-layer/commit/3f4618d4d2291e16369fbb4ecddd35d6a841833e)] [feat] GH nix builds (aprilthepink)
- [[`9bc640aadc`](https://github.com/versia-pub/versia-ap-layer/commit/9bc640aadcd2c02374fa195aed09452b4a3bb3ea)] [fix] format files (aprilthepink)
- [[`b90a332b3c`](https://github.com/versia-pub/versia-ap-layer/commit/b90a332b3c1e23198d1fad3caddcc8b69dee6a4b)] basic AP (aprilthepink)
- [[`1c09eb793d`](https://github.com/versia-pub/versia-ap-layer/commit/1c09eb793db0e9b29f3d45b044e269311420ed8b)] Rework readme (aprilthepink)
- [[`091b8efe8e`](https://github.com/versia-pub/versia-ap-layer/commit/091b8efe8e0578304a81722d11e12db906c4edbc)] [feat]: basic nix dev enviroment (aprilthepink)
- [[`51c7a6d6a2`](https://github.com/versia-pub/versia-ap-layer/commit/51c7a6d6a2054f6dafbbac70c6b41239e56e2fe9)] Removed mod entities from main.rs (Helba)
- [[`9a021e768d`](https://github.com/versia-pub/versia-ap-layer/commit/9a021e768d62d4355324f9137e63ae279358ebed)] Create LICENSE (Helba)
- [[`9609c7ab83`](https://github.com/versia-pub/versia-ap-layer/commit/9609c7ab83251ca31ded4f0589a7dac04ceca874)] initial commit (Helba)
### Stats
```diff

View file

@ -1,13 +0,0 @@
FROM rust:slim as builder
RUN apt-get update && apt-get install -y libpq-dev libssl-dev pkg-config musl-tools perl make && rm -rf /var/lib/apt/lists/*
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY . /app
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN strip /app/target/x86_64-unknown-linux-musl/release/microservice
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/microservice /microservice
WORKDIR /
CMD ["/microservice"]

View file

@ -1,7 +1,15 @@
## Lysand ActivityPub Layer
[![Nix Flake actions](https://github.com/lysand-org/lysand-ap-layer/actions/workflows/nix-flake.yml/badge.svg)](https://github.com/lysand-org/lysand-ap-layer/actions/workflows/nix-flake.yml)
A simple activitypub compatibility layer ("bridge"), to make Lysand compatible with the ActivityPub and ActivityStreams protocol.
The layer is realised in a microservice format.
<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>
## Versia ActivityPub Bridge
[![Nix Flake actions](https://github.com/versia-pub/activitypub/actions/workflows/nix-flake.yml/badge.svg)](https://github.com/versia-pub/activitypub/actions/workflows/nix-flake.yml)
**ActivityPub/ActivityStreams** compatibility layer for [**Versia Server**](https://github.com/versia-pub/versia).
Designed as a microservice, runs as its own process and communicates with the main server via HTTP.
## Development (Flakes)
@ -20,16 +28,22 @@ nix build
We also provide a [`justfile`](https://just.systems/) for Makefile'esque commands.
### Building and running the docker image
## Building
To build the docker image, run the following command:
### Docker/Podman
To build the Docker image, run the following command:
```bash
> docker build -t f:latest .
docker build -t activitypub:latest .
```
To run the docker image, run the following command:
To run the docker image, use the [`docker-compose.yml`](./docker-compose.yml) file:
```bash
docker run -i -e RUST_LOG="debug" -e DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/database" -e LISTEN="0.0.0.0:8080" -p 8080:8080 f:latest
wget https://raw.githubusercontent.com/versia-pub/activitypub/main/docker-compose.yml
docker-compose up -d
```
If you are building from source, make sure to replace the image name in the `docker-compose.yml` file.

99
ap_user_test/akkoma.json Normal file
View file

@ -0,0 +1,99 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://donotsta.re/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"alsoKnownAs": [
"https://catcatnya.com/users/riedler"
],
"attachment": [
{
"name": "Languages",
"type": "PropertyValue",
"value": "Austrian (native), English (fluent), Standard German (fluent)"
},
{
"name": "Website",
"type": "PropertyValue",
"value": "<a href=\"https://riedler.wien/\" rel=\"ugc\">https://riedler.wien/</a>"
},
{
"name": "programming (order: familiarity)",
"type": "PropertyValue",
"value": "Python, PHP, JS, C99, Java"
},
{
"name": "webdev (order: random)",
"type": "PropertyValue",
"value": "HTML5, CSS3, SVG"
},
{
"name": "pronounciation (approx)",
"type": "PropertyValue",
"value": "reedluh"
},
{
"name": "pronounciation (IPA)",
"type": "PropertyValue",
"value": "ʁiːdlä"
},
{
"name": "personal info :3",
"type": "PropertyValue",
"value": "<a href=\"https://pronouns.cc/@Riedler\" rel=\"ugc\">https://pronouns.cc/@Riedler</a>"
},
{
"name": "age",
"type": "PropertyValue",
"value": "adult"
}
],
"capabilities": {},
"discoverable": true,
"endpoints": {
"oauthAuthorizationEndpoint": "https://donotsta.re/oauth/authorize",
"oauthRegistrationEndpoint": "https://donotsta.re/api/v1/apps",
"oauthTokenEndpoint": "https://donotsta.re/oauth/token",
"sharedInbox": "https://donotsta.re/inbox"
},
"featured": "https://donotsta.re/users/Riedler/collections/featured",
"followers": "https://donotsta.re/users/Riedler/followers",
"following": "https://donotsta.re/users/Riedler/following",
"icon": {
"type": "Image",
"url": "https://asdf.donotsta.re/media/c5671c1d2d6eec83124b00dc2eb010cef7b4d9734074a27feff0cd2b9eac91dc.png"
},
"id": "https://donotsta.re/users/Riedler",
"image": {
"type": "Image",
"url": "https://asdf.donotsta.re/media/397f19d2f61853b9cba4f63aa7d4235225a4744a188279f6095b3cfbdf19a1eb.png"
},
"inbox": "https://donotsta.re/users/Riedler/inbox",
"manuallyApprovesFollowers": true,
"name": "Riedler (2004) [E] [!]",
"outbox": "https://donotsta.re/users/Riedler/outbox",
"preferredUsername": "Riedler",
"publicKey": {
"id": "https://donotsta.re/users/Riedler#main-key",
"owner": "https://donotsta.re/users/Riedler",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4rUBqCAMMSDjuiepr+3R\nZMLZ1qF4ybsAI1iOcMU+l7YLkZao8KpfYb7NI5AoXUiR8OhCaFFLgbnQQOBuXzzT\nmMa5KOMEiCUwTCSN7nHrAcULWuT9RPA8Uo/itin2Q5rLkGQHWu04IuBZSrLyql4E\nvnRIybaFaNx/uYIsVD6UI/9Gmp1HSYn1hfzSVywZy1umeo7dimRN8joaAy4VTBgl\nyA5TgFsEWKF//4JUtdPCIRMnRgUnihwNPk0dkWa7fPt4syFWEg1smjulFKJ4hSB6\nJ9oqQJ0Wj0i+K+x+hS68PG38Ofj+BTlF0os2MmEcSXG9859W7hAhA0TzoQQsEXDM\nHwIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"summary": "Austrian Musician and Programmer. From Linz, living in Vienna.<br/><br/>Several people have told me that not all of this bio federates to other software properly, so please open my profile externally, to read the whole thing. Thanks, and sorry for the inconvenience!<br/><br/>In uwus with <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AcLGFPIEBPczHLP2Lw\" href=\"https://tech.lgbt/@Autumn_Maxime\" rel=\"ugc\">@<span>Autumn_Maxime@tech.lgbt</span></a></span>, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AAcLsLRX2zm0dbbXHM\" href=\"https://donotsta.re/users/domi\" rel=\"ugc\">@<span>domi@donotsta.re</span></a></span>, she who bangs outside of fedi, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"Ae0GjjJMdsUKeCS8jQ\" href=\"https://not.an.evilcyberhacker.net/@phos\" rel=\"ugc\">@<span>phos@not.an.evilcyberhacker.net</span></a></span>, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AXcm6z3VImlLMSmYwi\" href=\"https://not.an.evilcyberhacker.net/@darkphoenix\" rel=\"ugc\">@<span>darkphoenix@not.an.evilcyberhacker.net</span></a></span> and <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQMoXdVLtxjDMiK0SO\" href=\"https://donotsta.re/users/Lili\" rel=\"ugc\">@<span>lili@donotsta.re</span></a></span> :3<br/><br/>Professional Reviews™:<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQkD6XWhWPJnGeN2MS\" href=\"https://catcatnya.com/@benaryorg\" rel=\"ugc\">@<span>benaryorg@catcatnya.com</span></a></span> - &quot;suspiciously knowledgeable tech person with inspiring Thoughts™ and Opinions™&quot;<br/> shebang - &quot;bsdhdkshhejeeh riedler whaaa eirurjrjrkeikwwbebrufyfirjrhjwnedj&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AXv31NdX2IJbWIgyUy\" href=\"https://plush.city/@heatherhorns_lite\" rel=\"ugc\">@<span>heatherhorns_lite@plush.city</span></a></span> - &quot;Riedler&#39;s got the moves&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AdVvWtdO2ct6eq1Z56\" href=\"https://tech.lgbt/@keyboardsmash\" rel=\"ugc\">@<span>keyboardsmash@tech.lgbt</span></a></span> - &quot;i know them from the internet and they are nice. also dad jokes.&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AiWaF6eoRSyY7HVttQ\" href=\"https://donotsta.re/users/Ak1ra\" rel=\"ugc\">@<span>Ak1ra</span></a></span> - &quot;You are very caring Riedler&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AAcLsLRX2zm0dbbXHM\" href=\"https://donotsta.re/users/domi\" rel=\"ugc\">@<span>domi</span></a></span> - &quot;God&#39;s biggest eeper&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AWFInQhcYuy288f9bk\" href=\"https://donotsta.re/users/april\" rel=\"ugc\">@<span>april</span></a></span> - &quot;I meowed at you, why u don respond🥺&quot;<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQMoXdVLtxjDMiK0SO\" href=\"https://donotsta.re/users/Lili\" rel=\"ugc\">@<span>Lili</span></a></span> - &quot;someday I&#39;ll think about something&quot;<br/><br/>I accept follow requests if you pass the vibe check. Try it out :meowmlem: To check that you read my bio, tell me your favourite food.<br/><br/>Profile picture is a very stylized version of a capital R. It&#39;s a pink-white-blue sine wave with a softened nail thing poking through the left side. Yes, trans colors.<br/>Header is a very stylized version of &quot;Riedler&quot;. The shape of the Rs are similar to my profile picture, just without the nail thing. Various shapes have lines exending throughout the rest of the image, creating a nice pattern between the pink, white and blue.",
"tag": [
{
"icon": {
"type": "Image",
"url": "https://donotsta.re/emoji/meow/meowmlem.png"
},
"id": "https://donotsta.re/emoji/meow/meowmlem.png",
"name": ":meowmlem:",
"type": "Emoji",
"updated": "1970-01-01T00:00:00Z"
}
],
"type": "Person",
"url": "https://donotsta.re/users/Riedler"
}

View file

@ -1,11 +0,0 @@
#[cfg(target_os = "windows")]
fn main() {
vcpkg::Config::new()
.emit_includes(true)
.copy_dlls(true)
.find_package("libpq")
.unwrap();
}
#[cfg(not(target_os = "windows"))]
fn main() {}

3
debug-bootstrap.sh Executable file
View file

@ -0,0 +1,3 @@
git pull
nix run .#ls-ap-migration
RUST_DEBUG=1 cargo run

9
docker-compose.yml Normal file
View file

@ -0,0 +1,9 @@
services:
activitypub:
environment:
- RUST_LOG=debug
- DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/database
- LISTEN=0.0.0.0:8080
ports:
- 8080:8080
image: ghcr.io/versia-pub/activitypub:main

88
flake.lock generated
View file

@ -1,15 +1,29 @@
{
"nodes": {
"flake-compat": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?rev=ff81ac966bb2cae68946d5ed5fc4994f96d0ffec&revCount=69"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1709336216,
"narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=",
"lastModified": 1738453229,
"narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2",
"rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
"type": "github"
},
"original": {
@ -18,35 +32,59 @@
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1736429655,
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1710806803,
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3",
"type": "github"
"lastModified": 0,
"narHash": "sha256-8Eo/jRAgT3CbAloyqOj6uPN1EqBvLI/Tv2g+RxHjkhU=",
"path": "/nix/store/vg3rs6imxilxn66gf6vb8m98d7ib35f8-source",
"type": "path"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "NixOS",
"lastModified": 1738452942,
"narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1739020877,
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
@ -54,8 +92,10 @@
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"systems": "systems",
"treefmt-nix": "treefmt-nix"
}
@ -82,11 +122,11 @@
]
},
"locked": {
"lastModified": 1710781103,
"narHash": "sha256-nehQK/XTFxfa6rYKtbi8M1w+IU1v5twYhiyA4dg1vpg=",
"lastModified": 1738953846,
"narHash": "sha256-yrK3Hjcr8F7qS/j2F+r7C7o010eVWWlm4T1PrbKBOxQ=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "7ee5aaac63c30d3c97a8c56efe89f3b2aa9ae564",
"rev": "4f09b473c936d41582dd744e19f34ec27592c5fd",
"type": "github"
},
"original": {

View file

@ -5,24 +5,44 @@
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default";
naersk.url = "github:nix-community/naersk";
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
# Dev tools
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{ flake-parts, ... }:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
nixConfig = {
extra-substituters = [
"https://cache.kyouma.net"
];
extra-trusted-public-keys = [
"cache.kyouma.net:Frjwu4q1rnwE/MnSTmX9yx86GNA/z3p/oElGvucLiZg="
];
};
outputs = inputs@{ flake-parts, self, ... }:
inputs.flake-parts.lib.mkFlake { inherit inputs self; } {
systems = import inputs.systems;
flake = {
hydraJobs = inputs.nixpkgs.lib.genAttrs [ "packages" "checks" "devShells" ] (attrs: {
inherit (self.${attrs}) x86_64-linux aarch64-linux;
});
};
imports = [
inputs.treefmt-nix.flakeModule
inputs.flake-parts.flakeModules.easyOverlay
];
perSystem = { config, self', pkgs, lib, system, ... }:
let
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
cargoMigToml = builtins.fromTOML (builtins.readFile ./migration/Cargo.toml);
nonRustDeps = with pkgs; [
libiconv
openssl
];
naersk' = pkgs.callPackage inputs.naersk { };
rust-toolchain = pkgs.symlinkJoin {
name = "rust-toolchain";
paths = [ pkgs.rustc pkgs.cargo pkgs.cargo-watch pkgs.rust-analyzer pkgs.rustPlatform.rustcSrc ];
@ -32,17 +52,51 @@
};
in
{
overlayAttrs = {
inherit (config.packages) versia-ap-layer ls-ap-migration;
};
# Rust package
packages.default = pkgs.rustPlatform.buildRustPackage {
packages.default = config.packages.versia-ap-layer;
packages.versia-ap-layer = naersk'.buildPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
buildInputs = nonRustDeps;
nativeBuildInputs = with pkgs; [
rust-toolchain
pkg-config
];
};
packages.ls-ap-migration = naersk'.buildPackage {
inherit (cargoMigToml.package) name version;
src = ./migration;
buildInputs = nonRustDeps;
nativeBuildInputs = with pkgs; [
rust-toolchain
pkg-config
];
};
packages.ociImage = pkgs.dockerTools.buildLayeredImage
{
name = "ghcr.io/versia-pub/activitypub";
tag = "main";
contents = [
config.packages.versia-ap-layer
config.packages.ls-ap-migration
pkgs.bash
];
config = {
Cmd = [
"${pkgs.bash}/bin/bash"
"${config.packages.ls-ap-migration}/bin/ls-ap-migration"
"up"
"&&"
"${config.packages.versia-ap-layer}/bin/versia-ap-layer"
];
ExposedPorts = {
"8080/tcp" = { };
};
};
};
# Rust dev environment
devShells.default = pkgs.mkShell {
@ -62,6 +116,7 @@
just
rust-toolchain
pkg-config
sea-orm-cli
];
RUST_BACKTRACE = 1;
};

2765
migration/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
migration/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "ls-ap-migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
chrono = "0.4.38"
dotenv = "0.15.0"
[dependencies.sea-orm-migration]
version = "0.12.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite","sqlx-mysql","with-chrono"
]

41
migration/README.md Normal file
View file

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

26
migration/src/lib.rs Normal file
View file

@ -0,0 +1,26 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_post_table;
mod m20240417_230111_user_table;
mod m20240417_233430_post_user_keys;
mod m20240505_002524_user_follow_relation;
mod m20240626_030922_store_ap_json_in_posts;
mod m20240719_235452_user_ap_column;
mod m20240725_120932_follow_table_two_point_zero;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_post_table::Migration),
Box::new(m20240417_230111_user_table::Migration),
Box::new(m20240417_233430_post_user_keys::Migration),
Box::new(m20240505_002524_user_follow_relation::Migration),
Box::new(m20240626_030922_store_ap_json_in_posts::Migration),
Box::new(m20240719_235452_user_ap_column::Migration),
Box::new(m20240725_120932_follow_table_two_point_zero::Migration),
]
}
}

View file

@ -0,0 +1,59 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Post::Table)
.if_not_exists()
.col(ColumnDef::new(Post::Id).string().not_null().primary_key())
.col(ColumnDef::new(Post::Title).string())
.col(ColumnDef::new(Post::Content).string().not_null())
.col(ColumnDef::new(Post::Local).boolean().not_null())
.col(ColumnDef::new(Post::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Post::UpdatedAt).timestamp())
.col(ColumnDef::new(Post::ReblogId).string())
.col(ColumnDef::new(Post::ContentType).string().not_null())
.col(ColumnDef::new(Post::Visibility).string().not_null())
.col(ColumnDef::new(Post::ReplyId).string())
.col(ColumnDef::new(Post::QuotingId).string())
.col(ColumnDef::new(Post::Sensitive).boolean().not_null())
.col(ColumnDef::new(Post::SpoilerText).string())
.col(ColumnDef::new(Post::Creator).string().not_null())
.col(ColumnDef::new(Post::Url).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Post::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
pub enum Post {
Table,
Id,
Url,
Creator,
Title,
Content,
Local,
CreatedAt,
UpdatedAt,
ReblogId,
ContentType,
Visibility,
ReplyId,
QuotingId,
Sensitive,
SpoilerText,
}

View file

@ -0,0 +1,61 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id).string().not_null().primary_key())
.col(ColumnDef::new(User::Username).string().not_null())
.col(ColumnDef::new(User::Name).string().not_null())
.col(ColumnDef::new(User::Summary).string())
.col(ColumnDef::new(User::Url).string().not_null())
.col(ColumnDef::new(User::PublicKey).string().not_null())
.col(ColumnDef::new(User::PrivateKey).string())
.col(ColumnDef::new(User::LastRefreshedAt).timestamp().not_null())
.col(ColumnDef::new(User::Local).boolean().not_null())
.col(ColumnDef::new(User::FollowerCount).integer().not_null())
.col(ColumnDef::new(User::FollowingCount).integer().not_null())
.col(ColumnDef::new(User::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(User::UpdatedAt).timestamp())
.col(ColumnDef::new(User::Following).string())
.col(ColumnDef::new(User::Followers).string())
.col(ColumnDef::new(User::Inbox).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
pub enum User {
Table,
Id,
Username,
Name,
Summary,
Url,
PublicKey,
PrivateKey,
LastRefreshedAt,
Local,
FollowerCount,
FollowingCount,
CreatedAt,
UpdatedAt,
Following,
Followers,
Inbox,
}

View file

@ -0,0 +1,80 @@
use sea_orm_migration::prelude::*;
use crate::{m20220101_000001_post_table::Post, m20240417_230111_user_table::User};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Post::Table).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Post::Table)
.if_not_exists()
.col(ColumnDef::new(Post::Id).string().not_null().primary_key())
.col(ColumnDef::new(Post::Title).string())
.col(ColumnDef::new(Post::Content).string().not_null())
.col(ColumnDef::new(Post::Local).boolean().not_null())
.col(ColumnDef::new(Post::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Post::UpdatedAt).timestamp())
.col(ColumnDef::new(Post::ReblogId).string())
.col(ColumnDef::new(Post::ContentType).string().not_null())
.col(ColumnDef::new(Post::Visibility).string().not_null())
.col(ColumnDef::new(Post::ReplyId).string())
.col(ColumnDef::new(Post::QuotingId).string())
.col(ColumnDef::new(Post::Sensitive).boolean().not_null())
.col(ColumnDef::new(Post::SpoilerText).string())
.col(ColumnDef::new(Post::Creator).string().not_null())
.col(ColumnDef::new(Post::Url).string().not_null())
.foreign_key(
ForeignKey::create()
.name("fk_post_creator_user_id")
.from(Post::Table, Post::Creator)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_post_reblog_id")
.from(Post::Table, Post::ReblogId)
.to(Post::Table, Post::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_post_reply_id")
.from(Post::Table, Post::ReplyId)
.to(Post::Table, Post::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_post_quoting_id")
.from(Post::Table, Post::QuotingId)
.to(Post::Table, Post::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_foreign_key(
ForeignKey::drop()
.name("fk_post_creator_user_id")
.table(Post::Table)
.to_owned(),
)
.await
}
}

View file

@ -0,0 +1,73 @@
use sea_orm_migration::prelude::*;
use crate::m20240417_230111_user_table::User;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(FollowRelation::Table)
.if_not_exists()
.col(
ColumnDef::new(FollowRelation::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(FollowRelation::FolloweeId)
.string()
.not_null(),
)
.col(
ColumnDef::new(FollowRelation::FollowerId)
.string()
.not_null(),
)
.col(ColumnDef::new(FollowRelation::FolloweeHost).string())
.col(ColumnDef::new(FollowRelation::FollowerHost).string())
.col(ColumnDef::new(FollowRelation::FolloweeInbox).string())
.col(ColumnDef::new(FollowRelation::FollowerInbox).string())
.foreign_key(
ForeignKey::create()
.name("fk_follow_relation_followee_id")
.from(FollowRelation::Table, FollowRelation::FolloweeId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_follow_relation_follower_id")
.from(FollowRelation::Table, FollowRelation::FollowerId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum FollowRelation {
Table,
Id,
FolloweeId,
FollowerId,
FolloweeHost,
FollowerHost,
FolloweeInbox,
FollowerInbox,
}

View file

@ -0,0 +1,35 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Post::Table)
.add_column_if_not_exists(ColumnDef::new(Post::ApJson).string())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Post::Table)
.drop_column(Post::ApJson)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
pub enum Post {
Table,
ApJson,
}

View file

@ -0,0 +1,35 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(User::Table)
.add_column_if_not_exists(ColumnDef::new(User::ApJson).string())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(User::Table)
.drop_column(User::ApJson)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
pub enum User {
Table,
ApJson,
}

View file

@ -0,0 +1,87 @@
use sea_orm_migration::prelude::*;
use crate::m20240417_230111_user_table::User;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let _ = manager
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
.await;
manager
.create_table(
Table::create()
.table(FollowRelation::Table)
.if_not_exists()
.col(
ColumnDef::new(FollowRelation::Id)
.string()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(FollowRelation::FolloweeId)
.string()
.not_null(),
)
.col(
ColumnDef::new(FollowRelation::FollowerId)
.string()
.not_null(),
)
.col(ColumnDef::new(FollowRelation::FolloweeHost).string())
.col(ColumnDef::new(FollowRelation::FollowerHost).string())
.col(ColumnDef::new(FollowRelation::FolloweeInbox).string())
.col(ColumnDef::new(FollowRelation::FollowerInbox).string())
.col(ColumnDef::new(FollowRelation::AcceptId).string())
.col(ColumnDef::new(FollowRelation::ApId).string())
.col(ColumnDef::new(FollowRelation::ApAcceptId).string())
.col(ColumnDef::new(FollowRelation::Remote).boolean().not_null())
.col(ColumnDef::new(FollowRelation::ApJson).string().not_null())
.col(ColumnDef::new(FollowRelation::ApAcceptJson).string())
.foreign_key(
ForeignKey::create()
.name("fk_follow_relation_followee_id")
.from(FollowRelation::Table, FollowRelation::FolloweeId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_follow_relation_follower_id")
.from(FollowRelation::Table, FollowRelation::FollowerId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum FollowRelation {
Table,
Id,
AcceptId,
Remote, // true if initial Follow came from Remote
ApId,
ApAcceptId,
FolloweeId,
FollowerId,
FolloweeHost,
FollowerHost,
FolloweeInbox,
FollowerInbox,
ApJson,
ApAcceptJson,
}

8
migration/src/main.rs Normal file
View file

@ -0,0 +1,8 @@
use dotenv::dotenv;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
dotenv().ok();
cli::run_cli(migration::Migrator).await;
}

2
nix-bootstrap.sh Executable file
View file

@ -0,0 +1,2 @@
nix run .#ls-ap-migration
nix run .#versia-ap-layer

View file

@ -1,13 +1,10 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs = with pkgs; [
rustc
cargo
rustfmt
rust-analyzer
clippy
];
RUST_BACKTRACE = 1;
}
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
fetchTarball {
url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; }
).shellNix

View file

@ -1,9 +1,18 @@
use crate::{
database::DatabaseHandle,
database::StateHandle,
entities::{self, post, prelude, user},
error::Error,
objects::post::DbPost,
objects::{person::DbUser, post::Note},
utils::generate_object_id,
objects::{
person::DbUser,
post::{DbPost, Note},
},
utils::{base_url_encode, generate_create_id, generate_random_object_id},
versia::{
conversion::{versia_post_from_db, versia_user_from_db},
objects::SortAlphabetically,
superx::request_client,
},
API_DOMAIN, AUTH, DB,
};
use activitypub_federation::{
activity_sending::SendActivityTask,
@ -13,13 +22,16 @@ use activitypub_federation::{
protocol::{context::WithContext, helpers::deserialize_one_or_many},
traits::{ActivityHandler, Object},
};
use reqwest::RequestBuilder;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreatePost {
pub(crate) actor: ObjectId<DbUser>,
pub(crate) actor: ObjectId<user::Model>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: Note,
@ -29,18 +41,52 @@ pub struct CreatePost {
}
impl CreatePost {
pub async fn send(note: Note, inbox: Url, data: &Data<DatabaseHandle>) -> Result<(), Error> {
pub async fn send(
note: Note,
db_entry: post::Model,
inbox: Url,
data: &Data<StateHandle>,
) -> Result<(), Error> {
print!("Sending reply to {}", &note.attributed_to);
let encoded_url = base_url_encode(&note.id.clone().into());
let create = CreatePost {
actor: note.attributed_to.clone(),
to: note.to.clone(),
object: note,
kind: CreateType::Create,
id: generate_object_id(data.domain())?,
id: generate_create_id(data.domain(), &db_entry.id, &encoded_url)?,
};
let create_with_context = WithContext::new_default(create);
let sends = SendActivityTask::prepare(
&create_with_context,
&data.local_user().await?,
vec![inbox],
data,
)
.await?;
for send in sends {
send.sign_and_send(data).await?;
}
Ok(())
}
pub async fn sends(
note: Note,
db_entry: post::Model,
inbox: Vec<Url>,
data: &Data<StateHandle>,
) -> Result<(), Error> {
print!("Sending reply to {}", &note.attributed_to);
let encoded_url = base_url_encode(&note.id.clone().into());
let create = CreatePost {
actor: note.attributed_to.clone(),
to: note.to.clone(),
object: note,
kind: CreateType::Create,
id: generate_create_id(data.domain(), &db_entry.id, &encoded_url)?,
};
let create_with_context = WithContext::new_default(create);
let sends =
SendActivityTask::prepare(&create_with_context, &data.local_user(), vec![inbox], data)
SendActivityTask::prepare(&create_with_context, &data.local_user().await?, inbox, data)
.await?;
for send in sends {
send.sign_and_send(data).await?;
@ -51,7 +97,7 @@ impl CreatePost {
#[async_trait::async_trait]
impl ActivityHandler for CreatePost {
type DataType = DatabaseHandle;
type DataType = StateHandle;
type Error = crate::error::Error;
fn id(&self) -> &Url {
@ -63,12 +109,73 @@ impl ActivityHandler for CreatePost {
}
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
DbPost::verify(&self.object, &self.id, data).await?;
post::Model::verify(&self.object, &self.id, data).await?;
Ok(())
}
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
DbPost::from_json(self.object, data).await?;
let note = post::Model::from_json(self.object, data).await?;
federate_inbox(note).await?;
Ok(())
}
}
async fn federate_inbox(note: crate::entities::post::Model) -> anyhow::Result<()> {
let versia_post = versia_post_from_db(note.clone()).await?;
let mut array;
if versia_post.mentions.is_some() {
info!("good");
array = versia_post.mentions.clone().unwrap();
info!("{:#?}", versia_post.mentions.clone().unwrap());
} else {
info!("fake");
array = Vec::new();
}
let db = DB.get().unwrap();
let list_model = entities::prelude::FollowRelation::find()
.filter(entities::follow_relation::Column::FolloweeId.eq(note.creator.to_string()))
.all(db)
.await?;
let mut list_url = Vec::new();
for model in list_model {
let url = Url::parse(&model.follower_inbox.unwrap())?;
list_url.push(url);
}
array.append(&mut list_url);
array.sort();
array.dedup();
let req_client = request_client();
let model = prelude::User::find()
.filter(user::Column::Id.eq(note.creator.as_str()))
.one(db)
.await?
.unwrap();
for inbox in array {
let push = req_client
.post(inbox.clone())
.bearer_auth(AUTH.to_string())
.json(&SortAlphabetically(&versia_post));
warn!("{}", inbox.to_string());
tokio::spawn(push_to_inbox(push));
}
Ok(())
}
async fn push_to_inbox(req: RequestBuilder) -> anyhow::Result<()> {
let req_client = request_client();
let response = req_client.execute(req.build()?).await?;
info!("{}", response.status());
info!("{:?}", response.text().await?);
Ok(())
}

203
src/activities/follow.rs Normal file
View file

@ -0,0 +1,203 @@
use activitypub_federation::{
activity_sending::SendActivityTask,
config::Data,
fetch::object_id::ObjectId,
protocol::context::WithContext,
traits::{ActivityHandler, Actor, Object},
};
use activitystreams_kinds::activity::{AcceptType, FollowType};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityOrSelect, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
database::StateHandle,
entities::{
follow_relation::{self, Entity},
post, prelude, user,
},
error,
utils::{generate_follow_accept_id, generate_random_object_id},
versia::funcs::send_follow_accept_to_versia,
DB,
};
#[derive(Deserialize, Serialize, Debug)]
pub struct Follow {
pub actor: ObjectId<user::Model>,
pub object: ObjectId<user::Model>,
#[serde(rename = "type")]
pub kind: FollowType,
pub id: Url,
}
impl Follow {
pub async fn send(
local_user: ObjectId<user::Model>,
followee: ObjectId<user::Model>,
inbox: Url,
data: &Data<StateHandle>,
) -> Result<(), error::Error> {
print!("Sending follow request to {}", &followee);
let create = Follow {
actor: local_user.clone(),
object: followee.clone(),
kind: FollowType::Follow,
id: generate_random_object_id(data.domain())?,
};
let create_with_context = WithContext::new_default(create);
let sends = SendActivityTask::prepare(
&create_with_context,
&data.local_user().await?,
vec![inbox],
data,
)
.await?;
for send in sends {
send.sign_and_send(data).await?;
}
Ok(())
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Accept {
actor: ObjectId<user::Model>,
object: Follow,
#[serde(rename = "type")]
kind: AcceptType,
id: Url,
}
impl Accept {
pub async fn send(
follow_relation: follow_relation::Model,
follow_req: Follow,
inbox: Url,
data: &Data<StateHandle>,
) -> Result<(), error::Error> {
print!("Sending accept to {}", &follow_relation.follower_id);
let create = Accept {
actor: follow_req.object.clone(),
object: follow_req,
kind: AcceptType::Accept,
id: generate_follow_accept_id(data.domain(), follow_relation.id.to_string().as_str())?,
};
let create_with_context = WithContext::new_default(create);
let sends = SendActivityTask::prepare(
&create_with_context,
&data.local_user().await?,
vec![inbox],
data,
)
.await?;
for send in sends {
send.sign_and_send(data).await?;
}
Ok(())
}
}
#[async_trait::async_trait]
impl ActivityHandler for Follow {
type DataType = StateHandle;
type Error = crate::error::Error;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
//accept_follow(self, data).await?; TODO replace w/ versia forward
Ok(())
}
}
#[async_trait::async_trait]
impl ActivityHandler for Accept {
type DataType = StateHandle;
type Error = crate::error::Error;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
let user = self.actor.dereference(data).await?;
let follower_id;
let follower_bridge_url = self.object.actor.clone().to_string();
let split = follower_bridge_url.split("/").collect::<Vec<&str>>();
if split[split.len() - 1].is_empty() {
follower_id = split[split.len() - 2];
} else {
follower_id = split[split.len() - 1];
}
let follower = prelude::User::find()
.filter(user::Column::Id.eq(follower_id))
.one(data.database_connection.as_ref())
.await?;
save_accept_follow(user, follower.unwrap(), self).await?;
Ok(())
}
}
/*
async fn accept_follow(
follow_req: Follow,
data: &Data<StateHandle>,
) -> Result<(), crate::error::Error> {
let local_user = follow_req.actor.dereference(data).await?;
let follower = follow_req.object.dereference(data).await?;
let follow_relation = save_follow(local_user, follower.clone()).await?;
Accept::send(follow_relation, follow_req, follower.inbox().clone(), data).await?;
Ok(())
}
*/
async fn save_accept_follow(
followee: user::Model,
follower: user::Model,
accept_activity: Accept,
) -> Result<follow_relation::Model, crate::error::Error> {
let db = DB.get().unwrap();
let query = prelude::FollowRelation::find()
.filter(follow_relation::Column::FollowerId.eq(follower.id.as_str()))
.filter(follow_relation::Column::FolloweeId.eq(followee.id.as_str()))
.one(db)
.await?;
if query.is_none() {
return Err(crate::error::Error(anyhow::anyhow!("oopsie woopise")));
}
let versia_accept_id = uuid::Uuid::now_v7().to_string();
// all values in the ActiveModel that are set, except the id, will be updated
let active_query = follow_relation::ActiveModel {
id: Set(query.unwrap().id),
ap_accept_id: Set(Some(accept_activity.id.to_string())),
ap_accept_json: Set(Some(serde_json::to_string(&accept_activity).unwrap())),
accept_id: Set(Some(versia_accept_id)),
..Default::default()
};
// modify db entry
let res = prelude::FollowRelation::update(active_query);
let model = res.exec(db).await?;
let _ = send_follow_accept_to_versia(model.clone()).await?;
Ok(model)
}

View file

@ -1 +1,2 @@
pub mod create_post;
pub mod follow;

View file

@ -1,8 +1,22 @@
use crate::{error::Error, objects::person::DbUser};
use super::entities::prelude::User;
use crate::{entities::user, error::Error, objects::person::DbUser, LOCAL_USER_NAME};
use anyhow::anyhow;
use std::sync::{Arc, Mutex};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use std::{
env,
sync::{Arc, Mutex},
};
pub type DatabaseHandle = Arc<Database>;
#[derive(Debug, Clone)]
pub struct Config {}
#[derive(Debug, Clone)]
pub struct State {
pub database_connection: Arc<DatabaseConnection>,
}
pub type StateHandle = State;
/// Our "database" which contains all known users (local and federated)
#[derive(Debug)]
@ -10,18 +24,25 @@ pub struct Database {
pub users: Mutex<Vec<DbUser>>,
}
impl Database {
pub fn local_user(&self) -> DbUser {
let lock = self.users.lock().unwrap();
lock.first().unwrap().clone()
impl State {
pub async fn local_user(&self) -> Result<user::Model, Error> {
let user = User::find()
.filter(
user::Column::Username
.eq(env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string())),
)
.one(self.database_connection.as_ref())
.await?
.unwrap();
Ok(user.clone())
}
pub fn read_user(&self, name: &str) -> Result<DbUser, Error> {
let db_user = self.local_user();
if name == db_user.name {
pub async fn read_user(&self, name: &str) -> Result<user::Model, Error> {
let db_user = self.local_user().await?;
if name == db_user.username {
Ok(db_user)
} else {
Err(anyhow!("Invalid user {name}").into())
Err(anyhow!("Invalid user {name} // {0}", db_user.username).into())
}
}
}

View file

@ -0,0 +1,44 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "follow_relation")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub followee_id: String,
pub follower_id: String,
pub followee_host: Option<String>,
pub follower_host: Option<String>,
pub followee_inbox: Option<String>,
pub follower_inbox: Option<String>,
pub accept_id: Option<String>,
pub ap_id: Option<String>,
pub ap_accept_id: Option<String>,
pub remote: bool,
pub ap_json: String,
pub ap_accept_json: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::FollowerId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User2,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::FolloweeId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User1,
}
impl ActiveModelBehavior for ActiveModel {}

7
src/entities/mod.rs Normal file
View file

@ -0,0 +1,7 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
pub mod prelude;
pub mod follow_relation;
pub mod post;
pub mod user;

72
src/entities/post.rs Normal file
View file

@ -0,0 +1,72 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use chrono::Utc;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub title: Option<String>,
pub content: String,
pub local: bool,
#[sea_orm(column_type = "Timestamp")]
pub created_at: chrono::DateTime<Utc>,
#[sea_orm(column_type = "Timestamp")]
pub updated_at: Option<chrono::DateTime<Utc>>,
pub reblog_id: Option<String>,
pub content_type: String,
pub visibility: String,
pub reply_id: Option<String>,
pub quoting_id: Option<String>,
pub sensitive: bool,
pub spoiler_text: Option<String>,
pub creator: String,
pub url: String,
pub ap_json: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::QuotingId",
to = "Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
SelfRef3,
#[sea_orm(
belongs_to = "Entity",
from = "Column::ReplyId",
to = "Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
SelfRef2,
#[sea_orm(
belongs_to = "Entity",
from = "Column::ReblogId",
to = "Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
SelfRef1,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::Creator",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

5
src/entities/prelude.rs Normal file
View file

@ -0,0 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
pub use super::follow_relation::Entity as FollowRelation;
pub use super::post::Entity as Post;
pub use super::user::Entity as User;

44
src/entities/user.rs Normal file
View file

@ -0,0 +1,44 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use chrono::Utc;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub username: String,
pub name: String,
pub summary: Option<String>,
pub url: String,
pub public_key: String,
pub private_key: Option<String>,
#[sea_orm(column_type = "Timestamp")]
pub last_refreshed_at: chrono::DateTime<Utc>,
pub local: bool,
pub follower_count: i32,
pub following_count: i32,
#[sea_orm(column_type = "Timestamp")]
pub created_at: chrono::DateTime<Utc>,
#[sea_orm(column_type = "Timestamp")]
pub updated_at: Option<chrono::DateTime<Utc>>,
pub following: Option<String>,
pub followers: Option<String>,
pub inbox: String,
pub ap_json: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::post::Entity")]
Post,
}
impl Related<super::post::Entity> for Entity {
fn to() -> RelationDef {
Relation::Post.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,22 +1,33 @@
use crate::{
database::DatabaseHandle,
database::StateHandle,
entities::user,
error::Error,
objects::person::{DbUser, PersonAcceptedActivities},
utils::generate_user_id,
versia::{
self,
conversion::{db_user_from_url, local_db_user_from_name, receive_versia_note},
},
API_DOMAIN, LYSAND_DOMAIN,
};
use activitypub_federation::{
actix_web::{inbox::receive_activity, signing_actor},
config::{Data, FederationConfig, FederationMiddleware},
fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
fetch::webfinger::{build_webfinger_response, extract_webfinger_name, WebFingerError},
protocol::context::WithContext,
traits::{Actor, Object},
FEDERATION_CONTENT_TYPE,
};
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
use anyhow::anyhow;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
use tracing::info;
use url::Url;
use webfinger::resolve;
pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
pub fn listen(config: &FederationConfig<StateHandle>) -> Result<(), Error> {
let hostname = config.domain();
info!("Listening with actix-web on {hostname}");
let config = config.clone();
@ -34,6 +45,15 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
Ok(())
}
pub fn versia_inbox(
note: web::Json<versia::objects::Note>,
id: web::Path<String>,
data: Data<StateHandle>,
) -> Result<HttpResponse, Error> {
tokio::spawn(receive_versia_note(note.into_inner(), id.into_inner()));
Ok(HttpResponse::Created().finish())
}
/// Handles requests to fetch system user json over HTTP
/*pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
let json_user = data.system_user.clone().into_json(&data).await?;
@ -46,7 +66,7 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
pub async fn http_get_user(
request: HttpRequest,
user_name: web::Path<String>,
data: Data<DatabaseHandle>,
data: Data<StateHandle>,
) -> Result<HttpResponse, Error> {
//let signed_by = signing_actor::<DbUser>(&request, None, &data).await?;
// here, checks can be made on the actor or the domain to which
@ -56,8 +76,8 @@ pub async fn http_get_user(
// signed_by.id()
//);
let db_user = data.local_user();
if user_name.into_inner() == db_user.name {
let db_user = data.local_user().await?;
if user_name.into_inner() == db_user.username {
let json_user = db_user.into_json(&data).await?;
Ok(HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
@ -71,9 +91,9 @@ pub async fn http_get_user(
pub async fn http_post_user_inbox(
request: HttpRequest,
body: Bytes,
data: Data<DatabaseHandle>,
data: Data<StateHandle>,
) -> Result<HttpResponse, Error> {
receive_activity::<WithContext<PersonAcceptedActivities>, DbUser, DatabaseHandle>(
receive_activity::<WithContext<PersonAcceptedActivities>, user::Model, StateHandle>(
request, body, &data,
)
.await
@ -86,12 +106,18 @@ pub struct WebfingerQuery {
pub async fn webfinger(
query: web::Query<WebfingerQuery>,
data: Data<DatabaseHandle>,
data: Data<StateHandle>,
) -> Result<HttpResponse, Error> {
let name = extract_webfinger_name(&query.resource, &data)?;
let db_user = data.read_user(name)?;
static WEBFINGER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^acct:([\p{L}0-9_\.\-]+)@(.*)$").expect("compile regex"));
let captures = WEBFINGER_REGEX
.captures(&query.resource)
.ok_or(WebFingerError::WrongFormat)?;
let account_name = captures.get(1).ok_or(WebFingerError::WrongFormat)?;
let name = account_name.as_str();
let user = local_db_user_from_name(name.to_string()).await?;
Ok(HttpResponse::Ok().json(build_webfinger_response(
query.resource.clone(),
db_user.ap_id.into_inner(),
generate_user_id(&API_DOMAIN, &user.id)?,
)))
}

View file

@ -1,30 +1,55 @@
use activitypub_federation::config::{FederationConfig, FederationMiddleware};
use actix_web::{get, http::KeepAlive, middleware, web, App, Error, HttpResponse, HttpServer};
use activitypub_federation::{
config::{Data, FederationConfig, FederationMiddleware},
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
http_signatures::generate_actor_keypair,
traits::Actor,
};
use activitystreams_kinds::public;
use actix_web::{
get, http::KeepAlive, middleware, post, web, App, Error, HttpResponse, HttpServer,
};
use actix_web_prom::PrometheusMetricsBuilder;
use async_once::AsyncOnce;
use chrono::{DateTime, Utc};
use clap::Parser;
use database::Database;
use entities::post;
use http::{http_get_user, http_post_user_inbox, webfinger};
use objects::person::DbUser;
use objects::person::{DbUser, Person};
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
env,
net::ToSocketAddrs,
sync::{Arc, Mutex},
sync::{Arc, Mutex, OnceLock},
};
use tokio::signal;
use tracing::{info, instrument::WithSubscriber};
use url::Url;
use utils::generate_object_id;
use uuid::Uuid;
use versia::http::{
create_activity, fetch_post, fetch_user, fetch_versia_post, query_post, versia_inbox,
};
use crate::{
activities::create_post::CreatePost,
database::{Config, State},
objects::post::{Mention, Note},
};
use crate::{activities::follow::Follow, entities::user};
use dotenv::dotenv;
use lazy_static::lazy_static;
mod activities;
mod database;
mod entities;
mod error;
mod http;
mod objects;
mod utils;
#[derive(Debug, Clone)]
struct State {
database: Arc<Database>,
}
mod versia;
#[derive(Debug, Serialize, Deserialize)]
struct Response {
@ -49,52 +74,214 @@ async fn index(_: web::Data<State>) -> actix_web::Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().json(Response { health: true }))
}
const DOMAIN: &str = "example.com";
const LOCAL_USER_NAME: &str = "example";
#[get("/test/postmanually/{user}/{post}")]
async fn post_manually(
path: web::Path<(String, String)>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let local_user = state.local_user().await?;
let data = FEDERATION_CONFIG.get().unwrap();
let target =
webfinger_resolve_actor::<State, user::Model>(path.0.as_str(), &data.to_request_data())
.await?;
let mention = Mention {
href: Url::parse(&target.id)?,
kind: Default::default(),
};
// TODO change
let uuid = uuid::Uuid::now_v7().to_string();
let id: ObjectId<post::Model> = generate_object_id(data.domain(), &uuid)?.into();
let note = Note {
kind: Default::default(),
id: id.clone(),
sensitive: Some(false),
attributed_to: Url::parse(&local_user.id).unwrap().into(),
to: vec![public(), mention.href.clone()],
content: format!("{} {}", path.1, target.name),
tag: vec![mention],
in_reply_to: None,
cc: vec![].into(),
};
let post = entities::post::ActiveModel {
id: Set(uuid),
creator: Set(local_user.id.clone()),
content: Set(note.content.clone()),
sensitive: Set(false),
created_at: Set(Utc::now()),
local: Set(true),
updated_at: Set(Some(Utc::now())),
content_type: Set("Note".to_string()),
visibility: Set("public".to_string()),
url: Set(id.to_string()),
ap_json: Set(Some(serde_json::to_string(&note).unwrap())),
..Default::default()
};
let post = post.insert(DB.get().unwrap()).await?;
CreatePost::send(
note,
post,
target.shared_inbox_or_inbox(),
&data.to_request_data(),
)
.await?;
Ok(HttpResponse::Ok().json(Response { health: true }))
}
#[get("/favicon")]
async fn favicon() -> actix_web::Result<actix_files::NamedFile> {
Ok(actix_files::NamedFile::open("static/favicon.ico")?)
}
#[get("/test/follow/{user}")]
async fn follow_manually(
path: web::Path<String>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let local_user = state.local_user().await?;
let data = FEDERATION_CONFIG.get().unwrap();
let followee =
webfinger_resolve_actor::<State, user::Model>(path.as_str(), &data.to_request_data())
.await?;
let followee_object: ObjectId<user::Model> = Url::parse(&followee.url)?.into();
let localuser_object: ObjectId<user::Model> = Url::parse(&local_user.url)?.into();
Follow::send(
localuser_object,
followee_object,
followee.shared_inbox_or_inbox(),
&data.to_request_data(),
)
.await?;
Ok(HttpResponse::Ok().json(Response { health: true }))
}
const DOMAIN_DEF: &str = "versia.social";
const LOCAL_USER_NAME: &str = "apservice";
lazy_static! {
static ref SERVER_URL: String = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_string());
static ref DATABASE_URL: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
static ref USERNAME: String =
env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string());
static ref API_DOMAIN: String = env::var("API_DOMAIN").expect("not set API_DOMAIN");
static ref AUTH: String = env::var("AUTH").expect("not set AUTH");
static ref LYSAND_DOMAIN: String = env::var("LYSAND_DOMAIN").expect("not set LYSAND_DOMAIN");
static ref FEDERATED_DOMAIN: String =
env::var("FEDERATED_DOMAIN").unwrap_or(API_DOMAIN.to_string());
}
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
static FEDERATION_CONFIG: OnceLock<FederationConfig<State>> = OnceLock::new();
#[actix_web::main]
async fn main() -> actix_web::Result<(), anyhow::Error> {
dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let server_url = env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string());
let uuid = Uuid::parse_str("019116ea-3bf6-7ba3-b437-2cd7aaf40f80")?;
let local_user = DbUser::new(
env::var("FEDERATED_DOMAIN")
.unwrap_or(DOMAIN.to_string())
.as_str(),
env::var("LOCAL_USER_NAME")
.unwrap_or(LOCAL_USER_NAME.to_string())
.as_str(),
)
.unwrap();
let ap_id = Url::parse(&format!(
"https://{}/apbridge/user/{}",
API_DOMAIN.to_string(),
&uuid.to_string()
))?;
let inbox = Url::parse(&format!(
"https://{}/{}/inbox",
API_DOMAIN.to_string(),
&USERNAME.to_string()
))?;
let keypair = generate_actor_keypair()?;
let database = Arc::new(Database {
users: Mutex::new(vec![local_user]),
});
let ap_json = Person {
id: ap_id.clone().into(),
preferred_username: USERNAME.to_string(),
name: "Test account <3".to_string(),
inbox: inbox.clone(),
public_key: activitypub_federation::protocol::public_key::PublicKey {
owner: ap_id.clone(),
public_key_pem: keypair.public_key.clone(),
id: format!("{}#main-key", ap_id.clone()),
},
summary: Some("Test account <3".to_string()),
url: ap_id.clone(),
kind: Default::default(),
indexable: Some(false),
discoverable: Some(false),
icon: None,
image: None,
attachment: None,
tag: None,
endpoints: None,
followers: None,
following: None,
featured: None,
outbox: None,
also_known_as: None,
featured_tags: None,
manually_approves_followers: Some(false),
};
let state = State { database };
let user = entities::user::ActiveModel {
id: Set(uuid.to_string()),
username: Set(USERNAME.to_string()),
name: Set("Test account <3".to_string()),
inbox: Set(inbox.to_string()),
public_key: Set(keypair.public_key.clone()),
private_key: Set(Some(keypair.private_key.clone())),
last_refreshed_at: Set(Utc::now()),
follower_count: Set(0),
following_count: Set(0),
url: Set(ap_id.to_string()),
local: Set(true),
created_at: Set(Utc::now()),
ap_json: Set(Some(serde_json::to_string(&ap_json).unwrap())),
..Default::default()
};
let db = sea_orm::Database::connect(DATABASE_URL.to_string()).await?;
info!("Connected to database: {:?}", db);
DB.set(db)
.expect("We were not able to save the DB conn into memory");
let db = DB.get().unwrap();
let user = user.insert(db).await;
if let Err(err) = user {
eprintln!("Error inserting user: {:?}", err);
} else {
info!("User inserted: {:?}", user.unwrap());
}
let state: State = State {
database_connection: Arc::new(db.clone()),
};
let data = FederationConfig::builder()
.domain(env::var("FEDERATED_DOMAIN").expect("FEDERATED_DOMAIN must be set"))
.app_data(state.clone().database)
.domain(FEDERATED_DOMAIN.to_string())
.app_data(state.clone())
.http_signature_compat(true)
.signed_fetch_actor(&state.local_user().await.unwrap())
.build()
.await?;
let mut labels = HashMap::new();
labels.insert(
"domain".to_string(),
env::var("FEDERATED_DOMAIN")
.expect("FEDERATED_DOMAIN must be set")
.to_string(),
);
labels.insert(
"name".to_string(),
env::var("LOCAL_USER_NAME")
.expect("LOCAL_USER_NAME must be set")
.to_string(),
);
let _ = FEDERATION_CONFIG.set(data.clone());
let prometheus = PrometheusMetricsBuilder::new("api")
let mut labels = HashMap::new();
labels.insert("domain".to_string(), FEDERATED_DOMAIN.to_string());
labels.insert("name".to_string(), USERNAME.to_string());
labels.insert("api_domain".to_string(), API_DOMAIN.to_string());
let prometheus = PrometheusMetricsBuilder::new("activitypub_bridge")
.endpoint("/metrics")
.const_labels(labels)
.build()
@ -106,12 +293,24 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
.wrap(middleware::Logger::default()) // enable logger
.wrap(prometheus.clone())
.wrap(FederationMiddleware::new(data.clone()))
.service(post_manually)
.service(versia_inbox)
.service(follow_manually)
.route("/{user}", web::get().to(http_get_user))
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
.route(
"/apbridge/{user}/inbox",
web::post().to(http_post_user_inbox),
)
.route("/.well-known/webfinger", web::get().to(webfinger))
.service(index)
.service(fetch_post)
.service(fetch_user)
.service(create_activity)
.service(query_post)
.service(fetch_versia_post)
})
.bind(&server_url)?
.bind(SERVER_URL.to_string())?
.workers(num_cpus::get())
.shutdown_timeout(20)
.keep_alive(KeepAlive::Os)
@ -127,5 +326,7 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
}
}
info!("Main thread shutdown..");
Ok(())
}

View file

@ -1,4 +1,13 @@
use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error};
use crate::{
activities::{
create_post::CreatePost,
follow::{self, Follow},
},
database::{State, StateHandle},
entities::{self, user},
error::Error,
API_DOMAIN,
};
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
@ -7,15 +16,20 @@ use activitypub_federation::{
protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, Object},
};
use chrono::{DateTime, Utc};
use actix_web::http::header::Accept;
use chrono::{prelude, DateTime, Utc};
use entities::prelude::User;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use tracing::info;
use url::Url;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct DbUser {
pub name: String,
pub ap_id: ObjectId<DbUser>,
pub ap_id: ObjectId<user::Model>,
pub inbox: Url,
// exists for all users (necessary to verify http signatures)
pub public_key: String,
@ -32,6 +46,8 @@ pub struct DbUser {
#[enum_delegate::implement(ActivityHandler)]
pub enum PersonAcceptedActivities {
CreateNote(CreatePost),
Follow(Follow),
Accept(follow::Accept),
}
impl DbUser {
@ -56,16 +72,85 @@ impl DbUser {
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(rename = "type")]
kind: PersonType,
preferred_username: String,
id: ObjectId<DbUser>,
inbox: Url,
public_key: PublicKey,
pub kind: PersonType,
pub preferred_username: String,
pub name: String,
pub summary: Option<String>,
pub url: Url,
pub id: ObjectId<user::Model>,
pub inbox: Url,
pub public_key: PublicKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discoverable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manually_approves_followers: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub followers: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub following: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub featured: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoints: Option<EndpointType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outbox: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub featured_tags: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<Vec<TagType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<IconType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<IconType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<Vec<AttachmentType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub also_known_as: Option<Vec<Url>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TagType {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<Url>,
pub name: String,
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<IconType>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointType {
pub shared_inbox: Url,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IconType {
#[serde(rename = "type")]
pub type_: String, //Always "Image"
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
pub url: Url,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AttachmentType {
#[serde(rename = "type")]
pub type_: String, //Always "PropertyValue"
pub name: String,
pub value: String,
}
#[async_trait::async_trait]
impl Object for DbUser {
type DataType = DatabaseHandle;
impl Object for user::Model {
type DataType = StateHandle;
type Kind = Person;
type Error = Error;
@ -77,22 +162,21 @@ impl Object for DbUser {
object_id: Url,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
let users = data.users.lock().unwrap();
let res = users
.clone()
.into_iter()
.find(|u| u.ap_id.inner() == &object_id);
println!("!!!!!!!!Reading user from id!!!!!!!!!!!: {}", object_id);
let res = entities::prelude::User::find()
.filter(entities::user::Column::Url.eq(object_id.to_string()))
.one(data.database_connection.as_ref())
.await?;
println!(
"!!!!!!!!Reading user from id!!!!!!!!!!!: {}",
res.clone().is_some()
);
Ok(res)
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(Person {
preferred_username: self.name.clone(),
kind: Default::default(),
id: self.ap_id.clone(),
inbox: self.inbox.clone(),
public_key: self.public_key(),
})
let serialized = serde_json::from_str(self.ap_json.as_ref().unwrap().as_str())?;
Ok(serialized)
}
async fn verify(
@ -104,26 +188,50 @@ impl Object for DbUser {
Ok(())
}
async fn from_json(
json: Self::Kind,
_data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
Ok(DbUser {
name: json.preferred_username,
ap_id: json.id,
inbox: json.inbox,
public_key: json.public_key.public_key_pem,
private_key: None,
last_refreshed_at: Utc::now(),
followers: vec![],
local: false,
})
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
let query = User::find()
.filter(user::Column::Url.eq(json.id.inner().as_str()))
.one(data.database_connection.as_ref())
.await?;
if let Some(user) = query {
return Ok(user);
}
let copied_json = json.clone();
let model = user::ActiveModel {
id: Set(Uuid::now_v7().to_string()),
username: Set(json.preferred_username),
name: Set(json.name),
inbox: Set(json.inbox.to_string()),
public_key: Set(json.public_key.public_key_pem),
local: Set(false),
summary: Set(json.summary),
url: Set(json.id.to_string()),
follower_count: Set(0),
following_count: Set(0),
created_at: Set(Utc::now()),
last_refreshed_at: Set(Utc::now()),
ap_json: Set(Some(serde_json::to_string(&copied_json).unwrap())),
..Default::default()
};
let model = model.insert(data.database_connection.as_ref()).await;
if let Err(err) = model {
eprintln!("Error inserting user: {:?}", err);
Err(err.into())
} else {
info!("User inserted: {:?}", model.as_ref().unwrap());
Ok(model.unwrap())
}
}
}
impl Actor for DbUser {
impl Actor for user::Model {
fn id(&self) -> Url {
self.ap_id.inner().clone()
Url::parse(&format!(
"https://{}/apbridge/user/{}",
API_DOMAIN.to_string(),
&self.id
))
.unwrap()
}
fn public_key_pem(&self) -> &str {
@ -135,6 +243,11 @@ impl Actor for DbUser {
}
fn inbox(&self) -> Url {
self.inbox.clone()
Url::parse(&self.inbox).unwrap()
}
//TODO: Differenciate shared inbox
fn shared_inbox(&self) -> Option<Url> {
None
}
}

View file

@ -1,6 +1,11 @@
use crate::{
activities::create_post::CreatePost, database::DatabaseHandle, error::Error,
objects::person::DbUser, utils::generate_object_id,
activities::create_post::CreatePost,
database::StateHandle,
entities::{post, prelude::Post, user},
error::Error,
objects::person::DbUser,
utils::generate_object_id,
versia::conversion::db_user_from_url,
};
use activitypub_federation::{
config::Data,
@ -10,29 +15,43 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use activitystreams_kinds::link::MentionType;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use tracing::info;
use url::Url;
use uuid::Uuid;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DbPost {
pub text: String,
pub ap_id: ObjectId<DbPost>,
pub creator: ObjectId<DbUser>,
pub ap_id: ObjectId<post::Model>,
pub creator: ObjectId<user::Model>,
pub local: bool,
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "type")]
kind: NoteType,
id: ObjectId<DbPost>,
pub(crate) attributed_to: ObjectId<DbUser>,
pub(crate) kind: NoteType,
pub(crate) id: ObjectId<post::Model>,
pub(crate) attributed_to: ObjectId<user::Model>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
content: String,
in_reply_to: Option<ObjectId<DbPost>>,
tag: Vec<Mention>,
pub(crate) content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) in_reply_to: Option<ObjectId<post::Model>>,
pub(crate) tag: Vec<Mention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) sensitive: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) cc: Option<Vec<Url>>,
}
impl Note {
pub fn from_db(post: &post::Model) -> Self {
serde_json::from_str(&post.ap_json.as_ref().unwrap()).unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -43,20 +62,48 @@ pub struct Mention {
}
#[async_trait::async_trait]
impl Object for DbPost {
type DataType = DatabaseHandle;
impl Object for post::Model {
type DataType = StateHandle;
type Kind = Note;
type Error = Error;
async fn read_from_id(
_object_id: Url,
_data: &Data<Self::DataType>,
object_id: Url,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(None)
let post = crate::entities::prelude::Post::find()
.filter(post::Column::Id.eq(object_id.to_string()))
.one(data.app_data().database_connection.clone().as_ref())
.await;
Ok(post.unwrap())
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
unimplemented!()
let creator = db_user_from_url(Url::parse(self.creator.as_str()).unwrap()).await?;
let to = match self.visibility.as_str() {
"public" => vec![
public(),
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
],
"followers" => vec![Url::parse(creator.followers.unwrap().as_str()).unwrap()],
"direct" => vec![], //TODO: implement this
"unlisted" => vec![
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
public(),
],
_ => vec![public()],
};
Ok(Note {
kind: Default::default(),
id: Url::parse(self.url.as_str()).unwrap().into(),
attributed_to: Url::parse(self.creator.as_str()).unwrap().into(),
to: to.clone(),
content: self.content,
in_reply_to: None,
tag: vec![],
sensitive: Some(self.sensitive),
cc: Some(to),
})
}
async fn verify(
@ -73,29 +120,37 @@ impl Object for DbPost {
"Received post with content {} and id {}",
&json.content, &json.id
);
let query = Post::find()
.filter(post::Column::Url.eq(json.id.inner().as_str()))
.one(data.database_connection.as_ref())
.await?;
if let Some(post) = query {
return Ok(post);
}
let creator = json.attributed_to.dereference(data).await?;
let post = DbPost {
text: json.content,
ap_id: json.id.clone(),
creator: json.attributed_to.clone(),
local: false,
let post: post::ActiveModel = post::ActiveModel {
content: Set(json.content.clone()),
id: Set(Uuid::now_v7().to_string()),
creator: Set(creator.id.to_string()),
created_at: Set(chrono::Utc::now()), //TODO: make this use the real timestamp
content_type: Set("text/plain".to_string()), // TODO: make this use the real content type
local: Set(false),
visibility: Set("public".to_string()), // TODO: make this use the real visibility
sensitive: Set(json.sensitive.clone().unwrap_or_default()),
url: Set(json.id.clone().to_string()),
ap_json: Set(Some(serde_json::to_string(&json).unwrap())),
..Default::default()
};
let post = post
.insert(data.app_data().database_connection.clone().as_ref())
.await;
let mention = Mention {
href: creator.ap_id.clone().into_inner(),
kind: Default::default(),
};
let note = Note {
kind: Default::default(),
id: generate_object_id(data.domain())?.into(),
attributed_to: data.local_user().ap_id,
to: vec![public()],
content: format!("Hello {}", creator.name),
in_reply_to: Some(json.id.clone()),
tag: vec![mention],
};
CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;
if let Err(err) = post {
eprintln!("Error inserting post: {:?}", err);
return Err(err.into());
}
info!("Post inserted: {:?}", post.as_ref().unwrap());
Ok(post)
Ok(post.unwrap())
}
}

View file

@ -1,13 +1,55 @@
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use url::{ParseError, Url};
/// Just generate random url as object id. In a real project, you probably want to use
/// an url which contains the database id for easy retrieval (or store the random id in db).
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
let id: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
Url::parse(&format!("https://{}/objects/{}", domain, id))
pub fn generate_object_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
Url::parse(&format!("https://{}/apbridge/object/{}", domain, uuid))
}
pub fn generate_user_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
Url::parse(&format!("https://{}/apbridge/user/{}", domain, uuid))
}
pub fn generate_random_object_id(domain: &str) -> Result<Url, ParseError> {
let id: String = uuid::Uuid::new_v4().to_string();
generate_object_id(domain, &id)
}
/// Generate a follow accept id
pub fn generate_follow_accept_id(domain: &str, db_id: &str) -> Result<Url, ParseError> {
Url::parse(&format!("https://{}/apbridge/follow/{}", domain, db_id))
}
pub fn generate_follow_req_id(domain: &str, db_id: &str) -> Result<Url, ParseError> {
Url::parse(&format!("https://{}/apbridge/followreq/{}", domain, db_id))
}
pub fn generate_versia_post_url(domain: &str, db_id: &str) -> Result<Url, ParseError> {
Url::parse(&format!(
"https://{}/apbridge/versia/object/{}",
domain, db_id
))
}
// TODO for later aprl: needs to be base64url!!!
pub fn generate_create_id(
domain: &str,
create_db_id: &str,
basesixfour_url: &str,
) -> Result<Url, ParseError> {
Url::parse(&format!(
"https://{}/apbridge/create/{}/{}",
domain, create_db_id, basesixfour_url
))
}
pub fn generate_random_create_id(domain: &str, basesixfour_url: &str) -> Result<Url, ParseError> {
let id: String = uuid::Uuid::new_v4().to_string();
generate_create_id(domain, &id, basesixfour_url)
}
pub fn base_url_encode(url: &Url) -> String {
base64_url::encode(&url.to_string())
}
pub fn base_url_decode(encoded: &str) -> String {
String::from_utf8(base64_url::decode(encoded).unwrap()).unwrap()
}

678
src/versia/conversion.rs Normal file
View file

@ -0,0 +1,678 @@
use std::fmt::format;
use activitypub_federation::{
fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, traits::Object,
};
use activitystreams_kinds::public;
use anyhow::{anyhow, Ok};
use async_recursion::async_recursion;
use chrono::{DateTime, TimeZone, Utc};
use reqwest::header::{self, CONTENT_TYPE};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use serde_json::to_string;
use time::OffsetDateTime;
use tracing::info;
use url::Url;
use crate::{
database::State,
entities::{self, post, prelude, user},
objects::{
self,
person::{AttachmentType, EndpointType, IconType, Person, TagType},
post::Mention,
},
utils::{generate_object_id, generate_user_id, generate_versia_post_url},
API_DOMAIN, DB, FEDERATION_CONFIG, LOCAL_USER_NAME, LYSAND_DOMAIN, USERNAME,
};
use super::{
objects::{CategoryType, ContentEntry, ContentFormat, Note, PublicKey, UserCollections},
superx::request_client,
};
pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::User> {
let req_client = request_client();
let request = req_client.get(url).send().await?;
Ok(request.json::<super::objects::User>().await?)
}
pub async fn versia_post_from_db(
post: entities::post::Model,
) -> anyhow::Result<super::objects::Note> {
let data = FEDERATION_CONFIG.get().unwrap();
let domain = data.domain();
let url = generate_versia_post_url(domain, &post.id)?;
let creator = prelude::User::find()
.filter(entities::user::Column::Id.eq(post.creator.clone()))
.one(DB.get().unwrap())
.await?;
let author = Url::parse(&creator.unwrap().url)?;
let group = match post.visibility.as_str() {
"public" => Some("public".to_string()),
"followers" => Some("followers".to_string()),
"direct" => None,
//"unlisted" => super::objects::VisibilityType::Unlisted,
_ => Some("public".to_string()),
};
let mut mentions = Vec::new();
let ap_obj =
serde_json::from_str::<crate::objects::post::Note>(post.ap_json.unwrap().as_str())?;
let req_data = data.to_request_data();
for obj in ap_obj.tag.clone() {
info!("Url: {}", obj.href);
let option = user::Model::read_from_id(obj.href.clone(), &req_data)
.await
.unwrap();
if let Some(model) = option {
info!("Model: {:?}", model);
let user = versia_user_from_db(model).await?;
let domain = user.inbox.domain();
//if domain.is_none() || domain.is_some_and(|domain| LYSAND_DOMAIN.as_str() != domain) {
// continue;
//} TODO
mentions.push(user.inbox);
} else if let Some(model) = entities::prelude::User::find()
.filter(
entities::user::Column::Id.eq(obj.href.path_segments().unwrap().last().unwrap()),
)
.one(data.database_connection.as_ref())
.await?
{
info!("Model: {:?}", model);
let user = versia_user_from_db(model).await?;
let domain = user.inbox.domain();
//if domain.is_none() || domain.is_some_and(|domain| LYSAND_DOMAIN.as_str() != domain) {
// continue;
//} TODO
mentions.push(user.inbox);
}
}
let mut content = ContentFormat::default();
content.x.insert(
"text/html".to_string(),
ContentEntry::from_string(post.content),
);
let note = super::objects::Note {
rtype: "Note".to_string(),
id: uuid::Uuid::parse_str(&post.id)?,
author: author.clone(),
uri: url.clone(),
created_at: OffsetDateTime::from_unix_timestamp(post.created_at.timestamp()).unwrap(),
content: Some(content),
mentions: Some(mentions),
category: Some(CategoryType::Microblog),
device: None,
previews: None,
replies_to: None,
quotes: None,
group,
attachments: None,
subject: post.title,
is_sensitive: Some(post.sensitive),
};
Ok(note)
}
pub async fn versia_user_from_db(
user: entities::user::Model,
) -> anyhow::Result<super::objects::User> {
let url = Url::parse(&user.url)?;
let ap = user.ap_json.unwrap();
let serialized_ap: crate::objects::person::Person = serde_json::from_str(&ap)?;
let inbox_url;
let outbox_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/outbox/" + &user.id).as_str(),
)?;
let followers_url;
let following_url;
let featured_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/featured/" + &user.id).as_str(),
)?;
let likes_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/likes/" + &user.id).as_str(),
)?;
let dislikes_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/dislikes/" + &user.id).as_str(),
)?;
if user.local {
inbox_url = Url::parse(&user.inbox)?;
followers_url = Url::parse(&user.followers.unwrap())?;
following_url = Url::parse(&user.following.unwrap())?;
} else {
inbox_url = Url::parse(&("https://".to_string() + &API_DOMAIN + "/apbridge/versia/inbox"))?;
followers_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/followers/" + &user.id)
.as_str(),
)?;
following_url = Url::parse(
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/following/" + &user.id)
.as_str(),
)?;
}
let og_displayname_ref = user.name.clone();
let og_username_ref = user.username.clone();
let empty = "".to_owned();
// linter was having a stroke
let display_name = match og_displayname_ref {
og_username_ref => None,
empty => None,
_ => Some(user.name),
};
let mut bio = ContentFormat::default();
bio.x.insert(
"text/html".to_string(),
ContentEntry::from_string(user.summary.unwrap_or_default()),
);
let avatar = match serialized_ap.icon {
Some(icon) => {
let mut content_format = ContentFormat::default();
let content_entry = ContentEntry::from_string(icon.url.to_string());
let media_type = icon.media_type.unwrap_or({
let req = request_client().get(icon.url.clone()).build()?;
let res = request_client().execute(req).await?;
let headers = res.headers();
let content_type_header = headers.get(CONTENT_TYPE);
content_type_header.unwrap().to_str().unwrap().to_string()
});
content_format.x.insert(media_type, content_entry);
Some(content_format)
}
None => None,
};
let header = match serialized_ap.image {
Some(image) => {
let mut content_format = ContentFormat::default();
let content_entry = ContentEntry::from_string(image.url.to_string());
let media_type = image.media_type.unwrap_or({
let req = request_client().get(image.url.clone()).build()?;
let res = request_client().execute(req).await?;
let headers = res.headers();
let content_type_header = headers.get(CONTENT_TYPE);
content_type_header.unwrap().to_str().unwrap().to_string()
});
content_format.x.insert(media_type, content_entry);
Some(content_format)
}
None => None,
};
let mut fields = Vec::new();
if let Some(attachments) = serialized_ap.attachment {
for attachment in attachments {
let mut key = ContentFormat::default();
let mut value = ContentFormat::default();
key.x.insert(
"text/html".to_string(),
ContentEntry::from_string(attachment.name),
);
value.x.insert(
"text/html".to_string(),
ContentEntry::from_string(attachment.value),
);
fields.push(super::objects::FieldKV { key, value });
}
}
let emojis = match serialized_ap.tag {
Some(tags) => {
let mut emojis = Vec::new();
for tag in tags {
let mut content_format = ContentFormat::default();
if tag.icon.is_none() {
continue;
}
let content_entry =
ContentEntry::from_string(tag.icon.clone().unwrap().url.to_string());
let icon = tag.icon.unwrap();
let media_type = icon.media_type.unwrap_or({
let req = request_client().get(icon.url.clone()).build()?;
let res = request_client().execute(req).await?;
let headers = res.headers();
let content_type_header = headers.get(CONTENT_TYPE);
if content_type_header.is_none() {
continue;
}
content_type_header.unwrap().to_str().unwrap().to_string()
});
content_format.x.insert(media_type, content_entry);
let name = tag.name;
emojis.push(super::objects::CustomEmoji {
name,
url: content_format,
});
}
Some(super::objects::CustomEmojis { emojis })
}
None => None,
};
let extensions = super::objects::ExtensionSpecs {
custom_emojis: emojis,
};
let collections = UserCollections {
outbox: outbox_url,
followers: followers_url,
following: following_url,
featured: featured_url,
likes: Some(likes_url),
dislikes: Some(dislikes_url),
};
let user = super::objects::User {
rtype: "User".to_string(),
id: uuid::Uuid::parse_str(&user.id)?,
uri: url.clone(),
username: user.username,
display_name,
inbox: inbox_url,
bio: Some(bio),
collections,
avatar,
header,
fields: Some(fields),
indexable: false,
created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(),
public_key: PublicKey {
actor: url.clone(),
key: "AAAAC3NzaC1lZDI1NTE5AAAAIMxsX+lEWkHZt9NOvn9yYFP0Z++186LY4b97C4mwj/f2".to_string(), // dummy key
algorithm: "ed25519".to_string(),
},
extensions: Some(extensions),
manually_approves_followers: false,
};
Ok(user)
}
pub async fn option_content_format_text(opt: Option<ContentFormat>) -> Option<String> {
if let Some(format) = opt {
return Some(format.select_rich_text().await.unwrap());
}
None
}
#[async_recursion]
pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model> {
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
return Err(anyhow!("not versias domain"));
}
let str_url = url.to_string();
let post_res: Option<post::Model> = prelude::Post::find()
.filter(entities::post::Column::Url.eq(str_url.clone()))
.one(DB.get().unwrap())
.await?;
if let Some(post) = post_res {
Ok(post)
} else {
let post = fetch_note_from_url(url.clone()).await?;
let res =
receive_versia_note(post, "https://".to_string() + &API_DOMAIN + "/example").await?; // TODO: Replace user id with actual user id
Ok(res)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ApiUser {
uri: Url,
}
pub async fn local_db_user_from_name(name: String) -> anyhow::Result<entities::user::Model> {
let user_res: Option<user::Model> = prelude::User::find()
.filter(entities::user::Column::Username.eq(name.clone()))
.filter(entities::user::Column::Local.eq(true))
.one(DB.get().unwrap())
.await?;
if let Some(user) = user_res {
Ok(user)
} else {
let client = request_client();
let api_url = Url::parse(&format!(
"https://{}/api/v1/accounts/id?username={}",
LYSAND_DOMAIN.to_string(),
name
))?;
let request = client.get(api_url).send().await?;
let user_json = request.json::<ApiUser>().await?;
Ok(db_user_from_url(user_json.uri).await?)
}
}
pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model> {
println!("Fetching user from domain: {}", url.domain().unwrap());
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str()))
&& !url.domain().eq(&Some(API_DOMAIN.as_str()))
{
return Err(anyhow!("not versias domain"));
}
let user_res: Option<user::Model> = prelude::User::find()
.filter(entities::user::Column::Url.eq(url.to_string()))
.one(DB.get().unwrap())
.await?;
if let Some(user) = user_res {
Ok(user)
} else {
let ls_user = fetch_user_from_url(url).await?;
let keypair = generate_actor_keypair()?;
let bridge_user_url = generate_user_id(&API_DOMAIN, &ls_user.id.to_string())?;
let inbox = Url::parse(&format!(
"https://{}/{}/inbox",
API_DOMAIN.to_string(),
ls_user.username.clone()
))?;
let icon = if let Some(avatar) = ls_user.avatar {
let avatar_url = avatar.select_rich_img_touple().await?;
Some(IconType {
type_: "Image".to_string(),
media_type: Some(avatar_url.0),
url: Url::parse(&avatar_url.1).unwrap(),
})
} else {
None
};
let image = if let Some(header) = ls_user.header {
let header_url = header.select_rich_img_touple().await?;
Some(IconType {
type_: "Image".to_string(),
media_type: Some(header_url.0),
url: Url::parse(&header_url.1).unwrap(),
})
} else {
None
};
let mut attachments: Vec<AttachmentType> = Vec::new();
if let Some(fields) = ls_user.fields {
for attachment in fields {
attachments.push(AttachmentType {
type_: "PropertyValue".to_string(),
name: attachment.key.select_rich_text().await?,
value: attachment.value.select_rich_text().await?,
});
}
}
let mut tags: Vec<TagType> = Vec::new();
if let Some(extensions) = ls_user.extensions {
if let Some(custom_emojis) = extensions.custom_emojis {
for emoji in custom_emojis.emojis {
let touple = emoji.url.select_rich_img_touple().await?;
tags.push(TagType {
id: Some(Url::parse(&touple.1).unwrap()),
name: emoji.name,
type_: "Emoji".to_string(),
updated: Some(Utc::now()),
href: None,
icon: Some(IconType {
type_: "Image".to_string(),
media_type: Some(touple.0),
url: Url::parse(&touple.1).unwrap(),
}),
});
}
}
}
let ap_json = Person {
kind: Default::default(),
id: bridge_user_url.clone().into(),
preferred_username: ls_user.username.clone(),
inbox,
public_key: activitypub_federation::protocol::public_key::PublicKey {
owner: bridge_user_url.clone(),
public_key_pem: keypair.public_key.clone(),
id: format!("{}#main-key", bridge_user_url.clone()),
},
name: ls_user
.display_name
.clone()
.unwrap_or(ls_user.username.clone()),
summary: option_content_format_text(ls_user.bio.clone()).await,
url: ls_user.uri.clone(),
indexable: Some(ls_user.indexable),
discoverable: Some(true),
manually_approves_followers: Some(false),
followers: None,
following: None,
featured: None,
featured_tags: None,
also_known_as: None,
outbox: None,
endpoints: Some(EndpointType {
shared_inbox: Url::parse(
&format!(
"https://{}/{}/inbox",
API_DOMAIN.to_string(),
&USERNAME.to_string()
)
.as_str(),
)
.unwrap(),
}),
icon,
image,
attachment: Some(attachments),
tag: Some(tags),
};
let user = entities::user::ActiveModel {
id: Set(ls_user.id.to_string()),
username: Set(ls_user.username.clone()),
name: Set(ls_user.display_name.unwrap_or(ls_user.username)),
inbox: Set(ls_user.inbox.to_string()),
public_key: Set(keypair.public_key.clone()),
private_key: Set(Some(keypair.private_key.clone())),
last_refreshed_at: Set(Utc::now()),
follower_count: Set(0),
following_count: Set(0),
url: Set(ls_user.uri.to_string()),
local: Set(true),
created_at: Set(
DateTime::from_timestamp(ls_user.created_at.unix_timestamp(), 0).unwrap(),
),
summary: Set(option_content_format_text(ls_user.bio).await),
updated_at: Set(Some(Utc::now())),
followers: Set(Some(ls_user.collections.followers.to_string())),
following: Set(Some(ls_user.collections.following.to_string())),
ap_json: Set(Some(serde_json::to_string(&ap_json).unwrap())),
..Default::default()
};
let db = DB.get().unwrap();
Ok(user.insert(db).await?)
}
}
pub async fn fetch_note_from_url(url: Url) -> anyhow::Result<super::objects::Note> {
let req_client = request_client();
let request = req_client.get(url).send().await?;
Ok(request.json::<super::objects::Note>().await?)
}
#[async_recursion]
pub async fn receive_versia_note(
note: Note,
db_id: String,
) -> anyhow::Result<entities::post::Model> {
let post_res: Option<post::Model> = prelude::Post::find()
.filter(entities::post::Column::Id.eq(note.id.to_string()))
.one(DB.get().unwrap())
.await?;
if let Some(post) = post_res {
return Ok(post);
}
let versia_author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
let user_res = prelude::User::find_by_id(db_id)
.one(DB.get().unwrap())
.await;
if user_res.is_err() {
println!("{}", user_res.as_ref().unwrap_err());
return Err(user_res.err().unwrap().into());
}
if let Some(target) = user_res? {
let data = FEDERATION_CONFIG.get().unwrap();
let id: ObjectId<post::Model> =
generate_object_id(data.domain(), &note.id.to_string())?.into();
let user_id = generate_user_id(data.domain(), &target.id.to_string())?;
println!("{}", note.author.clone());
let user = fetch_user_from_url(note.author.clone()).await?;
let mut tag: Vec<Mention> = Vec::new();
let domain = API_DOMAIN.as_str();
for l_tag in note.mentions.clone().unwrap_or_default() {
if l_tag.clone().to_string().contains("apbridge/user") {
println!("{}", l_tag.clone().to_string().contains("apbridge/user"));
tag.push(Mention {
href: l_tag,
kind: Default::default(),
});
continue;
} else if !(l_tag.clone().to_string().contains(LYSAND_DOMAIN.as_str())
|| l_tag.clone().to_string().contains(domain))
{
println!(
"{}",
l_tag.clone().to_string().contains(LYSAND_DOMAIN.as_str())
);
println!("{}", l_tag.clone().to_string().contains(domain));
println!(
"-------------- {} -----------------a",
l_tag.clone().to_string()
);
tag.push(Mention {
href: l_tag,
kind: Default::default(),
});
continue;
}
println!("+++++++ --------- ++++++++++");
let user = db_user_from_url(l_tag).await?;
let ap_url =
Url::parse(&format!("https://{}/apbridge/user/{}", domain, user.id).to_string())?;
tag.push(Mention {
href: ap_url,
kind: Default::default(),
});
}
let mut mentions = Vec::new();
for obj in tag.clone() {
mentions.push(obj.href.clone());
}
let to = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
"public" => {
let mut vec = vec![
public(),
Url::parse(&user.collections.followers.to_string().as_str())?,
];
vec.append(&mut mentions.clone());
vec
}
"unlisted" => {
let mut vec = vec![Url::parse(
&user.collections.followers.to_string().as_str(),
)?];
vec.append(&mut mentions.clone());
vec
}
"followers" => {
let mut vec = vec![Url::parse(
&user.collections.followers.to_string().as_str(),
)?];
vec.append(&mut mentions.clone());
vec
}
_ => mentions.clone(),
};
let cc = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
"unlisted" => Some(vec![public()]),
_ => None,
};
let reply: Option<ObjectId<entities::post::Model>> =
if let Some(rep) = note.replies_to.clone() {
let note = fetch_note_from_url(rep).await?;
let fake_rep_url = Url::parse(&format!(
"https://{}/apbridge/object/{}",
API_DOMAIN.to_string(),
&note.id.to_string()
))?;
Some(fake_rep_url.into())
} else {
None
};
let quote: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.quotes.clone()
{
let note = fetch_note_from_url(rep).await?;
let fake_rep_url = Url::parse(&format!(
"https://{}/apbridge/object/{}",
API_DOMAIN.to_string(),
&note.id.to_string()
))?;
Some(fake_rep_url.into())
} else {
None
};
let reply_uuid: Option<String> = if let Some(rep) = note.replies_to.clone() {
Some(db_post_from_url(rep).await?.id)
} else {
None
};
let quote_uuid: Option<String> = if let Some(rep) = note.quotes.clone() {
Some(db_post_from_url(rep).await?.id)
} else {
None
};
let ap_note = crate::objects::post::Note {
kind: Default::default(),
id,
sensitive: Some(note.is_sensitive.unwrap_or(false)),
cc,
to,
tag,
attributed_to: {
let user = db_user_from_url(Url::parse(user.uri.clone().as_str()).unwrap()).await?;
let ap_url = Url::parse(
&format!("https://{}/apbridge/user/{}", domain, user.id).to_string(),
)?;
ap_url.into()
},
content: option_content_format_text(note.content)
.await
.unwrap_or_default(),
in_reply_to: reply.clone(),
};
let visibility = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
"public" => "public",
"followers" => "followers",
"unlisted" => "unlisted",
_ => "direct",
};
if let Some(obj) = note.replies_to {
println!("Quoting: {}", db_post_from_url(obj).await?.url);
}
if let Some(obj) = note.quotes {
println!("Replying to: {}", db_post_from_url(obj).await?.url);
}
let post = entities::post::ActiveModel {
id: Set(note.id.to_string()),
creator: Set(versia_author.id.clone()),
content: Set(ap_note.content.clone()),
sensitive: Set(ap_note.sensitive.unwrap_or_default()),
created_at: Set(Utc
.timestamp_micros(note.created_at.unix_timestamp())
.unwrap()),
local: Set(true),
updated_at: Set(Some(Utc::now())),
content_type: Set("Note".to_string()),
visibility: Set(visibility.to_string()),
title: Set(note.subject.clone()),
url: Set(note.uri.clone().to_string()),
reply_id: Set(reply_uuid),
quoting_id: Set(quote_uuid),
spoiler_text: Set(note.subject),
ap_json: Set(Some(serde_json::to_string(&ap_note).unwrap())),
..Default::default()
};
let res = post.insert(DB.get().unwrap()).await?;
Ok(res)
} else {
Err(anyhow!("User not found"))
}
}

62
src/versia/funcs.rs Normal file
View file

@ -0,0 +1,62 @@
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use time::OffsetDateTime;
use url::Url;
use crate::{
entities::{follow_relation, prelude, user},
utils::generate_follow_accept_id,
API_DOMAIN, DB,
};
use super::{
conversion::{fetch_user_from_url, versia_user_from_db},
objects::FollowResult,
superx::request_client,
};
pub async fn send_follow_accept_to_versia(model: follow_relation::Model) -> anyhow::Result<()> {
let request_client = request_client();
let db = DB.get().unwrap();
let id_raw = model.accept_id.unwrap();
let id = uuid::Uuid::parse_str(&id_raw)?;
let uri = generate_follow_accept_id(API_DOMAIN.as_str(), &id_raw)?;
let follower_model = prelude::User::find()
.filter(user::Column::Id.eq(model.follower_id))
.one(db)
.await?
.unwrap();
let versia_follower = fetch_user_from_url(Url::parse(&follower_model.url)?).await?;
let followee_model = prelude::User::find()
.filter(user::Column::Id.eq(model.followee_id))
.one(db)
.await?
.unwrap();
let versia_followee = versia_user_from_db(followee_model).await?;
let entity = FollowResult {
rtype: "FollowAccept".to_string(),
id,
uri,
created_at: OffsetDateTime::now_utc(),
author: versia_followee.uri,
follower: versia_follower.uri,
};
let request = request_client
.post(versia_follower.inbox.as_str())
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.header("Date", entity.created_at.clone().to_string())
.json(&entity);
let response = request.send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(anyhow::anyhow!("Failed to send follow accept to Versia"))
}
}

269
src/versia/http.rs Normal file
View file

@ -0,0 +1,269 @@
use activitypub_federation::{
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
protocol::{context::WithContext, public_key::PublicKey},
traits::Object,
FEDERATION_CONTENT_TYPE,
};
use activitystreams_kinds::{activity::CreateType, object};
use actix_web::{get, post, web, HttpResponse};
use sea_orm::{query, ColumnTrait, EntityTrait, QueryFilter};
use url::Url;
use crate::{
database::State,
entities::{
post::{self, Entity},
prelude, user,
},
error,
objects::{self, person::Person},
utils::{base_url_decode, generate_create_id, generate_user_id},
versia::{
conversion::{versia_post_from_db, versia_user_from_db},
inbox::inbox_entry,
},
Response, API_DOMAIN, DB, FEDERATION_CONFIG,
};
use super::conversion::db_user_from_url;
#[derive(serde::Deserialize)]
struct VersiaQuery {
// Post url
url: Option<Url>,
// User handle
user: Option<String>,
// User URL
user_url: Option<Url>,
}
#[get("/apbridge/versia/query")]
async fn query_post(
query: web::Query<VersiaQuery>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
if query.url.is_none() && query.user.is_none() && query.user_url.is_none() {
return Ok(
HttpResponse::BadRequest().body("Bad Request. Error code: mrrrmrrrmrrawwawwawwa")
);
}
let db = DB.get().unwrap();
let data = FEDERATION_CONFIG.get().unwrap();
if let Some(user) = query.user.clone() {
let target =
webfinger_resolve_actor::<State, user::Model>(user.as_str(), &data.to_request_data())
.await?;
println!("!!!!!!! DB USER GOT");
let versia_user = versia_user_from_db(target).await?;
return Ok(HttpResponse::Ok()
.content_type("application/json")
.json(versia_user));
}
if let Some(user) = query.user_url.clone() {
let versia_user = versia_url_to_user(user).await?;
return Ok(HttpResponse::Ok()
.content_type("application/json")
.json(versia_user));
}
let opt_model = prelude::Post::find()
.filter(post::Column::Url.eq(query.url.clone().unwrap().as_str()))
.one(db)
.await?;
let target;
if let Some(model) = opt_model {
target = model;
} else {
target = ObjectId::<post::Model>::from(Url::parse(query.url.clone().unwrap().as_str())?)
.dereference(&data.to_request_data())
.await?;
}
Ok(HttpResponse::Ok()
.content_type("application/json")
.json(versia_post_from_db(target).await?))
}
#[post("/apbridge/versia/inbox")]
async fn versia_inbox(
body: web::Bytes,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let string = String::from_utf8(body.to_vec())?;
inbox_entry(&string).await?;
Ok(HttpResponse::Created().finish())
}
#[get("/apbridge/object/{post}")]
async fn fetch_post(
path: web::Path<String>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let db = DB.get().unwrap();
let post = prelude::Post::find()
.filter(post::Column::Id.eq(path.as_str()))
.one(db)
.await?;
let post = match post {
Some(post) => post,
None => return Ok(HttpResponse::NotFound().finish()),
};
Ok(HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
.json(crate::objects::post::Note::from_db(&post)))
}
#[get("/apbridge/user/{user}")]
async fn fetch_user(
path: web::Path<String>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let db = DB.get().unwrap();
let user = prelude::User::find()
.filter(user::Column::Id.eq(path.as_str()))
.one(db)
.await?;
let user = match user {
Some(user) => user,
None => return Ok(HttpResponse::NotFound().finish()),
};
let deserialized_user: Person = serde_json::from_str(user.ap_json.as_ref().unwrap().as_str())?;
Ok(HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
.json(WithContext::new_default(deserialized_user)))
}
#[get("/apbridge/versia/object/{post}")]
async fn fetch_versia_post(
path: web::Path<String>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let db = DB.get().unwrap();
let post = prelude::Post::find()
.filter(post::Column::Id.eq(path.as_str()))
.one(db)
.await?;
let post = match post {
Some(post) => post,
None => return Ok(HttpResponse::NotFound().finish()),
};
Ok(HttpResponse::Ok()
.content_type("application/json")
.json(versia_post_from_db(post).await?))
}
#[get("/apbridge/create/{id}/{base64url}")]
async fn create_activity(
path: web::Path<(String, String)>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
let db = DB.get().unwrap();
let url = base_url_decode(path.1.as_str());
let post = prelude::Post::find()
.filter(post::Column::Id.eq(path.0.as_str()))
.one(db)
.await?;
let post = match post {
Some(post) => post,
None => return Ok(HttpResponse::NotFound().finish()),
};
let ap_post = crate::objects::post::Note::from_db(&post);
let data = FEDERATION_CONFIG.get().unwrap();
let create = crate::activities::create_post::CreatePost {
actor: ap_post.attributed_to.clone(),
to: ap_post.to.clone(),
object: ap_post,
kind: CreateType::Create,
id: generate_create_id(&data.to_request_data().domain(), &path.0, &path.1)?,
};
let create_with_context = WithContext::new_default(create);
Ok(HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
.json(create_with_context))
}
pub async fn versia_url_to_user(url: Url) -> anyhow::Result<super::objects::User> {
let db = DB.get().unwrap();
let data = FEDERATION_CONFIG.get().unwrap();
let opt_model = prelude::User::find()
.filter(user::Column::Url.eq(url.as_str()))
.one(db)
.await?;
let target;
if let Some(model) = opt_model {
target = model;
} else {
target = ObjectId::<user::Model>::from(url)
.dereference(&data.to_request_data())
.await
.unwrap();
}
Ok(versia_user_from_db(target).await?)
}
pub async fn versia_url_to_user_and_model(
url: Url,
) -> anyhow::Result<(super::objects::User, user::Model)> {
let db = DB.get().unwrap();
let data = FEDERATION_CONFIG.get().unwrap();
let opt_model = prelude::User::find()
.filter(user::Column::Url.eq(url.to_string()))
.one(db)
.await?;
let target;
if let Some(model) = opt_model {
target = model;
} else {
target = ObjectId::<user::Model>::from(url)
.dereference(&data.to_request_data())
.await
.unwrap();
}
Ok((versia_user_from_db(target.clone()).await?, target))
}
pub async fn main_versia_url_to_user_and_model(
url: Url,
) -> anyhow::Result<(super::objects::User, user::Model)> {
let db = DB.get().unwrap();
let data = FEDERATION_CONFIG.get().unwrap();
let opt_model = prelude::User::find()
.filter(user::Column::Url.eq(url.as_str()))
.one(db)
.await?;
let target;
if let Some(model) = opt_model {
target = model;
} else {
target = db_user_from_url(url.clone()).await?;
}
Ok((versia_user_from_db(target.clone()).await?, target))
}

187
src/versia/inbox.rs Normal file
View file

@ -0,0 +1,187 @@
use crate::{
activities::{create_post::CreatePost, follow::Follow},
entities::{
self, follow_relation,
prelude::{self, FollowRelation},
user,
},
utils::generate_follow_req_id,
versia::http::main_versia_url_to_user_and_model,
API_DOMAIN, DB, FEDERATION_CONFIG,
};
use activitypub_federation::{
activity_sending::SendActivityTask, fetch::object_id::ObjectId, protocol::context::WithContext,
};
use activitystreams_kinds::{activity::FollowType, public};
use anyhow::Result;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityOrSelect, EntityTrait, QueryFilter, Set};
use serde::Deserialize;
use url::Url;
use super::{
conversion::{db_user_from_url, fetch_user_from_url, receive_versia_note, versia_user_from_db},
http::{versia_url_to_user, versia_url_to_user_and_model},
};
pub async fn inbox_entry(json: &str) -> Result<()> {
// Deserialize the JSON string into a dynamic value
let value: serde_json::Value = serde_json::from_str(json).unwrap();
// Extract the "type" field from the JSON
if let Some(json_type) = value.get("type") {
// Match the "type" field with the corresponding VersiaType
match json_type.as_str() {
Some("Note") => {
let note: super::objects::Note = serde_json::from_str(json)?;
federate_inbox(note).await?;
}
Some("Follow") => {
let follow_req: super::objects::Follow = serde_json::from_str(json)?;
follow_request(follow_req).await?;
}
Some("FollowAccept") => {
let follow_accept: super::objects::FollowResult = serde_json::from_str(json)?;
}
Some("FollowReject") => {
let follow_rej: super::objects::FollowResult = serde_json::from_str(json)?;
}
Some("Unfollow") => {
let unfollow: super::objects::Unfollow = serde_json::from_str(json)?;
}
// Add more cases for other types as needed
_ => {
return Err(anyhow::anyhow!(
"Unknown 'type' field in JSON, it is {}",
json_type
));
}
}
} else {
return Err(anyhow::anyhow!("Missing 'type' field in JSON"));
}
Ok(())
}
async fn follow_request(follow: super::objects::Follow) -> Result<()> {
// Check if the user is already following the requester
let db = DB.get().unwrap();
let query = FollowRelation::find()
.filter(follow_relation::Column::FollowerId.eq(follow.author.to_string().as_str()))
.filter(follow_relation::Column::FolloweeId.eq(follow.followee.to_string().as_str()))
.one(db)
.await?;
if query.is_some() {
return Err(anyhow::anyhow!(
"User is already follow requesting / following the followee"
));
}
let data = FEDERATION_CONFIG.get().unwrap();
let author = main_versia_url_to_user_and_model(follow.author.into()).await?;
println!("Followee URL: {}", &follow.followee.to_string());
let followee = versia_url_to_user_and_model(follow.followee.into()).await?;
let serial_ap_author = serde_json::from_str::<crate::objects::person::Person>(
&(author.1.ap_json.clone()).unwrap(),
)?;
let serial_ap_followee = serde_json::from_str::<crate::objects::person::Person>(
&(followee.1.ap_json.clone()).unwrap(),
)?;
let id = uuid::Uuid::now_v7().to_string();
let followee_object: ObjectId<user::Model> = serial_ap_followee.id;
let localuser_object: ObjectId<user::Model> = serial_ap_author.id;
println!(
"Sending follow request to {}",
&followee.0.display_name.unwrap_or(followee.0.username)
);
let create = Follow {
actor: localuser_object.clone(),
object: followee_object.clone(),
kind: FollowType::Follow,
id: generate_follow_req_id(&API_DOMAIN.to_string(), id.clone().as_str())?,
};
let ap_json = serde_json::to_string(&create)?;
let create_with_context = WithContext::new_default(create);
let follow_db_entry = follow_relation::ActiveModel {
id: Set(id.clone()),
followee_id: Set(followee.0.id.to_string()),
follower_id: Set(author.0.id.to_string()),
ap_id: Set(Some(id.clone())),
ap_json: Set(ap_json),
remote: Set(false),
..Default::default()
};
follow_db_entry.insert(db).await?;
let sends = SendActivityTask::prepare(
&create_with_context,
&author.1,
vec![serial_ap_followee.inbox],
&data.to_request_data(),
)
.await?;
for send in sends {
send.sign_and_send(&data.to_request_data()).await?;
}
Ok(())
}
async fn federate_inbox(note: super::objects::Note) -> Result<()> {
let db_user = db_user_from_url(note.author.clone()).await?;
let note = receive_versia_note(note, db_user.id).await?;
let ap_str = note.ap_json.clone().unwrap();
let ap_note = serde_json::from_str::<crate::objects::post::Note>(&ap_str)?;
tokio::spawn(async move {
let conf = FEDERATION_CONFIG.get().unwrap();
let inbox = get_inbox_vec(&ap_note).await;
let res = CreatePost::sends(ap_note, note, inbox, &conf.to_request_data()).await;
if let Err(e) = res {
panic!("Problem federating: {e:?}");
}
});
Ok(())
}
async fn get_inbox_vec(ap_note: &crate::objects::post::Note) -> Vec<Url> {
let mut inbox_users: Vec<Url> = Vec::new();
let mut inbox: Vec<Url> = Vec::new();
let entry = ap_note.to.get(0).unwrap();
if entry
.to_string()
.eq_ignore_ascii_case(public().to_string().as_str())
{
let (_, mentions) = ap_note.to.split_at(2);
inbox_users.append(&mut mentions.to_vec());
} else {
let (_, mentions) = ap_note.to.split_at(1);
inbox_users.append(&mut mentions.to_vec());
}
inbox_users.dedup();
let conf = FEDERATION_CONFIG.get().unwrap();
let data = &conf.to_request_data();
for user in inbox_users {
let ap_user = ObjectId::<user::Model>::from(user)
.dereference(data)
.await
.unwrap();
inbox.push(Url::parse(&ap_user.inbox).unwrap());
}
inbox.dedup();
inbox
}

7
src/versia/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod conversion;
pub mod funcs;
pub mod http;
pub mod inbox;
pub mod objects;
pub mod superx;
pub mod test;

405
src/versia/objects.rs Normal file
View file

@ -0,0 +1,405 @@
extern crate serde; // 1.0.68
extern crate serde_derive; // 1.0.68
use std::{
collections::HashMap,
fmt::{Display, Formatter},
};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use time::{
format_description::well_known::{iso8601, Iso8601},
OffsetDateTime,
};
use url::Url;
use uuid::Uuid;
const FORMAT: Iso8601<6651332276412969266533270467398074368> = Iso8601::<
{
iso8601::Config::DEFAULT
.set_year_is_six_digits(false)
.encode()
},
>;
time::serde::format_description!(iso_versia, OffsetDateTime, FORMAT);
fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
value: &T,
serializer: S,
) -> Result<S::Ok, S::Error> {
let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
value.serialize(serializer)
}
#[derive(Serialize)]
pub struct SortAlphabetically<T: Serialize>(#[serde(serialize_with = "sort_alphabetically")] pub T);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CategoryType {
Microblog,
Forum,
Blog,
Image,
Video,
Audio,
Messaging,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum VersiaExtensions {
#[serde(rename = "pub.versia:share/Share")]
Share,
#[serde(rename = "pub.versia:custom_emojis")]
CustomEmojis,
#[serde(rename = "pub.versia:reactions/Reaction")]
Reaction,
#[serde(rename = "pub.versia:reactions")]
Reactions,
#[serde(rename = "pub.versia:polls")]
Polls,
#[serde(rename = "pub.versia:is_cat")]
IsCat,
#[serde(rename = "pub.versia:server_endorsement/Endorsement")]
Endorsement,
#[serde(rename = "pub.versia:server_endorsement")]
EndorsementCollection,
#[serde(rename = "pub.versia:reports/Report")]
Report,
#[serde(rename = "pub.versia:vanity")]
Vanity,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PublicKey {
pub key: String,
pub actor: Url,
pub algorithm: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ContentHash {
md5: Option<String>,
sha1: Option<String>,
sha256: Option<String>,
sha512: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ContentFormat {
pub x: HashMap<String, ContentEntry>,
}
impl ContentFormat {
pub async fn select_rich_text(&self) -> anyhow::Result<String> {
if let Some(entry) = self.x.get("text/x.misskeymarkdown") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("text/html") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("text/markdown") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("text/plain") {
return Ok(entry.content.clone());
}
Ok(self.x.clone().values().next().unwrap().content.clone())
}
pub async fn select_rich_img(&self) -> anyhow::Result<String> {
if let Some(entry) = self.x.get("image/webp") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/png") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/avif") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/jxl") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/jpeg") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/gif") {
return Ok(entry.content.clone());
}
if let Some(entry) = self.x.get("image/bmp") {
return Ok(entry.content.clone());
}
Ok(self.x.clone().values().next().unwrap().content.clone())
}
pub async fn select_rich_img_touple(&self) -> anyhow::Result<(String, String)> {
if let Some(entry) = self.x.get("image/webp") {
return Ok(("image/webp".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/png") {
return Ok(("image/png".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/avif") {
return Ok(("image/avif".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/jxl") {
return Ok(("image/jxl".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/jpeg") {
return Ok(("image/jpeg".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/gif") {
return Ok(("image/gif".to_string(), entry.content.clone()));
}
if let Some(entry) = self.x.get("image/bmp") {
return Ok(("image/bmp".to_string(), entry.content.clone()));
}
let touple = self.x.iter().next().unwrap();
Ok((touple.0.clone(), touple.1.content.clone()))
}
}
impl Serialize for ContentFormat {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_map(Some(self.x.len()))?;
for (k, v) in &self.x {
seq.serialize_entry(&k.to_string(), &v)?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for ContentFormat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = HashMap::deserialize(deserializer)?;
Ok(ContentFormat { x: map })
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FieldKV {
pub key: ContentFormat,
pub value: ContentFormat,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ContentEntry {
content: String,
remote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
hash: Option<ContentHash>,
#[serde(skip_serializing_if = "Option::is_none")]
blurhash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
fps: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<u64>,
}
impl ContentEntry {
pub fn from_string(string: String) -> ContentEntry {
ContentEntry {
content: string,
remote: false,
description: None,
size: None,
hash: None,
blurhash: None,
fps: None,
width: None,
height: None,
duration: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
pub public_key: PublicKey,
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub uri: Url,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub collections: UserCollections,
pub inbox: Url,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<ContentFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<ContentFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header: Option<ContentFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<FieldKV>>,
pub indexable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<ExtensionSpecs>,
pub manually_approves_followers: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserCollections {
pub outbox: Url,
pub featured: Url,
pub followers: Url,
pub following: Url,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "pub.versia:likes/Likes"
)]
pub likes: Option<Url>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "pub.versia:likes/Dislikes"
)]
pub dislikes: Option<Url>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ExtensionSpecs {
#[serde(rename = "pub.versia:custom_emojis")]
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_emojis: Option<CustomEmojis>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CustomEmojis {
pub emojis: Vec<CustomEmoji>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CustomEmoji {
pub name: String,
pub url: ContentFormat,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DeviceInfo {
name: String,
version: String,
url: Url,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LinkPreview {
description: String,
title: String,
link: Url,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<Url>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Note {
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub uri: Url,
pub author: Url,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<CategoryType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ContentFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<DeviceInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previews: Option<Vec<LinkPreview>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<ContentFormat>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replies_to: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quotes: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<Url>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_sensitive: Option<bool>,
//TODO extensions
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Outbox {
pub first: Url,
pub last: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub next: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous: Option<Url>,
pub items: Vec<Note>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Follow {
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub uri: Url,
pub author: Url,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
pub followee: Url,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FollowResult {
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub uri: Url,
pub author: Url,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
pub follower: Url,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Unfollow {
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub author: Url,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
pub followee: Url,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Delete {
#[serde(rename = "type")]
pub rtype: String,
pub id: Uuid,
pub author: Option<Url>,
#[serde(with = "iso_versia")]
pub created_at: OffsetDateTime,
pub deleted_type: String,
pub deleted: Url,
}

53
src/versia/superx.rs Normal file
View file

@ -0,0 +1,53 @@
use super::objects::SortAlphabetically;
pub async fn deserialize_user(data: String) -> anyhow::Result<super::objects::User> {
let user: super::objects::User = serde_json::from_str(&data)?;
Ok(user)
}
pub async fn serialize_user(user: super::objects::User) -> anyhow::Result<String> {
let data = serde_json::to_string(&SortAlphabetically(&user))?;
Ok(data)
}
pub async fn deserialize_versia_type(data: String) -> anyhow::Result<String> {
let versia_type: String = serde_json::from_str(&data)?;
Ok(versia_type)
}
pub async fn serialize_versia_type(versia_type: String) -> anyhow::Result<String> {
let data = serde_json::to_string(&versia_type)?;
Ok(data)
}
pub async fn deserialize_note(data: String) -> anyhow::Result<super::objects::Note> {
let post: super::objects::Note = serde_json::from_str(&data)?;
Ok(post)
}
pub async fn serialize_note(post: super::objects::Note) -> anyhow::Result<String> {
let data = serde_json::to_string(&SortAlphabetically(&post))?;
Ok(data)
}
pub async fn deserialize_outbox(data: String) -> anyhow::Result<super::objects::Outbox> {
let outbox: super::objects::Outbox = serde_json::from_str(&data)?;
Ok(outbox)
}
pub async fn serialize_outbox(outbox: super::objects::Outbox) -> anyhow::Result<String> {
let data = serde_json::to_string(&SortAlphabetically(&outbox))?;
Ok(data)
}
#[inline]
pub fn request_client() -> reqwest::Client {
reqwest::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION"),
))
.build()
.unwrap()
}

66
src/versia/test.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::versia::objects::SortAlphabetically;
use super::superx::request_client;
#[actix_web::test]
async fn test_user_serial() {
let client = request_client();
let response = client
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
.send()
.await
.unwrap();
let user = super::superx::deserialize_user(response.text().await.unwrap())
.await
.unwrap();
let response_outbox = client
.get(user.collections.outbox.as_str())
.send()
.await
.unwrap();
let outbox = super::superx::deserialize_outbox(response_outbox.text().await.unwrap())
.await
.unwrap();
assert!(outbox.items.len() > 0);
}
pub async fn main() -> anyhow::Result<()> {
let client = request_client();
println!("Requesting user");
let response = client
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
.send()
.await?;
println!("Response: {:?}", response);
let user_json = response.text().await?;
println!("User JSON: {:?}", user_json);
let user = super::superx::deserialize_user(user_json).await?;
println!("\n\n\nUser: ");
print!("{:#?}", user);
println!("\n\n\nas JSON:");
let user_json = serde_json::to_string_pretty(&SortAlphabetically(&user))?;
println!("{}", user_json);
let response_outbox = client.get(user.collections.outbox.as_str()).send().await?;
let outbox_json = response_outbox.text().await?;
let outbox = super::superx::deserialize_outbox(outbox_json).await?;
println!("\n\n\nOutbox: ");
print!("{:#?}", outbox);
println!("\n\n\nas AP:");
for item in outbox.items {
let ap_item = super::conversion::receive_versia_note(
item,
"https://ap.versia.social/example".to_string(),
)
.await?;
println!("{:#?}", ap_item);
}
Ok(())
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB