├── .dockerignore ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── scripts │ ├── determine-dockerfiles.sh │ └── determine-workspace-members.sh └── workflows │ ├── cypher-backend.yml │ ├── cypher-frontend.yml │ ├── rsky-pdsadmin.yml │ └── rust.yml ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── sqldialects.xml └── vcs.xml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── ROADMAP.md ├── cypher ├── README.md ├── backend │ ├── Cargo.toml │ └── src │ │ ├── auth.rs │ │ ├── db.rs │ │ ├── firehose.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── models.rs │ │ ├── routes.rs │ │ ├── tests.rs │ │ ├── vendored.rs │ │ └── vendored │ │ └── atrium_oauth_client │ │ ├── atproto.rs │ │ ├── constants.rs │ │ ├── error.rs │ │ ├── http_client.rs │ │ ├── http_client │ │ ├── default.rs │ │ └── dpop.rs │ │ ├── jose.rs │ │ ├── jose │ │ ├── jws.rs │ │ ├── jwt.rs │ │ └── signing.rs │ │ ├── keyset.rs │ │ ├── lib.rs │ │ ├── mod.rs │ │ ├── oauth_client.rs │ │ ├── resolver.rs │ │ ├── resolver │ │ ├── oauth_authorization_server_resolver.rs │ │ └── oauth_protected_resource_resolver.rs │ │ ├── server_agent.rs │ │ ├── store.rs │ │ ├── store │ │ ├── memory.rs │ │ └── state.rs │ │ ├── types.rs │ │ ├── types │ │ ├── client_metadata.rs │ │ ├── metadata.rs │ │ ├── request.rs │ │ ├── response.rs │ │ └── token.rs │ │ └── utils.rs └── frontend │ ├── Cargo.toml │ ├── Dioxus.toml │ ├── assets │ ├── styling │ │ └── main.css │ └── tailwind.css │ ├── input.css │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── components │ │ └── mod.rs │ └── main.rs │ └── tailwind.config.js ├── rsky-common ├── Cargo.toml ├── README.md └── src │ ├── async.rs │ ├── env.rs │ ├── explicit_slurs.rs │ ├── ipld.rs │ ├── lib.rs │ ├── sign.rs │ ├── tid.rs │ └── time.rs ├── rsky-crypto ├── Cargo.toml ├── README.md └── src │ ├── constants.rs │ ├── did.rs │ ├── lib.rs │ ├── multibase.rs │ ├── p256 │ ├── encoding.rs │ ├── mod.rs │ ├── operations.rs │ └── plugin.rs │ ├── secp256k1 │ ├── encoding.rs │ ├── mod.rs │ ├── operations.rs │ └── plugin.rs │ ├── types.rs │ ├── utils.rs │ └── verify.rs ├── rsky-feedgen ├── Cargo.toml ├── Dockerfile ├── README.md ├── diesel.toml ├── migrations │ ├── .keep │ ├── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-06-12-212554_001 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-06-26-191108_create_memberships │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-06-27-050840_003 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-06-27-054243_004 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-02-021849_005 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-04-040513_006 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-06-051432_007 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-06-222616_008 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-07-024146_009 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-07-07-161148_010 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-09-11-223751_011 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-09-12-164434_012 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-09-17-190313_013 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-09-20-161623_014 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-10-26-204539_015 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-10-27-222305_016 │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-11-06-001456_017 │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-09-12-133947_018 │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-11-09-085601_019 │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-11-16-235919_020 │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-12-01-074204_021 │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-12-14-193843_022 │ │ ├── down.sql │ │ └── up.sql │ ├── 2025-02-26-002028_023 │ │ ├── down.sql │ │ └── up.sql │ ├── 2025-02-26-044129_024 │ │ ├── down.sql │ │ └── up.sql │ ├── 2025-02-26-044132_025 │ │ ├── down.sql │ │ └── up.sql │ ├── 2025-02-26-044137_026 │ │ ├── down.sql │ │ └── up.sql │ └── 2025-02-26-070232_026 │ │ ├── down.sql │ │ └── up.sql └── src │ ├── apis │ └── mod.rs │ ├── auth.rs │ ├── db │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── models │ ├── algo_response.rs │ ├── create_request.rs │ ├── delete_request.rs │ ├── error_code.rs │ ├── internal_error_code.rs │ ├── internal_error_message_response.rs │ ├── jwt_parts.rs │ ├── known_service.rs │ ├── membership.rs │ ├── mod.rs │ ├── not_found_error_code.rs │ ├── path_unknown_error_message_response.rs │ ├── post.rs │ ├── post_result.rs │ ├── sub_state.rs │ ├── validation_error_message_response.rs │ └── well_known.rs │ ├── routes.rs │ └── schema.rs ├── rsky-firehose ├── Cargo.toml ├── Dockerfile ├── README.md └── src │ ├── car.rs │ ├── firehose.rs │ ├── lib.rs │ ├── main.rs │ └── models │ ├── create_op.rs │ ├── delete_op.rs │ └── mod.rs ├── rsky-identity ├── Cargo.toml ├── README.md └── src │ ├── common │ └── mod.rs │ ├── did │ ├── atproto_data.rs │ ├── did_resolver.rs │ ├── mod.rs │ ├── plc_resolver.rs │ └── web_resolver.rs │ ├── errors.rs │ ├── handle │ └── mod.rs │ ├── lib.rs │ └── types.rs ├── rsky-jetstream-subscriber ├── Cargo.toml ├── Dockerfile ├── README.md └── src │ ├── jetstream.rs │ ├── lib.rs │ ├── main.rs │ └── models │ ├── create_op.rs │ ├── delete_op.rs │ └── mod.rs ├── rsky-labeler ├── Cargo.toml ├── Dockerfile ├── README.md └── src │ ├── car.rs │ ├── firehose.rs │ ├── lib.rs │ └── main.rs ├── rsky-lexicon ├── Cargo.toml ├── README.md └── src │ ├── app │ ├── bsky │ │ ├── actor.rs │ │ ├── embed │ │ │ ├── external.rs │ │ │ ├── images.rs │ │ │ ├── mod.rs │ │ │ ├── record.rs │ │ │ ├── record_with_media.rs │ │ │ └── video.rs │ │ ├── feed │ │ │ ├── like.rs │ │ │ └── mod.rs │ │ ├── graph │ │ │ ├── follow.rs │ │ │ └── mod.rs │ │ ├── labeler │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── notification │ │ │ └── mod.rs │ │ └── richtext │ │ │ └── mod.rs │ └── mod.rs │ ├── blob_refs.rs │ ├── chat │ ├── bsky │ │ ├── actor │ │ │ └── mod.rs │ │ ├── convo │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── moderation │ │ │ └── mod.rs │ └── mod.rs │ ├── com │ ├── atproto │ │ ├── admin.rs │ │ ├── identity.rs │ │ ├── label │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── repo.rs │ │ ├── server.rs │ │ └── sync.rs │ └── mod.rs │ └── lib.rs ├── rsky-pds ├── Cargo.toml ├── Dockerfile ├── README.md ├── diesel.toml ├── migrations │ ├── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-11-15-004814_pds_init │ │ ├── down.sql │ │ └── up.sql │ └── 2024-03-20-042639_account_deactivation │ │ ├── down.sql │ │ └── up.sql ├── src │ ├── account_manager │ │ ├── helpers │ │ │ ├── account.rs │ │ │ ├── auth.rs │ │ │ ├── email_token.rs │ │ │ ├── invite.rs │ │ │ ├── mod.rs │ │ │ ├── password.rs │ │ │ └── repo.rs │ │ └── mod.rs │ ├── actor_store │ │ ├── aws │ │ │ ├── mod.rs │ │ │ └── s3.rs │ │ ├── blob │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── preference │ │ │ ├── mod.rs │ │ │ └── util.rs │ │ ├── record │ │ │ └── mod.rs │ │ └── repo │ │ │ ├── mod.rs │ │ │ ├── sql_repo.rs │ │ │ └── types.rs │ ├── apis │ │ ├── app │ │ │ ├── bsky │ │ │ │ ├── actor │ │ │ │ │ ├── get_preferences.rs │ │ │ │ │ ├── get_profile.rs │ │ │ │ │ ├── get_profiles.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── put_preferences.rs │ │ │ │ ├── feed │ │ │ │ │ ├── get_actor_likes.rs │ │ │ │ │ ├── get_author_feed.rs │ │ │ │ │ ├── get_feed.rs │ │ │ │ │ ├── get_post_thread.rs │ │ │ │ │ ├── get_timeline.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── notification │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── register_push.rs │ │ │ │ └── util │ │ │ │ │ └── mod.rs │ │ │ └── mod.rs │ │ ├── com │ │ │ ├── atproto │ │ │ │ ├── admin │ │ │ │ │ ├── delete_account.rs │ │ │ │ │ ├── disable_account_invites.rs │ │ │ │ │ ├── disable_invite_codes.rs │ │ │ │ │ ├── enable_account_invites.rs │ │ │ │ │ ├── get_account_info.rs │ │ │ │ │ ├── get_invite_codes.rs │ │ │ │ │ ├── get_subject_status.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── send_email.rs │ │ │ │ │ ├── update_account_email.rs │ │ │ │ │ ├── update_account_handle.rs │ │ │ │ │ ├── update_account_password.rs │ │ │ │ │ └── update_subject_status.rs │ │ │ │ ├── identity │ │ │ │ │ ├── get_recommended_did_credentials.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── request_plc_operation_signature.rs │ │ │ │ │ ├── resolve_handle.rs │ │ │ │ │ ├── sign_plc_operation.rs │ │ │ │ │ ├── submit_plc_operation.rs │ │ │ │ │ └── update_handle.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── repo │ │ │ │ │ ├── apply_writes.rs │ │ │ │ │ ├── create_record.rs │ │ │ │ │ ├── delete_record.rs │ │ │ │ │ ├── describe_repo.rs │ │ │ │ │ ├── get_record.rs │ │ │ │ │ ├── import_repo.rs │ │ │ │ │ ├── list_missing_blobs.rs │ │ │ │ │ ├── list_records.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── put_record.rs │ │ │ │ │ └── upload_blob.rs │ │ │ │ ├── server │ │ │ │ │ ├── activate_account.rs │ │ │ │ │ ├── check_account_status.rs │ │ │ │ │ ├── confirm_email.rs │ │ │ │ │ ├── create_account.rs │ │ │ │ │ ├── create_app_password.rs │ │ │ │ │ ├── create_invite_code.rs │ │ │ │ │ ├── create_invite_codes.rs │ │ │ │ │ ├── create_session.rs │ │ │ │ │ ├── deactivate_account.rs │ │ │ │ │ ├── delete_account.rs │ │ │ │ │ ├── delete_session.rs │ │ │ │ │ ├── describe_server.rs │ │ │ │ │ ├── get_account_invite_codes.rs │ │ │ │ │ ├── get_service_auth.rs │ │ │ │ │ ├── get_session.rs │ │ │ │ │ ├── list_app_passwords.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── refresh_session.rs │ │ │ │ │ ├── request_account_delete.rs │ │ │ │ │ ├── request_email_confirmation.rs │ │ │ │ │ ├── request_email_update.rs │ │ │ │ │ ├── request_password_reset.rs │ │ │ │ │ ├── reserve_signing_key.rs │ │ │ │ │ ├── reset_password.rs │ │ │ │ │ ├── revoke_app_password.rs │ │ │ │ │ └── update_email.rs │ │ │ │ └── sync │ │ │ │ │ ├── get_blob.rs │ │ │ │ │ ├── get_blocks.rs │ │ │ │ │ ├── get_latest_commit.rs │ │ │ │ │ ├── get_record.rs │ │ │ │ │ ├── get_repo.rs │ │ │ │ │ ├── get_repo_status.rs │ │ │ │ │ ├── list_blobs.rs │ │ │ │ │ ├── list_repos.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── subscribe_repos.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── auth_verifier.rs │ ├── config │ │ └── mod.rs │ ├── context.rs │ ├── crawlers.rs │ ├── db │ │ └── mod.rs │ ├── handle │ │ ├── errors.rs │ │ ├── explicit_slurs.rs │ │ ├── mod.rs │ │ └── reserved.rs │ ├── image │ │ └── mod.rs │ ├── lexicon │ │ ├── lexicons.rs │ │ ├── lexicons.toml │ │ └── mod.rs │ ├── lib.rs │ ├── mailer │ │ ├── mod.rs │ │ └── moderation.rs │ ├── main.rs │ ├── models │ │ ├── error_code.rs │ │ ├── error_message_response.rs │ │ ├── mod.rs │ │ ├── models.rs │ │ └── server_version.rs │ ├── pipethrough.rs │ ├── plc │ │ ├── mod.rs │ │ ├── operations.rs │ │ └── types.rs │ ├── read_after_write │ │ ├── mod.rs │ │ ├── types.rs │ │ ├── util.rs │ │ └── viewer.rs │ ├── repo │ │ ├── mod.rs │ │ └── prepare.rs │ ├── schema.rs │ ├── sequencer │ │ ├── events.rs │ │ ├── mod.rs │ │ └── outbox.rs │ ├── well_known.rs │ └── xrpc_server │ │ ├── auth.rs │ │ ├── mod.rs │ │ ├── stream │ │ ├── frames.rs │ │ ├── mod.rs │ │ └── types.rs │ │ └── types.rs └── tests │ ├── common │ └── mod.rs │ └── integration_tests.rs ├── rsky-pdsadmin ├── Cargo.toml ├── README.md ├── pds.env ├── src │ ├── commands │ │ ├── account │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ │ └── mod.rs │ │ ├── create_invite_code │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── request_crawl │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ │ └── mod.rs │ │ ├── rsky_pds │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ │ └── mod.rs │ │ └── update │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ └── util │ │ ├── env.rs │ │ ├── http_client.rs │ │ └── mod.rs └── tests │ ├── external_commands_tests.rs │ ├── file_tests.rs │ └── integration_tests.rs ├── rsky-relay ├── Cargo.toml ├── README.md ├── crawler.py ├── rustfmt.toml ├── src │ ├── crawler │ │ ├── client.rs │ │ ├── connection.rs │ │ ├── manager.rs │ │ ├── mod.rs │ │ ├── types.rs │ │ └── worker.rs │ ├── lib.rs │ ├── main.rs │ ├── publisher │ │ ├── connection.rs │ │ ├── manager.rs │ │ ├── mod.rs │ │ ├── types.rs │ │ └── worker.rs │ ├── server │ │ ├── mod.rs │ │ ├── server.rs │ │ └── types.rs │ ├── types.rs │ └── validator │ │ ├── event.rs │ │ ├── manager.rs │ │ ├── mod.rs │ │ ├── resolver.rs │ │ ├── types.rs │ │ └── utils.rs └── ssl.sh ├── rsky-repo ├── Cargo.toml ├── README.md ├── resources │ └── test │ │ └── valid_repo.car ├── src │ ├── block_map.rs │ ├── car.rs │ ├── cid_set.rs │ ├── data_diff.rs │ ├── error.rs │ ├── lib.rs │ ├── mst │ │ ├── diff.rs │ │ ├── mod.rs │ │ ├── util.rs │ │ └── walker.rs │ ├── parse.rs │ ├── readable_repo.rs │ ├── repo.rs │ ├── storage │ │ ├── memory_blockstore.rs │ │ ├── mod.rs │ │ ├── readable_blockstore.rs │ │ ├── sync_storage.rs │ │ └── types.rs │ ├── sync │ │ ├── consumer.rs │ │ ├── mod.rs │ │ └── provider.rs │ ├── types.rs │ └── util.rs └── tests │ └── interop.rs ├── rsky-satnav ├── .gitignore ├── Cargo.toml ├── Dioxus.toml ├── README.md ├── assets │ ├── favicon.ico │ ├── header.svg │ ├── main.css │ └── tailwind.css ├── input.css ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── hero.rs │ │ └── mod.rs │ └── main.rs └── tailwind.config.js ├── rsky-syntax ├── ATURI_VALIDATION.md ├── Cargo.toml ├── README.md └── src │ ├── aturi.rs │ ├── aturi_validation.rs │ ├── datetime.rs │ ├── did.rs │ ├── handle.rs │ ├── lib.rs │ ├── nsid.rs │ ├── record_key.rs │ └── tid.rs └── rust-toolchain /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | .DS_Store 5 | .gitignore 6 | Dockerfile 7 | .dockerignore 8 | .git 9 | *.pem 10 | Rocket.toml 11 | rustc-*.txt -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: blacksky 4 | github: blacksky-algorithms 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. 18 | 19 | **Expected behavior** 20 | 21 | 22 | 23 | **Details** 24 | 25 | - Operating system: 26 | - Node version: 27 | 28 | **Additional context** 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | ## Related Issues 5 | 6 | 7 | ## Changes 8 | - [ ] Feature implementation 9 | - [ ] Bug fix 10 | - [ ] Documentation update 11 | - [ ] Other (please specify): 12 | 13 | ## Checklist 14 | - [ ] I have tested the changes (including writing unit tests). 15 | - [ ] I confirm that my implementation aligns with the [canonical Typescript implementation](https://github.com/bluesky-social/atproto) and/or [atproto spec](https://atproto.com/specs/atp) 16 | - [ ] I have updated relevant documentation. 17 | - [ ] I have formatted my code correctly 18 | - [ ] I have provided examples for how this code works or will be used -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rsky-repo/tests/interop_src"] 2 | path = rsky-repo/tests/interop_src 3 | url = https://github.com/DavidBuchanan314/mst-test-suite 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "rsky-common", "rsky-crypto","rsky-feedgen", "rsky-firehose", "rsky-identity", "rsky-labeler", "rsky-lexicon", "rsky-pds", "rsky-syntax", "rsky-jetstream-subscriber", "rsky-repo", "cypher/backend", "cypher/frontend", "rsky-satnav", "rsky-relay"] 3 | resolver = "2" 4 | 5 | [workspace.dependencies] 6 | cargo = { version = "0.84.0",features = ["vendored-openssl"] } 7 | serde = { version = "1.0.160", features = ["derive"] } 8 | serde_derive = "^1.0" 9 | serde_ipld_dagcbor = { version = "0.6.1" ,features = ["codec"]} 10 | lexicon_cid = { package = "cid", version = "0.11.1", features = ["serde-codec"] } 11 | ipld-core = "0.4.2" 12 | serde_cbor = "0.11.2" 13 | serde_bytes = "0.11.15" 14 | tokio = { version = "1.28.2",features = ["full"] } 15 | sha2 = "0.10.8" 16 | rand = "0.8.5" 17 | rand_core = "0.6.4" 18 | secp256k1 = { version = "0.28.2", features = ["global-context", "serde", "rand", "hashes","rand-std"] } 19 | serde_json = { version = "1.0.96",features = ["preserve_order"] } 20 | rsky-lexicon = {path = "rsky-lexicon", version = "0.2.8"} 21 | rsky-identity = {path = "rsky-identity", version = "0.1.0"} 22 | rsky-crypto = {path = "rsky-crypto", version = "0.1.1"} 23 | rsky-syntax = {path = "rsky-syntax", version = "0.1.0"} 24 | rsky-common = {path = "rsky-common", version = "0.1.2"} 25 | rsky-repo = {path = "rsky-repo", version = "0.0.2"} 26 | rsky-firehose = {path = "rsky-firehose", version = "0.2.1"} 27 | 28 | [profile.release] 29 | debug = 2 # Or any level from 0 to 2 30 | 31 | [profile.wasm-dev] 32 | inherits = "dev" 33 | opt-level = 1 34 | 35 | [profile.server-dev] 36 | inherits = "dev" 37 | 38 | [profile.android-dev] 39 | inherits = "dev" 40 | -------------------------------------------------------------------------------- /cypher/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | axum = { version = "0.8.1",features = ["macros"] } 8 | tokio = { workspace = true } 9 | surrealdb = { version = "2.2.1", features = ["kv-rocksdb"] } 10 | rsky-lexicon = { workspace = true } 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | serde_cbor = { workspace = true } 14 | uuid = { version = "1", features = ["v4"] } 15 | tower-http = { version = "0.6.2", features = ["fs", "cors"] } 16 | chrono = "0.4.40" 17 | atrium-identity = "0.1.0" 18 | thiserror = "2.0.12" 19 | sha2 = { workspace = true } 20 | rand = { workspace = true, features = ["small_rng"] } 21 | atrium-api = { version = "0.25.0", default-features = false } 22 | atrium-common = "0.1.0" 23 | atrium-xrpc = "0.12.1" 24 | base64 = "0.22.1" 25 | ecdsa = { version = "0.16.9",features = ["signing"] } 26 | elliptic-curve = "0.13.8" 27 | jose-jwa = "0.1.2" 28 | jose-jwk = { version = "0.1.2", features = ["p256"] } 29 | p256 = { version = "0.13.2",features = ["ecdsa"] } 30 | reqwest = { version = "0.12.12", optional = true } 31 | serde_html_form = "0.2.7" 32 | trait-variant = "0.1.2" 33 | hickory-resolver = "0.24.1" 34 | anyhow = "1.0.97" 35 | tokio-tungstenite = "0.23.1" 36 | futures = "0.3.31" 37 | rsky-firehose = { workspace = true } 38 | tracing = "0.1.41" 39 | tokio-stream = { version = "0.1.17",features = ["sync"] } 40 | axum-extra = { version = "0.10.0", features = ["typed-header"] } 41 | headers = "0.4" 42 | tower = "0.5.2" 43 | 44 | [dev-dependencies] 45 | p256 = { version = "0.13.2",features = ["pem"] } 46 | tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } 47 | 48 | [[bin]] 49 | name = "backend" 50 | path = "src/main.rs" 51 | 52 | [features] 53 | default = ["default-client"] 54 | default-client = ["reqwest/default-tls"] -------------------------------------------------------------------------------- /cypher/backend/src/db.rs: -------------------------------------------------------------------------------- 1 | use crate::models::Post; 2 | use surrealdb::engine::local::Db; 3 | use surrealdb::{Error as SurrealError, Surreal}; 4 | 5 | pub async fn save_post(db: &Surreal, post: Post) -> Result<(), SurrealError> { 6 | // Use the post.uri as the record ID in SurrealDB (post:uri) 7 | db.update::>(("post", &post.uri)) 8 | .content(post) 9 | .await?; 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /cypher/backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod db; 3 | pub mod firehose; 4 | pub mod models; 5 | pub mod routes; 6 | pub mod tests; 7 | pub mod vendored; 8 | -------------------------------------------------------------------------------- /cypher/backend/src/models.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | // models.rs 3 | use crate::auth::HickoryDnsTxtResolver; 4 | use crate::vendored::atrium_oauth_client::store::state::MemoryStateStore; 5 | use crate::vendored::atrium_oauth_client::{DefaultHttpClient, OAuthClient}; 6 | use atrium_identity::did::CommonDidResolver; 7 | use atrium_identity::handle::AtprotoHandleResolver; 8 | use chrono::{DateTime, Utc}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::{Arc, Mutex}; 11 | use surrealdb::Surreal; 12 | use surrealdb::engine::local::Db; 13 | use tokio::sync::broadcast; 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct SessionInfo { 17 | pub did: String, 18 | pub token: String, 19 | } 20 | 21 | #[derive(Clone)] 22 | pub struct AppState { 23 | pub db: Surreal, 24 | pub sessions: Arc>>, // <-- use SessionInfo here 25 | pub tx: broadcast::Sender, 26 | pub oauth_client: Arc< 27 | OAuthClient< 28 | MemoryStateStore, 29 | CommonDidResolver, 30 | AtprotoHandleResolver, 31 | >, 32 | >, 33 | } 34 | 35 | #[derive(Clone, Debug, Serialize, Deserialize)] 36 | pub struct Post { 37 | // Primary key (we'll also use this as SurrealDB record ID) 38 | pub uri: String, 39 | pub cid: String, 40 | pub reply_parent: Option, 41 | pub reply_root: Option, 42 | pub indexed_at: DateTime, 43 | pub prev: Option, 44 | pub sequence: i64, 45 | pub text: String, 46 | pub langs: Option>, 47 | pub author: String, 48 | pub external_uri: Option, 49 | pub external_title: Option, 50 | pub external_description: Option, 51 | pub external_thumb: Option, 52 | pub quote_uri: Option, 53 | pub quote_cid: Option, 54 | pub created_at: DateTime, 55 | pub labels: Option>, 56 | pub local_only: bool, 57 | } 58 | -------------------------------------------------------------------------------- /cypher/backend/src/tests.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored.rs: -------------------------------------------------------------------------------- 1 | pub mod atrium_oauth_client; 2 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/constants.rs: -------------------------------------------------------------------------------- 1 | pub const FALLBACK_ALG: &str = "ES256"; 2 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error(transparent)] 6 | ClientMetadata(#[from] crate::vendored::atrium_oauth_client::atproto::Error), 7 | #[error(transparent)] 8 | Keyset(#[from] crate::vendored::atrium_oauth_client::keyset::Error), 9 | #[error(transparent)] 10 | Identity(#[from] atrium_identity::Error), 11 | #[error(transparent)] 12 | ServerAgent(#[from] crate::vendored::atrium_oauth_client::server_agent::Error), 13 | #[error("authorize error: {0}")] 14 | Authorize(String), 15 | #[error("callback error: {0}")] 16 | Callback(String), 17 | #[error("state store error: {0:?}")] 18 | StateStore(Box), 19 | } 20 | 21 | pub type Result = core::result::Result; 22 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/http_client.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "default-client")] 2 | pub mod default; 3 | pub mod dpop; 4 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/http_client/default.rs: -------------------------------------------------------------------------------- 1 | use atrium_xrpc::HttpClient; 2 | use reqwest::Client; 3 | 4 | pub struct DefaultHttpClient { 5 | client: Client, 6 | } 7 | 8 | impl HttpClient for DefaultHttpClient { 9 | async fn send_http( 10 | &self, 11 | request: atrium_xrpc::http::Request>, 12 | ) -> core::result::Result< 13 | atrium_xrpc::http::Response>, 14 | Box, 15 | > { 16 | let response = self.client.execute(request.try_into()?).await?; 17 | let mut builder = atrium_xrpc::http::Response::builder().status(response.status()); 18 | for (k, v) in response.headers() { 19 | builder = builder.header(k, v); 20 | } 21 | builder 22 | .body(response.bytes().await?.to_vec()) 23 | .map_err(Into::into) 24 | } 25 | } 26 | 27 | impl Default for DefaultHttpClient { 28 | fn default() -> Self { 29 | Self { 30 | client: reqwest::Client::new(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/jose.rs: -------------------------------------------------------------------------------- 1 | pub mod jws; 2 | pub mod jwt; 3 | pub mod signing; 4 | 5 | pub use self::signing::create_signed_jwt; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 9 | #[serde(untagged)] 10 | pub enum Header { 11 | Jws(jws::Header), 12 | // TODO: JWE? 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use jose_jwa::{Algorithm, Signing}; 18 | use jws::RegisteredHeader; 19 | 20 | use super::*; 21 | 22 | #[test] 23 | fn test_serialize_claims() { 24 | let header = Header::from(RegisteredHeader::from(Algorithm::Signing(Signing::Es256))); 25 | let json = serde_json::to_string(&header).expect("failed to serialize header"); 26 | assert_eq!(json, r#"{"alg":"ES256"}"#); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/jose/signing.rs: -------------------------------------------------------------------------------- 1 | use super::Header; 2 | use super::jwt::Claims; 3 | use base64::Engine; 4 | use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5 | use ecdsa::{ 6 | Signature, SignatureSize, SigningKey, 7 | hazmat::{DigestPrimitive, SignPrimitive}, 8 | signature::Signer, 9 | }; 10 | use elliptic_curve::{ 11 | CurveArithmetic, PrimeCurve, Scalar, generic_array::ArrayLength, ops::Invert, subtle::CtOption, 12 | }; 13 | 14 | pub fn create_signed_jwt( 15 | key: SigningKey, 16 | header: Header, 17 | claims: Claims, 18 | ) -> serde_json::Result 19 | where 20 | C: PrimeCurve + CurveArithmetic + DigestPrimitive, 21 | Scalar: Invert>> + SignPrimitive, 22 | SignatureSize: ArrayLength, 23 | { 24 | let header = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header)?); 25 | let payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims)?); 26 | let signature: Signature<_> = key.sign(format!("{header}.{payload}").as_bytes()); 27 | Ok(format!( 28 | "{header}.{payload}.{}", 29 | URL_SAFE_NO_PAD.encode(signature.to_bytes()) 30 | )) 31 | } 32 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/lib.rs: -------------------------------------------------------------------------------- 1 | mod atproto; 2 | mod constants; 3 | mod error; 4 | mod http_client; 5 | mod jose; 6 | mod keyset; 7 | mod oauth_client; 8 | mod resolver; 9 | mod server_agent; 10 | pub mod store; 11 | mod types; 12 | mod utils; 13 | 14 | pub use atproto::{ 15 | AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, GrantType, KnownScope, Scope, 16 | }; 17 | pub use error::{Error, Result}; 18 | #[cfg(feature = "default-client")] 19 | pub use http_client::default::DefaultHttpClient; 20 | pub use http_client::dpop::DpopClient; 21 | pub use oauth_client::{OAuthClient, OAuthClientConfig}; 22 | pub use resolver::OAuthResolverConfig; 23 | pub use types::{ 24 | AuthorizeOptionPrompt, AuthorizeOptions, CallbackParams, OAuthClientMetadata, TokenSet, 25 | }; -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/mod.rs: -------------------------------------------------------------------------------- 1 | /// Module version of lib.rs 2 | mod atproto; 3 | mod constants; 4 | mod error; 5 | mod http_client; 6 | mod jose; 7 | mod keyset; 8 | mod oauth_client; 9 | mod resolver; 10 | mod server_agent; 11 | pub mod store; 12 | mod types; 13 | mod utils; 14 | 15 | pub use atproto::{ 16 | AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, GrantType, KnownScope, Scope, 17 | }; 18 | pub use error::{Error, Result}; 19 | #[cfg(feature = "default-client")] 20 | pub use http_client::default::DefaultHttpClient; 21 | pub use http_client::dpop::DpopClient; 22 | pub use oauth_client::{OAuthClient, OAuthClientConfig}; 23 | pub use resolver::OAuthResolverConfig; 24 | pub use types::{ 25 | AuthorizeOptionPrompt, AuthorizeOptions, CallbackParams, OAuthClientMetadata, TokenSet, 26 | }; 27 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/store.rs: -------------------------------------------------------------------------------- 1 | pub mod memory; 2 | pub mod state; 3 | 4 | use std::error::Error; 5 | use std::future::Future; 6 | use std::hash::Hash; 7 | 8 | #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 9 | pub trait SimpleStore 10 | where 11 | K: Eq + Hash, 12 | V: Clone, 13 | { 14 | type Error: Error + Send + Sync + 'static; 15 | 16 | fn get(&self, key: &K) -> impl Future, Self::Error>>; 17 | fn set(&self, key: K, value: V) -> impl Future>; 18 | fn del(&self, key: &K) -> impl Future>; 19 | fn clear(&self) -> impl Future>; 20 | } 21 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/store/memory.rs: -------------------------------------------------------------------------------- 1 | use super::SimpleStore; 2 | use std::collections::HashMap; 3 | use std::fmt::Debug; 4 | use std::hash::Hash; 5 | use std::sync::{Arc, Mutex}; 6 | use thiserror::Error; 7 | 8 | #[derive(Error, Debug)] 9 | #[error("memory store error")] 10 | pub struct Error; 11 | 12 | // TODO: LRU cache? 13 | pub struct MemorySimpleStore { 14 | store: Arc>>, 15 | } 16 | 17 | impl Default for MemorySimpleStore { 18 | fn default() -> Self { 19 | Self { 20 | store: Arc::new(Mutex::new(HashMap::new())), 21 | } 22 | } 23 | } 24 | 25 | impl SimpleStore for MemorySimpleStore 26 | where 27 | K: Debug + Eq + Hash + Send + Sync + 'static, 28 | V: Debug + Clone + Send + Sync + 'static, 29 | { 30 | type Error = Error; 31 | 32 | async fn get(&self, key: &K) -> Result, Self::Error> { 33 | Ok(self.store.lock().unwrap().get(key).cloned()) 34 | } 35 | async fn set(&self, key: K, value: V) -> Result<(), Self::Error> { 36 | self.store.lock().unwrap().insert(key, value); 37 | Ok(()) 38 | } 39 | async fn del(&self, key: &K) -> Result<(), Self::Error> { 40 | self.store.lock().unwrap().remove(key); 41 | Ok(()) 42 | } 43 | async fn clear(&self) -> Result<(), Self::Error> { 44 | self.store.lock().unwrap().clear(); 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/store/state.rs: -------------------------------------------------------------------------------- 1 | use super::SimpleStore; 2 | use super::memory::MemorySimpleStore; 3 | use jose_jwk::Key; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 7 | pub struct InternalStateData { 8 | pub iss: String, 9 | pub dpop_key: Key, 10 | pub verifier: String, 11 | } 12 | 13 | pub trait StateStore: SimpleStore {} 14 | 15 | pub type MemoryStateStore = MemorySimpleStore; 16 | 17 | impl StateStore for MemoryStateStore {} 18 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/types/client_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::vendored::atrium_oauth_client::keyset::Keyset; 2 | use jose_jwk::JwkSet; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 6 | pub struct OAuthClientMetadata { 7 | pub client_id: String, 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub client_uri: Option, 10 | pub redirect_uris: Vec, 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub scope: Option, 13 | #[serde(skip_serializing_if = "Option::is_none")] 14 | pub grant_types: Option>, 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub token_endpoint_auth_method: Option, 17 | // https://datatracker.ietf.org/doc/html/rfc9449#section-5.2 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub dpop_bound_access_tokens: Option, 20 | // https://datatracker.ietf.org/doc/html/rfc7591#section-2 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub jwks_uri: Option, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub jwks: Option, 25 | // https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub token_endpoint_auth_signing_alg: Option, 28 | } 29 | 30 | pub trait TryIntoOAuthClientMetadata { 31 | type Error; 32 | 33 | fn try_into_client_metadata( 34 | self, 35 | keyset: &Option, 36 | ) -> core::result::Result; 37 | } 38 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/types/response.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 4 | pub struct OAuthPusehedAuthorizationRequestResponse { 5 | pub request_uri: String, 6 | pub expires_in: Option, 7 | } 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 10 | pub enum OAuthTokenType { 11 | DPoP, 12 | Bearer, 13 | } 14 | 15 | // https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 16 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 17 | pub struct OAuthTokenResponse { 18 | pub access_token: String, 19 | pub token_type: OAuthTokenType, 20 | pub expires_in: Option, 21 | pub refresh_token: Option, 22 | pub scope: Option, 23 | // ATPROTO extension: add the sub claim to the token response to allow 24 | // clients to resolve the PDS url (audience) using the did resolution 25 | // mechanism. 26 | pub sub: Option, 27 | } 28 | -------------------------------------------------------------------------------- /cypher/backend/src/vendored/atrium_oauth_client/types/token.rs: -------------------------------------------------------------------------------- 1 | use super::response::OAuthTokenType; 2 | use atrium_api::types::string::Datetime; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 6 | pub struct TokenSet { 7 | pub iss: String, 8 | pub sub: String, 9 | pub aud: String, 10 | pub scope: Option, 11 | 12 | pub refresh_token: Option, 13 | pub access_token: String, 14 | pub token_type: OAuthTokenType, 15 | 16 | pub expires_at: Option, 17 | } 18 | -------------------------------------------------------------------------------- /cypher/frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.0.1" 4 | authors = ["Rudy Fraser "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | dioxus = { version = "0.6.1", features = [] } 11 | web-sys = { version = "0.3.77", features = ["MediaQueryList", "Window", "EventSource","DomTokenList","Element"] } 12 | serde = { workspace = true } 13 | serde_json = { workspace = true } 14 | wasm-bindgen = "0.2.100" 15 | js-sys = "0.3.77" 16 | chrono = "0.4.40" 17 | wasm-bindgen-futures = "0.4.50" 18 | reqwest = { version = "0.12.12", features = ["json"] } 19 | gloo-storage = "0.3.0" 20 | 21 | [features] 22 | default = ["web"] 23 | web = ["dioxus/web"] 24 | desktop = ["dioxus/desktop"] 25 | mobile = ["dioxus/mobile"] 26 | -------------------------------------------------------------------------------- /cypher/frontend/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | [web.app] 4 | 5 | # HTML title tag content 6 | title = "frontend" 7 | 8 | # include `assets` in web platform 9 | [web.resource] 10 | 11 | # Additional CSS style files 12 | style = [] 13 | 14 | # Additional JavaScript files 15 | script = [] 16 | 17 | [web.resource.dev] 18 | 19 | # Javascript code file 20 | # serve: [dev-server] only 21 | script = [] 22 | -------------------------------------------------------------------------------- /cypher/frontend/assets/styling/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #0f1116; 3 | color: #ffffff; 4 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 5 | margin: 20px; 6 | } 7 | 8 | #hero { 9 | margin: 0; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | #links { 17 | width: 400px; 18 | text-align: left; 19 | font-size: x-large; 20 | color: white; 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | #links a { 26 | color: white; 27 | text-decoration: none; 28 | margin-top: 20px; 29 | margin: 10px 0px; 30 | border: white 1px solid; 31 | border-radius: 5px; 32 | padding: 10px; 33 | } 34 | 35 | #links a:hover { 36 | background-color: #1f1f1f; 37 | cursor: pointer; 38 | } 39 | 40 | #header { 41 | max-width: 1200px; 42 | } -------------------------------------------------------------------------------- /cypher/frontend/input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @custom-variant dark (&:where(.dark, .dark *)); 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; -------------------------------------------------------------------------------- /cypher/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@tailwindcss/cli": "^4.0.12", 4 | "tailwindcss": "^4.0.12" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cypher/frontend/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cypher/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "all", 4 | darkMode: "class", // Ensure class-based dark mode is enabled 5 | content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /rsky-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-common" 3 | version = "0.1.2" 4 | authors = ["Rudy Fraser "] 5 | description = "Shared code for rsky" 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = true 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-common" 11 | documentation = "https://docs.rs/rsky-common" 12 | 13 | 14 | [dependencies] 15 | regex = "1.8.4" 16 | serde = { version = "1.0.217", features = ["derive"] } 17 | thiserror = "2.0.11" 18 | serde_ipld_dagcbor = { workspace = true } 19 | anyhow = "1.0.79" 20 | chrono = "0.4.39" 21 | rand = {workspace = true} 22 | rand_core = { workspace = true } 23 | url = "2.5.4" 24 | serde_json = "1.0.138" 25 | tracing = "0.1.41" # @TODO: Remove anyhow in lib 26 | rsky-identity = {workspace = true} 27 | base64ct = "1.6.0" 28 | urlencoding = "2.1.3" 29 | futures = "0.3.28" 30 | ipld-core = {workspace = true} 31 | multihash = "0.19" 32 | multihash-codetable = { version = "0.1.3",features = ["sha2"]} 33 | indexmap = { version = "1.9.3",features = ["serde-1"] } 34 | secp256k1 = {workspace = true} 35 | sha2 = {workspace = true} 36 | lexicon_cid = {workspace = true} 37 | 38 | [dev-dependencies] 39 | temp-env = { version = "0.3.6"} -------------------------------------------------------------------------------- /rsky-common/README.md: -------------------------------------------------------------------------------- 1 | # rsky-common 2 | 3 | Shared code. 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-common?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-common) 6 | 7 | ## License 8 | 9 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-common/src/env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub fn env_int(name: &str) -> Option { 4 | match env::var(name) { 5 | Ok(str) => match str.parse::() { 6 | Ok(int) => Some(int), 7 | _ => None, 8 | }, 9 | _ => None, 10 | } 11 | } 12 | 13 | pub fn env_str(name: &str) -> Option { 14 | match env::var(name) { 15 | Ok(str) => Some(str), 16 | _ => None, 17 | } 18 | } 19 | 20 | pub fn env_bool(name: &str) -> Option { 21 | match env::var(name) { 22 | Ok(str) if str == "true" || str == "1" => Some(true), 23 | Ok(str) if str == "false" || str == "0" => Some(false), 24 | _ => None, 25 | } 26 | } 27 | 28 | pub fn env_list(name: &str) -> Vec { 29 | match env::var(name) { 30 | Ok(str) => str.split(",").into_iter().map(|s| s.to_string()).collect(), 31 | _ => Vec::new(), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rsky-common/src/ipld.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ipld_core::codec::Codec; 3 | use lexicon_cid::Cid; 4 | use multihash::Multihash; 5 | use serde::Serialize; 6 | use sha2::{Digest, Sha256}; 7 | 8 | const SHA2_256: u64 = 0x12; 9 | const DAGCBORCODEC: u64 = 0x71; 10 | // https://docs.rs/libipld-core/0.16.0/src/libipld_core/raw.rs.html#19 11 | const RAWCODEC: u64 = 0x77; 12 | 13 | pub fn cid_for_cbor(data: &T) -> Result { 14 | let bytes = crate::struct_to_cbor(data)?; 15 | let mut sha = Sha256::new(); 16 | sha.update(&bytes); 17 | let hash = sha.finalize(); 18 | let cid = Cid::new_v1( 19 | DAGCBORCODEC, 20 | Multihash::<64>::wrap(SHA2_256, hash.as_slice())?, 21 | ); 22 | Ok(cid) 23 | } 24 | 25 | pub fn sha256_to_cid(hash: Vec) -> Cid { 26 | let cid = Cid::new_v1( 27 | RAWCODEC, 28 | Multihash::<64>::wrap(SHA2_256, hash.as_slice()).unwrap(), 29 | ); 30 | cid 31 | } 32 | -------------------------------------------------------------------------------- /rsky-common/src/sign.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use indexmap::IndexMap; 3 | use secp256k1::{Message, SecretKey}; 4 | use serde::Serialize; 5 | use serde_json::Value as JsonValue; 6 | use sha2::{Digest, Sha256}; 7 | 8 | pub fn atproto_sign(obj: &T, key: &SecretKey) -> Result<[u8; 64]> { 9 | // Encode object to json before dag-cbor because serde_ipld_dagcbor doesn't properly 10 | // sort by keys 11 | let json = serde_json::to_string(obj).unwrap(); 12 | // Deserialize to IndexMap with preserve key order enabled. serde_ipld_dagcbor does not sort nested 13 | // objects properly by keys 14 | let map_unsigned: IndexMap = serde_json::from_str(&json).unwrap(); 15 | let unsigned_bytes = serde_ipld_dagcbor::to_vec(&map_unsigned).unwrap(); 16 | // Hash dag_cbor to sha256 17 | let hash = Sha256::digest(&*unsigned_bytes); 18 | // Sign sha256 hash using private key 19 | let message = Message::from_digest_slice(hash.as_ref()).unwrap(); 20 | let mut sig = key.sign_ecdsa(message); 21 | // Convert to low-s 22 | sig.normalize_s(); 23 | // ASN.1 encoded per decode_dss_signature 24 | let normalized_compact_sig = sig.serialize_compact(); 25 | Ok(normalized_compact_sig) 26 | } 27 | 28 | pub fn sign_without_indexmap(obj: &T, key: &SecretKey) -> Result<[u8; 64]> { 29 | let unsigned_bytes = serde_ipld_dagcbor::to_vec(&obj)?; 30 | // Hash dag_cbor to sha256 31 | let hash = Sha256::digest(&*unsigned_bytes); 32 | // Sign sha256 hash using private key 33 | let message = Message::from_digest_slice(hash.as_ref())?; 34 | let mut sig = key.sign_ecdsa(message); 35 | // Convert to low-s 36 | sig.normalize_s(); 37 | // ASN.1 encoded per decode_dss_signature 38 | let normalized_compact_sig = sig.serialize_compact(); 39 | Ok(normalized_compact_sig) 40 | } 41 | -------------------------------------------------------------------------------- /rsky-common/src/time.rs: -------------------------------------------------------------------------------- 1 | use crate::RFC3339_VARIANT; 2 | use anyhow::Result; 3 | use chrono::offset::Utc as UtcOffset; 4 | use chrono::{DateTime, NaiveDateTime, Utc}; 5 | use std::time::SystemTime; 6 | 7 | pub const SECOND: i32 = 1000; 8 | pub const MINUTE: i32 = SECOND * 60; 9 | pub const HOUR: i32 = MINUTE * 60; 10 | pub const DAY: i32 = HOUR * 24; 11 | 12 | pub fn less_than_ago_s(time: DateTime, range: i32) -> bool { 13 | let now = SystemTime::now() 14 | .duration_since(SystemTime::UNIX_EPOCH) 15 | .expect("timestamp in micros since UNIX epoch") 16 | .as_secs() as usize; 17 | let x = time.timestamp() as usize + range as usize; 18 | now < x 19 | } 20 | 21 | pub fn from_str_to_micros(str: &String) -> i64 { 22 | NaiveDateTime::parse_from_str(str, RFC3339_VARIANT) 23 | .unwrap() 24 | .and_utc() 25 | .timestamp_micros() 26 | } 27 | 28 | pub fn from_str_to_millis(str: &String) -> Result { 29 | Ok(NaiveDateTime::parse_from_str(str, RFC3339_VARIANT)? 30 | .and_utc() 31 | .timestamp_millis()) 32 | } 33 | 34 | pub fn from_str_to_utc(str: &String) -> DateTime { 35 | NaiveDateTime::parse_from_str(str, RFC3339_VARIANT) 36 | .unwrap() 37 | .and_utc() 38 | } 39 | 40 | #[allow(deprecated)] 41 | pub fn from_micros_to_utc(micros: i64) -> DateTime { 42 | let nanoseconds = 230 * 1000000; 43 | DateTime::::from_utc(NaiveDateTime::from_timestamp(micros, nanoseconds), Utc) 44 | } 45 | 46 | pub fn from_micros_to_str(micros: i64) -> String { 47 | format!("{}", from_micros_to_utc(micros).format(RFC3339_VARIANT)) 48 | } 49 | 50 | pub fn from_millis_to_utc(millis: i64) -> DateTime { 51 | DateTime::::from_utc(NaiveDateTime::from_timestamp_millis(millis).unwrap(), Utc) 52 | } 53 | 54 | pub fn from_millis_to_str(millis: i64) -> String { 55 | format!("{}", from_millis_to_utc(millis).format(RFC3339_VARIANT)) 56 | } 57 | -------------------------------------------------------------------------------- /rsky-crypto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-crypto" 3 | version = "0.1.2" 4 | authors = ["Rudy Fraser "] 5 | description = "Rust library providing basic cryptographic helpers as needed in atproto" 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = true 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-crypto" 11 | documentation = "https://docs.rs/rsky-crypto" 12 | 13 | [dependencies] 14 | multibase = "0.9.1" 15 | secp256k1 = { workspace = true } 16 | anyhow = "1.0.79" 17 | p256 = { version = "0.13.2", features = ["ecdsa","arithmetic","alloc"] } 18 | unsigned-varint = "0.8.0" 19 | -------------------------------------------------------------------------------- /rsky-crypto/README.md: -------------------------------------------------------------------------------- 1 | # rsky-crypto 2 | 3 | Rust crate providing basic cryptographic helpers as needed in [atproto](https://atproto.com). 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-identity?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-identity) 6 | 7 | This crate implements the two currently supported cryptographic systems: 8 | 9 | - P-256 elliptic curve: aka "NIST P-256", aka secp256r1 (note the r), aka prime256v1 10 | - K-256 elliptic curve: aka "NIST K-256", aka secp256k1 (note the k) 11 | 12 | The details of cryptography in atproto are described in [the specification](https://atproto.com/specs/cryptography). This includes string encodings, validity of "low-S" signatures, byte representation "compression", hashing, and more. 13 | 14 | ## License 15 | 16 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-crypto/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::p256::plugin::P256_PLUGIN; 2 | use crate::secp256k1::plugin::SECP256K1_PLUGIN; 3 | use crate::types::DidKeyPlugin; 4 | 5 | pub const BASE58_MULTIBASE_PREFIX: &str = "z"; 6 | pub const DID_KEY_PREFIX: &str = "did:key:"; 7 | pub const SECP256K1_DID_PREFIX: [u8; 2] = [0xe7, 0x01]; 8 | pub const P256_DID_PREFIX: [u8; 2] = [0x80, 0x24]; 9 | pub const P256_JWT_ALG: &str = "ES256"; 10 | pub const SECP256K1_JWT_ALG: &str = "ES256K"; 11 | pub const PLUGINS: [DidKeyPlugin; 2] = [P256_PLUGIN, SECP256K1_PLUGIN]; 12 | -------------------------------------------------------------------------------- /rsky-crypto/src/did.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX, PLUGINS}; 2 | use crate::utils::{extract_multikey, extract_prefixed_bytes, has_prefix}; 3 | use anyhow::{bail, Result}; 4 | use multibase::{encode, Base}; 5 | 6 | #[derive(Clone)] 7 | pub struct ParsedMultikey { 8 | pub jwt_alg: String, 9 | pub key_bytes: Vec, 10 | } 11 | 12 | pub fn parse_multikey(multikey: String) -> Result { 13 | let prefixed_bytes = extract_prefixed_bytes(multikey)?; 14 | let plugin = PLUGINS 15 | .into_iter() 16 | .find(|p| has_prefix(&prefixed_bytes, &p.prefix.to_vec())); 17 | if let Some(plugin) = plugin { 18 | let key_bytes = (plugin.decompress_pubkey)(prefixed_bytes[plugin.prefix.len()..].to_vec())?; 19 | Ok(ParsedMultikey { 20 | jwt_alg: plugin.jwt_alg.to_string(), 21 | key_bytes, 22 | }) 23 | } else { 24 | bail!("Unsupported key type") 25 | } 26 | } 27 | 28 | pub fn format_multikey(jwt_alg: String, key_bytes: Vec) -> Result { 29 | let plugin = PLUGINS 30 | .into_iter() 31 | .find(|p| p.jwt_alg.to_string() == jwt_alg); 32 | if let Some(plugin) = plugin { 33 | let prefixed_bytes: Vec = 34 | [plugin.prefix.to_vec(), (plugin.compress_pubkey)(key_bytes)?].concat(); 35 | 36 | Ok([ 37 | BASE58_MULTIBASE_PREFIX, 38 | encode(Base::Base58Btc, prefixed_bytes).as_str(), 39 | ] 40 | .concat()) 41 | } else { 42 | bail!("Unsupported key type") 43 | } 44 | } 45 | 46 | pub fn parse_did_key(did: &String) -> Result { 47 | let multikey = extract_multikey(did)?; 48 | parse_multikey(multikey) 49 | } 50 | 51 | pub fn format_did_key(jwt_alg: String, key_bytes: Vec) -> Result { 52 | Ok([ 53 | DID_KEY_PREFIX, 54 | format_multikey(jwt_alg, key_bytes)?.as_str(), 55 | ] 56 | .concat()) 57 | } 58 | -------------------------------------------------------------------------------- /rsky-crypto/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod did; 3 | pub mod multibase; 4 | pub mod p256; 5 | pub mod secp256k1; 6 | pub mod types; 7 | pub mod utils; 8 | pub mod verify; 9 | -------------------------------------------------------------------------------- /rsky-crypto/src/multibase.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use multibase::{encode, Base}; 3 | 4 | pub fn multibase_to_bytes(mb: String) -> Result> { 5 | match mb.get(0..1) { 6 | None => bail!("empty multibase string"), 7 | Some(base) => match (base, mb.get(1..)) { 8 | ("f", Some(key)) => Ok(encode(Base::Base16Lower, key).into_bytes()), 9 | ("F", Some(key)) => Ok(encode(Base::Base16Upper, key).into_bytes()), 10 | ("b", Some(key)) => Ok(encode(Base::Base32Lower, key).into_bytes()), 11 | ("B", Some(key)) => Ok(encode(Base::Base32Upper, key).into_bytes()), 12 | ("z", Some(key)) => Ok(encode(Base::Base58Btc, key).into_bytes()), 13 | ("m", Some(key)) => Ok(encode(Base::Base64, key).into_bytes()), 14 | ("u", Some(key)) => Ok(encode(Base::Base64Url, key).into_bytes()), 15 | ("U", Some(key)) => Ok(encode(Base::Base64UrlPad, key).into_bytes()), 16 | (&_, _) => bail!("Unsupported multibase: {mb}"), 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rsky-crypto/src/p256/encoding.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use p256::ecdsa::VerifyingKey; 3 | 4 | pub fn compress_pubkey(pubkey_bytes: Vec) -> Result> { 5 | let point = VerifyingKey::from_sec1_bytes(pubkey_bytes.as_slice())?.to_encoded_point(true); 6 | Ok(point.as_bytes().to_vec()) 7 | } 8 | 9 | pub fn decompress_pubkey(compressed: Vec) -> Result> { 10 | if compressed.len() != 33 { 11 | bail!("Expected 33 byte compress pubkey") 12 | } 13 | let point = VerifyingKey::from_sec1_bytes(compressed.as_slice())?.to_encoded_point(false); 14 | Ok(point.as_bytes().to_vec()) 15 | } 16 | -------------------------------------------------------------------------------- /rsky-crypto/src/p256/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod encoding; 2 | pub mod operations; 3 | pub mod plugin; 4 | -------------------------------------------------------------------------------- /rsky-crypto/src/p256/operations.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::P256_DID_PREFIX; 2 | use crate::types::VerifyOptions; 3 | use crate::utils::{extract_multikey, extract_prefixed_bytes, has_prefix}; 4 | use anyhow::{bail, Result}; 5 | use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; 6 | 7 | pub fn verify_did_sig( 8 | did: &String, 9 | data: &[u8], 10 | sig: &[u8], 11 | opts: Option, 12 | ) -> Result { 13 | let prefixed_bytes = extract_prefixed_bytes(extract_multikey(&did)?)?; 14 | if !has_prefix(&prefixed_bytes, &P256_DID_PREFIX.to_vec()) { 15 | bail!("Not a P-256 did:key: {did}"); 16 | } 17 | let key_bytes = &prefixed_bytes[P256_DID_PREFIX.len()..]; 18 | verify_sig(key_bytes, data, sig, opts) 19 | } 20 | 21 | pub fn verify_sig( 22 | public_key: &[u8], 23 | data: &[u8], 24 | sig: &[u8], 25 | opts: Option, 26 | ) -> Result { 27 | let allow_malleable = match opts { 28 | Some(opts) if opts.allow_malleable_sig.is_some() => opts.allow_malleable_sig.unwrap(), 29 | _ => false, 30 | }; 31 | if !allow_malleable && !is_compact_format(sig) { 32 | return Ok(false); 33 | } 34 | let verifying_key = VerifyingKey::from_sec1_bytes(public_key)?; 35 | let signature = Signature::try_from(sig)?; 36 | Ok(verifying_key.verify(data, &signature).is_ok()) 37 | } 38 | 39 | pub fn is_compact_format(sig: &[u8]) -> bool { 40 | let mut parsed = match Signature::try_from(sig) { 41 | Ok(res) => res, 42 | Err(_) => return false, 43 | }; 44 | parsed = match parsed.normalize_s() { 45 | Some(res) => res, 46 | None => return false, 47 | }; 48 | parsed.to_vec() == *sig 49 | } 50 | -------------------------------------------------------------------------------- /rsky-crypto/src/p256/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{P256_DID_PREFIX, P256_JWT_ALG}; 2 | use crate::p256::encoding::{compress_pubkey, decompress_pubkey}; 3 | use crate::p256::operations::verify_did_sig; 4 | use crate::types::DidKeyPlugin; 5 | 6 | pub const P256_PLUGIN: DidKeyPlugin = DidKeyPlugin { 7 | prefix: P256_DID_PREFIX, 8 | jwt_alg: P256_JWT_ALG, 9 | compress_pubkey, 10 | decompress_pubkey, 11 | verify_signature: verify_did_sig, 12 | }; 13 | -------------------------------------------------------------------------------- /rsky-crypto/src/secp256k1/encoding.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use secp256k1::PublicKey; 3 | 4 | pub fn compress_pubkey(pubkey_bytes: Vec) -> Result> { 5 | let point = PublicKey::from_slice(pubkey_bytes.as_slice())?.serialize(); 6 | Ok(point.to_vec()) 7 | } 8 | 9 | pub fn decompress_pubkey(compressed: Vec) -> Result> { 10 | if compressed.len() != 33 { 11 | bail!("Expected 33 byte compress pubkey") 12 | } 13 | let point = PublicKey::from_slice(compressed.as_slice())?.serialize_uncompressed(); 14 | Ok(point.to_vec()) 15 | } 16 | -------------------------------------------------------------------------------- /rsky-crypto/src/secp256k1/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod encoding; 2 | pub mod operations; 3 | pub mod plugin; 4 | -------------------------------------------------------------------------------- /rsky-crypto/src/secp256k1/operations.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::SECP256K1_DID_PREFIX; 2 | use crate::types::VerifyOptions; 3 | use crate::utils::{extract_multikey, extract_prefixed_bytes, has_prefix}; 4 | use anyhow::{bail, Result}; 5 | use secp256k1::{ecdsa, Message, PublicKey, Secp256k1}; 6 | 7 | pub fn verify_did_sig( 8 | did: &String, 9 | data: &[u8], 10 | sig: &[u8], 11 | opts: Option, 12 | ) -> Result { 13 | let prefixed_bytes = extract_prefixed_bytes(extract_multikey(&did)?)?; 14 | if !has_prefix(&prefixed_bytes, &SECP256K1_DID_PREFIX.to_vec()) { 15 | bail!("Not a secp256k1 did:key: {did}"); 16 | } 17 | let key_bytes = &prefixed_bytes[SECP256K1_DID_PREFIX.len()..]; 18 | verify_sig(key_bytes, data, sig, opts) 19 | } 20 | 21 | pub fn verify_sig( 22 | public_key: &[u8], 23 | data: &[u8], 24 | sig: &[u8], 25 | opts: Option, 26 | ) -> Result { 27 | let allow_malleable = match opts { 28 | Some(opts) if opts.allow_malleable_sig.is_some() => opts.allow_malleable_sig.unwrap(), 29 | _ => false, 30 | }; 31 | let is_compact = is_compact_format(sig); 32 | if !allow_malleable && !is_compact { 33 | return Ok(false); 34 | } 35 | let secp = Secp256k1::verification_only(); 36 | let public_key = PublicKey::from_slice(public_key)?; 37 | let data = Message::from_digest_slice(data)?; 38 | let sig = match is_compact { 39 | true => ecdsa::Signature::from_compact(sig)?, 40 | false => ecdsa::Signature::from_der(sig)?, 41 | }; 42 | Ok(secp.verify_ecdsa(&data, &sig, &public_key).is_ok()) 43 | } 44 | 45 | pub fn is_compact_format(sig: &[u8]) -> bool { 46 | match ecdsa::Signature::from_compact(sig) { 47 | Ok(parsed) => parsed.serialize_compact() == sig, 48 | Err(_) => false, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rsky-crypto/src/secp256k1/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{SECP256K1_DID_PREFIX, SECP256K1_JWT_ALG}; 2 | use crate::secp256k1::encoding::{compress_pubkey, decompress_pubkey}; 3 | use crate::secp256k1::operations::verify_did_sig; 4 | use crate::types::DidKeyPlugin; 5 | 6 | pub const SECP256K1_PLUGIN: DidKeyPlugin = DidKeyPlugin { 7 | prefix: SECP256K1_DID_PREFIX, 8 | jwt_alg: SECP256K1_JWT_ALG, 9 | compress_pubkey, 10 | decompress_pubkey, 11 | verify_signature: verify_did_sig, 12 | }; 13 | -------------------------------------------------------------------------------- /rsky-crypto/src/types.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub struct DidKeyPlugin<'p> { 4 | pub prefix: [u8; 2], 5 | pub jwt_alg: &'p str, 6 | pub compress_pubkey: fn(Vec) -> Result>, 7 | pub decompress_pubkey: fn(Vec) -> Result>, 8 | pub verify_signature: fn(&String, &[u8], &[u8], Option) -> Result, 9 | } 10 | 11 | pub struct VerifyOptions { 12 | pub allow_malleable_sig: Option, 13 | } 14 | -------------------------------------------------------------------------------- /rsky-crypto/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX}; 2 | use anyhow::{bail, Result}; 3 | use multibase::decode; 4 | use multibase::Base::Base58Btc; 5 | use secp256k1::rand::rngs::OsRng; 6 | use secp256k1::rand::RngCore; 7 | use secp256k1::PublicKey; 8 | use unsigned_varint::encode::u16 as encode_varint; 9 | 10 | pub fn extract_multikey(did: &String) -> Result { 11 | if !did.starts_with(DID_KEY_PREFIX) { 12 | bail!("Incorrect prefix for did:key: {did}") 13 | } 14 | Ok(did[DID_KEY_PREFIX.len()..].to_string()) 15 | } 16 | 17 | pub fn extract_prefixed_bytes(multikey: String) -> Result> { 18 | if !multikey.starts_with(BASE58_MULTIBASE_PREFIX) { 19 | bail!("Incorrect prefix for multikey: {multikey}") 20 | } 21 | let (_base, bytes) = decode(&multikey)?; 22 | Ok(bytes) 23 | } 24 | 25 | pub fn has_prefix(bytes: &Vec, prefix: &Vec) -> bool { 26 | *prefix == bytes[0..prefix.len()] 27 | } 28 | 29 | pub fn random_bytes(len: usize) -> Vec { 30 | let mut buf = vec![0u8; len]; 31 | OsRng.fill_bytes(&mut buf); 32 | buf 33 | } 34 | 35 | /// https://github.com/gnunicorn/rust-multicodec/blob/master/src/lib.rs#L249-L260 36 | pub fn multicodec_wrap(bytes: Vec) -> Vec { 37 | let mut buf = [0u8; 3]; 38 | encode_varint(0xe7, &mut buf); 39 | let mut v: Vec = Vec::new(); 40 | for b in &buf { 41 | v.push(*b); 42 | // varint uses first bit to indicate another byte follows, stop if not the case 43 | if *b <= 127 { 44 | break; 45 | } 46 | } 47 | v.extend(bytes); 48 | v 49 | } 50 | 51 | pub fn encode_did_key(pubkey: &PublicKey) -> String { 52 | let pk_compact = pubkey.serialize(); 53 | let pk_wrapped = multicodec_wrap(pk_compact.to_vec()); 54 | let pk_multibase = multibase::encode(Base58Btc, pk_wrapped.as_slice()); 55 | format!("{DID_KEY_PREFIX}{pk_multibase}") 56 | } 57 | -------------------------------------------------------------------------------- /rsky-crypto/src/verify.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::PLUGINS; 2 | use crate::did::parse_did_key; 3 | use crate::types::VerifyOptions; 4 | use anyhow::{bail, Result}; 5 | 6 | pub fn verify_signature( 7 | did_key: &String, 8 | data: &[u8], 9 | sig: &[u8], 10 | opts: Option, 11 | ) -> Result { 12 | let parsed = parse_did_key(did_key)?; 13 | let plugin = PLUGINS 14 | .into_iter() 15 | .find(|p| p.jwt_alg.to_string() == parsed.jwt_alg); 16 | match plugin { 17 | None => bail!("Unsupported signature alg: {0}", parsed.jwt_alg), 18 | Some(plugin) => (plugin.verify_signature)(did_key, data, sig, opts), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rsky-feedgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-feedgen" 3 | version = "1.1.4" 4 | authors = ["Rudy Fraser "] 5 | description = "A framework for building AT Protocol feed generators, in Rust." 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-feedgen" 11 | documentation = "https://docs.rs/rsky-feedgen" 12 | 13 | [dependencies] 14 | rsky-lexicon = { workspace = true } 15 | rsky-common = { workspace = true } 16 | rocket = { version = "=0.5.1", features = ["json"] } 17 | serde = { version = "1.0.160", features = ["derive"] } 18 | serde_derive = "^1.0" 19 | serde_bytes = "0.11.9" 20 | serde_ipld_dagcbor = "0.3.0" 21 | serde_json = "1.0.96" 22 | serde_cbor = "0.11.2" 23 | diesel = { version = "=2.1.5", features = ["chrono", "postgres"] } 24 | dotenvy = "0.15" 25 | chrono = "0.4.26" 26 | regex = "1.8.4" 27 | base64 = "0.21.2" 28 | rand = "0.8.5" 29 | once_cell = "1.19.0" 30 | moka = { version = "0.12", features = ["future"] } 31 | chrono-tz = "0.10.1" 32 | 33 | [dependencies.rocket_sync_db_pools] 34 | version = "=0.1.0" 35 | features = ["diesel_postgres_pool"] 36 | 37 | [dependencies.reqwest] 38 | version = "^0.11" 39 | features = ["json", "multipart"] 40 | 41 | [dev-dependencies] 42 | tokio = { version = "1", features = ["full"] } 43 | temp-env = { version = "0.3.6",features = ["async_closure"] } 44 | serial_test = "*" 45 | -------------------------------------------------------------------------------- /rsky-feedgen/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Rust image. 2 | # https://hub.docker.com/_/rust 3 | FROM rust AS builder 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /usr/src/rsky 7 | COPY Cargo.toml rust-toolchain ./ 8 | 9 | # Copy only the Cargo.toml from our package 10 | COPY rsky-feedgen/Cargo.toml rsky-feedgen/Cargo.toml 11 | 12 | # Copy all workspace members except our target package 13 | COPY cypher cypher 14 | COPY rsky-common rsky-common 15 | COPY rsky-crypto rsky-crypto 16 | COPY rsky-identity rsky-identity 17 | COPY rsky-firehose rsky-firehose 18 | COPY rsky-jetstream-subscriber rsky-jetstream-subscriber 19 | COPY rsky-labeler rsky-labeler 20 | COPY rsky-lexicon rsky-lexicon 21 | COPY rsky-pds rsky-pds 22 | COPY rsky-relay rsky-relay 23 | COPY rsky-repo rsky-repo 24 | COPY rsky-satnav rsky-satnav 25 | COPY rsky-syntax rsky-syntax 26 | 27 | # Create an empty src directory to trick Cargo into thinking it's a valid Rust project 28 | RUN mkdir -p rsky-feedgen/src && echo "fn main() {}" > rsky-feedgen/src/main.rs 29 | 30 | # Install production dependencies and build a release artifact. 31 | RUN cargo build --release --package rsky-feedgen 32 | 33 | # Now copy the real source code and build the final binary 34 | COPY rsky-feedgen/src rsky-feedgen/src 35 | COPY rsky-feedgen/migrations rsky-feedgen/migrations 36 | COPY rsky-feedgen/diesel.toml rsky-feedgen/diesel.toml 37 | 38 | RUN cargo build --release --package rsky-feedgen 39 | 40 | FROM debian:bullseye-slim 41 | WORKDIR /usr/src/rsky 42 | COPY --from=builder /usr/src/rsky/target/release/rsky-feedgen rsky-feedgen 43 | LABEL org.opencontainers.image.source=https://github.com/blacksky-algorithms/rsky 44 | # Run the web service on container startup with the same environment variables 45 | CMD ["sh", "-c", "ROCKET_PORT=$PORT ROCKET_ADDRESS=0.0.0.0 ROCKET_ENV=prod ./rsky-feedgen"] -------------------------------------------------------------------------------- /rsky-feedgen/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId"] 7 | 8 | [migrations_directory] 9 | dir = "migrations" 10 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksky-algorithms/rsky/458f3826bf857f8d05a6fc42a6822000c36a60a1/rsky-feedgen/migrations/.keep -------------------------------------------------------------------------------- /rsky-feedgen/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-12-212554_001/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE post; 3 | DROP TABLE sub_state; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-12-212554_001/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.post ( 3 | uri character varying NOT NULL, 4 | cid character varying NOT NULL, 5 | "replyParent" character varying, 6 | "replyRoot" character varying, 7 | "indexedAt" character varying NOT NULL 8 | ); 9 | 10 | ALTER TABLE ONLY public.post 11 | DROP CONSTRAINT IF EXISTS post_pkey; 12 | ALTER TABLE ONLY public.post 13 | ADD CONSTRAINT post_pkey PRIMARY KEY (uri); 14 | 15 | CREATE TABLE IF NOT EXISTS public.sub_state ( 16 | service character varying NOT NULL, 17 | cursor integer NOT NULL 18 | ); 19 | 20 | ALTER TABLE ONLY public.sub_state 21 | DROP CONSTRAINT IF EXISTS sub_state_pkey; 22 | ALTER TABLE ONLY public.sub_state 23 | ADD CONSTRAINT sub_state_pkey PRIMARY KEY (service); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-26-191108_create_memberships/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE membership; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-26-191108_create_memberships/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.membership ( 3 | did character varying NOT NULL, 4 | included BOOLEAN NOT NULL, 5 | excluded BOOLEAN NOT NULL, 6 | list character varying NOT NULL 7 | ); 8 | 9 | ALTER TABLE ONLY public.membership 10 | ADD CONSTRAINT membership_pkey PRIMARY KEY (did); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-27-050840_003/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | DROP CONSTRAINT IF EXISTS unique_sequence; 4 | 5 | ALTER TABLE post 6 | DROP COLUMN IF EXISTS prev, 7 | DROP COLUMN IF EXISTS sequence; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-27-050840_003/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE post 3 | ADD COLUMN prev VARCHAR, 4 | ADD COLUMN sequence NUMERIC; 5 | 6 | ALTER TABLE post 7 | ADD CONSTRAINT unique_sequence UNIQUE (sequence); 8 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-27-054243_004/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | ALTER COLUMN sequence TYPE NUMERIC; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-06-27-054243_004/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE post 3 | ALTER COLUMN sequence TYPE bigint; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-02-021849_005/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE sub_state 3 | ALTER COLUMN cursor TYPE INTEGER; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-02-021849_005/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE sub_state 3 | ALTER COLUMN cursor TYPE bigint; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-04-040513_006/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE visitor; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-04-040513_006/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.visitor ( 3 | id SERIAL PRIMARY KEY, 4 | did character varying NOT NULL, 5 | web character varying NOT NULL, 6 | visited_at character varying NOT NULL 7 | ); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-06-051432_007/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE public.like; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-06-051432_007/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.like ( 3 | uri character varying NOT NULL, 4 | cid character varying NOT NULL, 5 | author character varying NOT NULL, 6 | "subjectCid" character varying NOT NULL, 7 | "subjectUri" character varying NOT NULL, 8 | "createdAt" character varying NOT NULL, 9 | "indexedAt" character varying NOT NULL 10 | ); 11 | 12 | ALTER TABLE ONLY public.like 13 | DROP CONSTRAINT IF EXISTS like_pkey; 14 | ALTER TABLE ONLY public.like 15 | ADD CONSTRAINT like_pkey PRIMARY KEY (uri); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-06-222616_008/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.like 3 | DROP CONSTRAINT IF EXISTS unique_like_sequence; 4 | 5 | ALTER TABLE public.like 6 | DROP COLUMN IF EXISTS prev, 7 | DROP COLUMN IF EXISTS sequence; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-06-222616_008/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.like 3 | ADD COLUMN prev VARCHAR, 4 | ADD COLUMN sequence NUMERIC; 5 | 6 | ALTER TABLE public.like 7 | ADD CONSTRAINT unique_like_sequence UNIQUE (sequence); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-07-024146_009/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.like 3 | ALTER COLUMN sequence TYPE NUMERIC; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-07-024146_009/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.like 3 | ALTER COLUMN sequence TYPE bigint; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-07-161148_010/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE public.follow; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-07-07-161148_010/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.follow ( 3 | uri character varying NOT NULL, 4 | cid character varying NOT NULL, 5 | author character varying NOT NULL, 6 | "subject" character varying NOT NULL, 7 | "createdAt" character varying NOT NULL, 8 | "indexedAt" character varying NOT NULL, 9 | prev character varying, 10 | sequence bigint 11 | ); 12 | 13 | ALTER TABLE ONLY public.follow 14 | DROP CONSTRAINT IF EXISTS follow_pkey; 15 | ALTER TABLE ONLY public.follow 16 | ADD CONSTRAINT follow_pkey PRIMARY KEY (uri); 17 | ALTER TABLE ONLY public.follow 18 | DROP CONSTRAINT IF EXISTS unique_follow_sequence; 19 | ALTER TABLE ONLY public.follow 20 | ADD CONSTRAINT unique_follow_sequence UNIQUE (sequence); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-11-223751_011/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.post 3 | DROP COLUMN IF EXISTS "text", 4 | DROP COLUMN IF EXISTS lang; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-11-223751_011/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.post 3 | ADD COLUMN "text" VARCHAR, 4 | ADD COLUMN lang VARCHAR; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-12-164434_012/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE public.image; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-12-164434_012/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.image ( 3 | cid character varying NOT NULL, 4 | alt character varying, 5 | "postCid" character varying NOT NULL, 6 | "postUri" character varying NOT NULL, 7 | "createdAt" character varying NOT NULL, 8 | "indexedAt" character varying NOT NULL 9 | ); 10 | 11 | ALTER TABLE ONLY public.image 12 | DROP CONSTRAINT IF EXISTS image_pkey; 13 | ALTER TABLE ONLY public.image 14 | ADD CONSTRAINT image_pkey PRIMARY KEY (cid); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-17-190313_013/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.image 3 | DROP COLUMN IF EXISTS labels; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-17-190313_013/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.image 3 | ADD COLUMN labels TEXT []; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-20-161623_014/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.visitor 3 | DROP COLUMN IF EXISTS feed; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-09-20-161623_014/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.visitor 3 | ADD COLUMN feed VARCHAR; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-10-26-204539_015/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | DROP COLUMN IF EXISTS author, 4 | DROP COLUMN IF EXISTS externalUri, 5 | DROP COLUMN IF EXISTS externalTitle, 6 | DROP COLUMN IF EXISTS externalDescription, 7 | DROP COLUMN IF EXISTS externalThumb, 8 | DROP COLUMN IF EXISTS quoteCid, 9 | DROP COLUMN IF EXISTS quoteUri; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-10-26-204539_015/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE post 3 | ADD COLUMN author character varying NOT NULL DEFAULT '', 4 | ADD COLUMN "externalUri" character varying, 5 | ADD COLUMN "externalTitle" character varying, 6 | ADD COLUMN "externalDescription" character varying, 7 | ADD COLUMN "externalThumb" character varying, 8 | ADD COLUMN "quoteCid" character varying, 9 | ADD COLUMN "quoteUri" character varying; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-10-27-222305_016/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE public.membership 3 | DROP CONSTRAINT IF EXISTS membership_pkey; 4 | 5 | ALTER TABLE ONLY public.membership 6 | ADD CONSTRAINT membership_pkey PRIMARY KEY (did); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-10-27-222305_016/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE public.membership 3 | DROP CONSTRAINT IF EXISTS membership_pkey; 4 | 5 | ALTER TABLE public.membership 6 | ADD CONSTRAINT membership_pkey PRIMARY KEY (did, list); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-11-06-001456_017/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | ADD CONSTRAINT unique_sequence UNIQUE (sequence); 4 | ALTER TABLE public.like 5 | ADD CONSTRAINT unique_like_sequence UNIQUE (sequence); 6 | ALTER TABLE ONLY public.follow 7 | ADD CONSTRAINT unique_follow_sequence UNIQUE (sequence); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2023-11-06-001456_017/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE post 3 | DROP CONSTRAINT IF EXISTS unique_sequence; 4 | ALTER TABLE public.like 5 | DROP CONSTRAINT IF EXISTS unique_like_sequence; 6 | ALTER TABLE ONLY public.follow 7 | DROP CONSTRAINT IF EXISTS unique_follow_sequence; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-09-12-133947_018/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE public.video; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-09-12-133947_018/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE IF NOT EXISTS public.video ( 3 | cid character varying NOT NULL, 4 | alt character varying, 5 | "postCid" character varying NOT NULL, 6 | "postUri" character varying NOT NULL, 7 | "createdAt" character varying NOT NULL, 8 | "indexedAt" character varying NOT NULL, 9 | labels TEXT [] 10 | ); 11 | 12 | ALTER TABLE ONLY public.video 13 | DROP CONSTRAINT IF EXISTS video_pkey; 14 | ALTER TABLE ONLY public.video 15 | ADD CONSTRAINT video_pkey PRIMARY KEY (cid); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-11-09-085601_019/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | DROP COLUMN IF EXISTS "createdAt"; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-11-09-085601_019/up.sql: -------------------------------------------------------------------------------- 1 | -- Step 1: Add the new column with the same data type 2 | ALTER TABLE post 3 | ADD COLUMN "createdAt" character varying DEFAULT to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'); 4 | 5 | -- Step 2: Update the new column to copy values from the existing column 6 | UPDATE post 7 | SET "createdAt" = "indexedAt"; 8 | 9 | -- Step 3: Alter the column to set it as NOT NULL 10 | ALTER TABLE post 11 | ALTER COLUMN "createdAt" SET NOT NULL; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-11-16-235919_020/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE public.banned_from_tv; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-11-16-235919_020/up.sql: -------------------------------------------------------------------------------- 1 | -- For tracking people not allowed to access a feed 2 | CREATE TABLE IF NOT EXISTS public.banned_from_tv ( 3 | did character varying NOT NULL, 4 | reason character varying, 5 | "createdAt" character varying DEFAULT to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), 6 | tags TEXT [] 7 | ); 8 | 9 | ALTER TABLE ONLY public.banned_from_tv 10 | DROP CONSTRAINT IF EXISTS banned_from_tv_pkey; 11 | ALTER TABLE ONLY public.banned_from_tv 12 | ADD CONSTRAINT banned_from_tv_pkey PRIMARY KEY (did); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-12-01-074204_021/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP INDEX IF EXISTS idx_like_subjecturi_indexedat; 3 | DROP INDEX IF EXISTS idx_post_createdAt_cid; 4 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-12-01-074204_021/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE INDEX idx_like_subjecturi_indexedat ON public.like ("subjectUri", "indexedAt"); 3 | CREATE INDEX idx_post_createdAt_cid ON "post" ("createdAt" DESC, "cid" DESC) 4 | WHERE "replyParent" IS NULL AND "replyRoot" IS NULL; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-12-14-193843_022/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE post 3 | DROP CONSTRAINT IF EXISTS no_nulls_in_labels, 4 | DROP COLUMN IF EXISTS labels; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2024-12-14-193843_022/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE post 3 | ADD COLUMN labels TEXT [] NOT NULL DEFAULT '{}', 4 | ADD CONSTRAINT no_nulls_in_labels CHECK (NOT (labels && ARRAY[NULL])); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-002028_023/down.sql: -------------------------------------------------------------------------------- 1 | -- Revert the post table 2 | ALTER TABLE post 3 | ALTER COLUMN "createdAt" DROP DEFAULT; 4 | 5 | -- Convert back to character varying using a text cast 6 | ALTER TABLE post 7 | ALTER COLUMN "createdAt" TYPE character varying USING "createdAt"::text, 8 | ALTER COLUMN "indexedAt" TYPE character varying USING "indexedAt"::text; 9 | 10 | -- Reapply the original default for "createdAt" 11 | ALTER TABLE post 12 | ALTER COLUMN "createdAt" SET DEFAULT to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'); 13 | 14 | UPDATE post 15 | SET "createdAt" = "indexedAt"; 16 | 17 | -- Set "createdAt" as NOT NULL 18 | ALTER TABLE post 19 | ALTER COLUMN "createdAt" SET NOT NULL; 20 | 21 | -- Revert the like table (note the quotes because "like" is a reserved word) 22 | ALTER TABLE "like" 23 | ALTER COLUMN "createdAt" TYPE character varying 24 | USING "createdAt"::text; 25 | ALTER TABLE "like" 26 | ALTER COLUMN "indexedAt" TYPE character varying 27 | USING "indexedAt"::text; 28 | 29 | -- Revert the video table 30 | ALTER TABLE video 31 | ALTER COLUMN "createdAt" TYPE character varying 32 | USING "createdAt"::text; 33 | ALTER TABLE video 34 | ALTER COLUMN "indexedAt" TYPE character varying 35 | USING "indexedAt"::text; 36 | 37 | -- Revert the image table 38 | ALTER TABLE image 39 | ALTER COLUMN "createdAt" TYPE character varying 40 | USING "createdAt"::text; 41 | ALTER TABLE image 42 | ALTER COLUMN "indexedAt" TYPE character varying 43 | USING "indexedAt"::text; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-002028_023/up.sql: -------------------------------------------------------------------------------- 1 | -- Drop defaults on post table so they don’t interfere 2 | ALTER TABLE post ALTER COLUMN "createdAt" DROP DEFAULT; 3 | 4 | -- Update the post table 5 | ALTER TABLE post 6 | ALTER COLUMN "createdAt" TYPE timestamptz 7 | USING "createdAt"::timestamptz; 8 | ALTER TABLE post 9 | ALTER COLUMN "indexedAt" TYPE timestamptz 10 | USING "indexedAt"::timestamptz; 11 | ALTER TABLE post ALTER COLUMN "createdAt" SET DEFAULT now(); 12 | 13 | -- Update the like table (note the quotes because like is reserved) 14 | ALTER TABLE "like" 15 | ALTER COLUMN "createdAt" TYPE timestamptz 16 | USING "createdAt"::timestamptz; 17 | ALTER TABLE "like" 18 | ALTER COLUMN "indexedAt" TYPE timestamptz 19 | USING "indexedAt"::timestamptz; 20 | 21 | -- Update the video table 22 | ALTER TABLE video 23 | ALTER COLUMN "createdAt" TYPE timestamptz 24 | USING "createdAt"::timestamptz; 25 | ALTER TABLE video 26 | ALTER COLUMN "indexedAt" TYPE timestamptz 27 | USING "indexedAt"::timestamptz; 28 | 29 | -- Update the image table 30 | ALTER TABLE image 31 | ALTER COLUMN "createdAt" TYPE timestamptz 32 | USING "createdAt"::timestamptz; 33 | ALTER TABLE image 34 | ALTER COLUMN "indexedAt" TYPE timestamptz 35 | USING "indexedAt"::timestamptz; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044129_024/down.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | DROP INDEX CONCURRENTLY IF EXISTS image_posturi_idx; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044129_024/up.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | CREATE INDEX CONCURRENTLY image_posturi_idx ON image("postUri"); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044132_025/down.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | DROP INDEX CONCURRENTLY IF EXISTS video_postcid_idx; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044132_025/up.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | CREATE INDEX CONCURRENTLY video_postcid_idx ON video("postCid"); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044137_026/down.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | DROP INDEX CONCURRENTLY IF EXISTS video_posturi_idx; -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-044137_026/up.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | CREATE INDEX CONCURRENTLY video_posturi_idx ON video("postUri"); -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-070232_026/down.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | DROP INDEX CONCURRENTLY IF EXISTS image_postcid_idx; 3 | -------------------------------------------------------------------------------- /rsky-feedgen/migrations/2025-02-26-070232_026/up.sql: -------------------------------------------------------------------------------- 1 | -- !no-transaction 2 | CREATE INDEX CONCURRENTLY image_postcid_idx ON image("postCid"); -------------------------------------------------------------------------------- /rsky-feedgen/src/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::models::JwtParts; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use std::time::{SystemTime, UNIX_EPOCH}; 4 | 5 | pub fn verify_jwt(jwtstr: &String, service_did: &String) -> Result { 6 | let parts = jwtstr.split(".").map(String::from).collect::>(); 7 | 8 | if parts.len() != 3 { 9 | return Err("poorly formatted jwt".into()); 10 | } 11 | 12 | let bytes = general_purpose::STANDARD_NO_PAD.decode(&parts[1]).unwrap(); 13 | 14 | if let Ok(payload) = std::str::from_utf8(&bytes) { 15 | if let Ok(payload) = serde_json::from_str::(payload) { 16 | let start = SystemTime::now(); 17 | let since_the_epoch = start 18 | .duration_since(UNIX_EPOCH) 19 | .expect("Time went backwards"); 20 | 21 | if since_the_epoch.as_millis() / 1000 > payload.exp { 22 | return Err("jwt expired".into()); 23 | } 24 | if service_did != &payload.aud { 25 | return Err("jwt audience does not match service did".into()); 26 | } 27 | // TO DO: Verify cryptographic signature 28 | if let Ok(jwtstr) = serde_json::to_string(&payload) { 29 | Ok(jwtstr) 30 | } else { 31 | Err("error parsing payload".into()) 32 | } 33 | } else { 34 | Err("error parsing payload".into()) 35 | } 36 | } else { 37 | Err("error parsing payload".into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rsky-feedgen/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | use diesel::pg::PgConnection; 2 | use diesel::prelude::*; 3 | use dotenvy::dotenv; 4 | use std::env; 5 | 6 | pub fn establish_connection() -> Result> { 7 | dotenv().ok(); 8 | 9 | let database_url = env::var("DATABASE_URL").unwrap_or("".into()); 10 | let result = PgConnection::establish(&database_url).map_err(|_| { 11 | eprintln!("Error connecting to {database_url:?}"); 12 | "Internal error" 13 | })?; 14 | 15 | Ok(result) 16 | } 17 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/algo_response.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct AlgoResponse { 3 | #[serde(rename = "cursor", skip_serializing_if = "Option::is_none")] 4 | pub cursor: Option, 5 | #[serde(rename = "feed")] 6 | pub feed: Vec, 7 | } 8 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/create_request.rs: -------------------------------------------------------------------------------- 1 | use rsky_lexicon::com::atproto::label::Label; 2 | 3 | #[derive(Debug, Deserialize, Serialize)] 4 | #[serde(tag = "$type")] 5 | pub enum Lexicon { 6 | #[serde(rename(deserialize = "app.bsky.feed.post", serialize = "app.bsky.feed.post"))] 7 | AppBskyFeedPost(rsky_lexicon::app::bsky::feed::Post), 8 | #[serde(rename(deserialize = "app.bsky.feed.like", serialize = "app.bsky.feed.like"))] 9 | AppBskyFeedLike(rsky_lexicon::app::bsky::feed::like::Like), 10 | #[serde(rename( 11 | deserialize = "app.bsky.graph.follow", 12 | serialize = "app.bsky.graph.follow" 13 | ))] 14 | AppBskyFeedFollow(rsky_lexicon::app::bsky::graph::follow::Follow), 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize)] 18 | #[serde(untagged)] 19 | pub enum CreateRecord { 20 | Lexicon(Lexicon), 21 | Label(Label), 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize)] 25 | pub struct CreateRequest { 26 | #[serde(rename = "uri")] 27 | pub uri: String, 28 | #[serde(rename = "cid")] 29 | pub cid: String, 30 | #[serde(rename = "sequence")] 31 | pub sequence: Option, 32 | #[serde(rename = "prev")] 33 | pub prev: Option, 34 | #[serde(rename = "author")] 35 | pub author: String, 36 | #[serde(rename = "record")] 37 | pub record: CreateRecord, 38 | } 39 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/delete_request.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Serialize, Deserialize)] 2 | pub struct DeleteRequest { 3 | #[serde(rename = "uri")] 4 | pub uri: String, 5 | } 6 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/internal_error_code.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] 2 | pub enum InternalErrorCode { 3 | #[serde(rename = "no_internal_error")] 4 | NoInternalError, 5 | #[serde(rename = "internal_error")] 6 | InternalError, 7 | #[serde(rename = "cancelled")] 8 | Cancelled, 9 | #[serde(rename = "deadline_exceeded")] 10 | DeadlineExceeded, 11 | #[serde(rename = "already_exists")] 12 | AlreadyExists, 13 | #[serde(rename = "resource_exhausted")] 14 | ResourceExhausted, 15 | #[serde(rename = "failed_precondition")] 16 | FailedPrecondition, 17 | #[serde(rename = "aborted")] 18 | Aborted, 19 | #[serde(rename = "out_of_range")] 20 | OutOfRange, 21 | #[serde(rename = "unavailable")] 22 | Unavailable, 23 | #[serde(rename = "data_loss")] 24 | DataLoss, 25 | } 26 | 27 | impl ToString for InternalErrorCode { 28 | fn to_string(&self) -> String { 29 | match self { 30 | Self::NoInternalError => String::from("no_internal_error"), 31 | Self::InternalError => String::from("internal_error"), 32 | Self::Cancelled => String::from("cancelled"), 33 | Self::DeadlineExceeded => String::from("deadline_exceeded"), 34 | Self::AlreadyExists => String::from("already_exists"), 35 | Self::ResourceExhausted => String::from("resource_exhausted"), 36 | Self::FailedPrecondition => String::from("failed_precondition"), 37 | Self::Aborted => String::from("aborted"), 38 | Self::OutOfRange => String::from("out_of_range"), 39 | Self::Unavailable => String::from("unavailable"), 40 | Self::DataLoss => String::from("data_loss"), 41 | } 42 | } 43 | } 44 | 45 | impl Default for InternalErrorCode { 46 | fn default() -> InternalErrorCode { 47 | Self::NoInternalError 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/internal_error_message_response.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct InternalErrorMessageResponse { 3 | #[serde(rename = "code", skip_serializing_if = "Option::is_none")] 4 | pub code: Option, 5 | #[serde(rename = "message", skip_serializing_if = "Option::is_none")] 6 | pub message: Option, 7 | } 8 | 9 | impl InternalErrorMessageResponse { 10 | pub fn new() -> InternalErrorMessageResponse { 11 | InternalErrorMessageResponse { 12 | code: None, 13 | message: None, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/jwt_parts.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Serialize, Deserialize)] 2 | pub struct JwtParts { 3 | pub iss: String, 4 | pub aud: String, 5 | pub exp: u128, 6 | } 7 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/known_service.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Serialize, Deserialize)] 2 | pub struct KnownService { 3 | #[serde(rename = "id")] 4 | pub id: String, 5 | #[serde(rename = "type")] 6 | pub r#type: String, 7 | #[serde(rename = "serviceEndpoint")] 8 | pub service_endpoint: String, 9 | } 10 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/membership.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | #[derive(Queryable, Selectable, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 4 | #[diesel(table_name = crate::schema::membership)] 5 | #[diesel(check_for_backend(diesel::pg::Pg))] 6 | pub struct Membership { 7 | #[serde(rename = "did")] 8 | pub did: String, 9 | #[serde(rename = "included")] 10 | pub included: bool, 11 | #[serde(rename = "excluded")] 12 | pub excluded: bool, 13 | #[serde(rename = "list")] 14 | pub list: String, 15 | } 16 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error_code; 2 | pub use self::error_code::ErrorCode; 3 | pub mod internal_error_code; 4 | pub use self::internal_error_code::InternalErrorCode; 5 | pub mod internal_error_message_response; 6 | pub use self::internal_error_message_response::InternalErrorMessageResponse; 7 | pub mod not_found_error_code; 8 | pub use self::not_found_error_code::NotFoundErrorCode; 9 | pub mod path_unknown_error_message_response; 10 | pub use self::path_unknown_error_message_response::PathUnknownErrorMessageResponse; 11 | pub mod validation_error_message_response; 12 | pub use self::validation_error_message_response::ValidationErrorMessageResponse; 13 | pub mod post; 14 | pub use self::post::Post; 15 | pub mod post_result; 16 | pub use self::post_result::PostResult; 17 | pub mod algo_response; 18 | pub use self::algo_response::AlgoResponse; 19 | pub mod sub_state; 20 | pub use self::sub_state::SubState; 21 | pub mod create_request; 22 | pub use self::create_request::CreateRequest; 23 | pub use self::create_request::Lexicon; 24 | pub mod delete_request; 25 | pub use self::delete_request::DeleteRequest; 26 | pub mod membership; 27 | pub use self::membership::Membership; 28 | pub mod well_known; 29 | pub use self::well_known::WellKnown; 30 | pub mod known_service; 31 | pub use self::known_service::KnownService; 32 | pub mod jwt_parts; 33 | pub use self::jwt_parts::JwtParts; 34 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/not_found_error_code.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] 2 | pub enum NotFoundErrorCode { 3 | #[serde(rename = "not_found_error")] 4 | NotFoundError, 5 | #[serde(rename = "undefined_endpoint")] 6 | UndefinedEndpoint, 7 | #[serde(rename = "unimplemented")] 8 | Unimplemented, 9 | } 10 | 11 | impl ToString for NotFoundErrorCode { 12 | fn to_string(&self) -> String { 13 | match self { 14 | Self::NotFoundError => String::from("not_found_error"), 15 | Self::UndefinedEndpoint => String::from("undefined_endpoint"), 16 | Self::Unimplemented => String::from("unimplemented"), 17 | } 18 | } 19 | } 20 | 21 | impl Default for NotFoundErrorCode { 22 | fn default() -> NotFoundErrorCode { 23 | Self::NotFoundError 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/path_unknown_error_message_response.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 5 | pub struct PathUnknownErrorMessageResponse { 6 | #[serde(rename = "code", skip_serializing_if = "Option::is_none")] 7 | pub code: Option, 8 | #[serde(rename = "message", skip_serializing_if = "Option::is_none")] 9 | pub message: Option, 10 | } 11 | 12 | impl PathUnknownErrorMessageResponse { 13 | pub fn new() -> PathUnknownErrorMessageResponse { 14 | PathUnknownErrorMessageResponse { 15 | code: None, 16 | message: None, 17 | } 18 | } 19 | } 20 | 21 | impl fmt::Display for PathUnknownErrorMessageResponse { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | let mut message = "".to_owned(); 24 | if let Some(error_message) = &self.message { 25 | message = error_message.clone(); 26 | } 27 | write!(f, "not_found_error: {}", message) 28 | } 29 | } 30 | 31 | impl Error for PathUnknownErrorMessageResponse {} 32 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/post_result.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct PostResult { 3 | #[serde(rename = "post")] 4 | pub post: String, 5 | } 6 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/sub_state.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | #[derive(Queryable, Selectable, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 4 | #[diesel(table_name = crate::schema::sub_state)] 5 | #[diesel(check_for_backend(diesel::pg::Pg))] 6 | pub struct SubState { 7 | #[serde(rename = "service")] 8 | pub service: String, 9 | #[serde(rename = "cursor")] 10 | pub cursor: i64, 11 | } 12 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/validation_error_message_response.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 5 | pub struct ValidationErrorMessageResponse { 6 | #[serde(rename = "code", skip_serializing_if = "Option::is_none")] 7 | pub code: Option, 8 | #[serde(rename = "message", skip_serializing_if = "Option::is_none")] 9 | pub message: Option, 10 | } 11 | 12 | impl ValidationErrorMessageResponse { 13 | pub fn new() -> ValidationErrorMessageResponse { 14 | ValidationErrorMessageResponse { 15 | code: None, 16 | message: None, 17 | } 18 | } 19 | } 20 | 21 | impl fmt::Display for ValidationErrorMessageResponse { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | let mut message = "".to_owned(); 24 | if let Some(error_message) = &self.message { 25 | message = error_message.clone(); 26 | } 27 | write!(f, "validation_error: {}", message) 28 | } 29 | } 30 | 31 | impl Error for ValidationErrorMessageResponse {} 32 | -------------------------------------------------------------------------------- /rsky-feedgen/src/models/well_known.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Serialize, Deserialize)] 2 | pub struct WellKnown { 3 | #[serde(rename = "@context")] 4 | pub context: Vec, 5 | #[serde(rename = "id")] 6 | pub id: String, 7 | #[serde(rename = "service")] 8 | pub service: Vec, 9 | } 10 | -------------------------------------------------------------------------------- /rsky-firehose/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-firehose" 3 | version = "0.2.1" 4 | authors = ["Rudy Fraser "] 5 | description = "A framework for subscribing to the AT Protocol firehose, in Rust." 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-firehose" 11 | documentation = "https://docs.rs/rsky-firehose" 12 | 13 | [dependencies] 14 | rsky-lexicon = { workspace = true } 15 | lexicon_cid = {workspace = true} 16 | ciborium = "0.2.0" 17 | futures = "0.3.28" 18 | tokio = { version = "1.28.0", features = ["full"] } 19 | tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } 20 | url = "2.3.1" 21 | chrono = { version = "0.4.24", features = ["serde"] } 22 | derive_builder = "0.12.0" 23 | miette = "5.8.0" 24 | parking_lot = "0.12.1" 25 | reqwest = { version = "0.11.16", features = ["json", "rustls"] } 26 | serde = { version = "1.0.160", features = ["derive"] } 27 | serde_derive = "^1.0" 28 | serde_bytes = "0.11.9" 29 | serde_ipld_dagcbor = "0.6.1" 30 | serde_json = "1.0.96" 31 | serde_cbor = "0.11.2" 32 | thiserror = "1.0.40" 33 | dotenvy = "0.15.7" 34 | retry = "2.0.0" 35 | anyhow = "1.0.81" 36 | multihash = "0.19" 37 | -------------------------------------------------------------------------------- /rsky-firehose/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Rust image. 2 | # https://hub.docker.com/_/rust 3 | FROM rust AS builder 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /usr/src/rsky 7 | 8 | # Start by copying the workspace configuration files 9 | COPY Cargo.toml rust-toolchain ./ 10 | 11 | # Copy only the Cargo.toml from our package 12 | COPY rsky-firehose/Cargo.toml rsky-firehose/Cargo.toml 13 | 14 | # Copy all workspace members except our target package 15 | COPY cypher cypher 16 | COPY rsky-common rsky-common 17 | COPY rsky-crypto rsky-crypto 18 | COPY rsky-feedgen rsky-feedgen 19 | COPY rsky-identity rsky-identity 20 | COPY rsky-jetstream-subscriber rsky-jetstream-subscriber 21 | COPY rsky-labeler rsky-labeler 22 | COPY rsky-lexicon rsky-lexicon 23 | COPY rsky-pds rsky-pds 24 | COPY rsky-relay rsky-relay 25 | COPY rsky-repo rsky-repo 26 | COPY rsky-satnav rsky-satnav 27 | COPY rsky-syntax rsky-syntax 28 | 29 | # Create an empty src directory to trick Cargo into thinking it's a valid Rust project 30 | RUN mkdir -p rsky-firehose/src && echo "fn main() {}" > rsky-firehose/src/main.rs 31 | 32 | ## Install production dependencies and build a release artifact. 33 | RUN cargo build --release --package rsky-firehose 34 | 35 | # Now copy the real source code and build the final binary 36 | COPY rsky-firehose/src rsky-firehose/src 37 | 38 | RUN cargo build --release --package rsky-firehose 39 | 40 | FROM debian:bullseye-slim 41 | WORKDIR /usr/src/rsky 42 | COPY --from=builder /usr/src/rsky/target/release/rsky-firehose rsky-firehose 43 | LABEL org.opencontainers.image.source=https://github.com/blacksky-algorithms/rsky 44 | CMD ["./rsky-firehose"] -------------------------------------------------------------------------------- /rsky-firehose/README.md: -------------------------------------------------------------------------------- 1 | # rsky-firehose 2 | 3 | Subscriber for the AT Protocol [`event stream`](https://atproto.com/specs/event-stream) 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-firehose?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-firehose) 6 | 7 | ## License 8 | 9 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-firehose/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | extern crate serde; 5 | extern crate serde_json; 6 | 7 | pub mod car; 8 | pub mod firehose; 9 | pub mod models; 10 | -------------------------------------------------------------------------------- /rsky-firehose/src/models/create_op.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct CreateOp { 3 | #[serde(rename = "uri")] 4 | pub uri: String, 5 | #[serde(rename = "cid")] 6 | pub cid: String, 7 | #[serde(rename = "sequence")] 8 | pub sequence: i64, 9 | #[serde(rename = "prev")] 10 | pub prev: Option, 11 | #[serde(rename = "author")] 12 | pub author: String, 13 | #[serde(rename = "record")] 14 | pub record: T, 15 | } 16 | -------------------------------------------------------------------------------- /rsky-firehose/src/models/delete_op.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct DeleteOp { 3 | #[serde(rename = "uri")] 4 | pub uri: String, 5 | } 6 | -------------------------------------------------------------------------------- /rsky-firehose/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_op; 2 | pub use self::create_op::CreateOp; 3 | pub mod delete_op; 4 | pub use self::delete_op::DeleteOp; 5 | -------------------------------------------------------------------------------- /rsky-identity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-identity" 3 | version = "0.1.0" 4 | authors = ["Rudy Fraser "] 5 | description = "Rust library for decentralized identities in atproto using DIDs and handles." 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-identity" 11 | documentation = "https://docs.rs/rsky-identity" 12 | 13 | [dependencies] 14 | anyhow = "1.0.82" 15 | reqwest = { version = "0.12", default-features = false, features = ["gzip", "hickory-dns", "http2", "json", "rustls-tls-webpki-roots-no-provider"] } 16 | serde_json = { version = "1.0.115",features = ["preserve_order"] } 17 | urlencoding = "2.1.3" 18 | thiserror = "1.0.58" 19 | url = "2.5.0" 20 | serde = { version = "1.0.197", features = ["derive"] } 21 | rsky-crypto = { workspace = true } 22 | hickory-resolver = "0.24.1" 23 | -------------------------------------------------------------------------------- /rsky-identity/README.md: -------------------------------------------------------------------------------- 1 | # rsky-identity 2 | 3 | Rust crate for decentralized identities in [atproto](https://atproto.com) using DIDs and handles 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-identity?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-identity) 6 | 7 | ## License 8 | 9 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-identity/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use urlencoding::{decode, encode}; 3 | 4 | pub const SECOND: i32 = 1000; 5 | pub const MINUTE: i32 = SECOND * 60; 6 | pub const HOUR: i32 = MINUTE * 60; 7 | pub const DAY: i32 = HOUR * 24; 8 | 9 | pub fn encode_uri_component(input: &String) -> String { 10 | encode(input).to_string() 11 | } 12 | pub fn decode_uri_component(input: &str) -> Result { 13 | Ok(decode(input)?.to_string()) 14 | } 15 | -------------------------------------------------------------------------------- /rsky-identity/src/did/atproto_data.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use rsky_crypto::constants::{P256_JWT_ALG, SECP256K1_JWT_ALG}; 3 | use rsky_crypto::did::{format_did_key, parse_multikey}; 4 | use rsky_crypto::multibase::multibase_to_bytes; 5 | 6 | #[derive(Clone)] 7 | pub struct VerificationMaterial { 8 | pub r#type: String, 9 | pub public_key_multibase: String, 10 | } 11 | 12 | pub fn get_did_key_from_multibase(key: VerificationMaterial) -> Result> { 13 | let key_bytes = multibase_to_bytes(key.public_key_multibase.clone())?; 14 | let did_key = match key.r#type.as_str() { 15 | "EcdsaSecp256r1VerificationKey2019" => { 16 | Some(format_did_key(P256_JWT_ALG.to_string(), key_bytes)?) 17 | } 18 | "EcdsaSecp256k1VerificationKey2019" => { 19 | Some(format_did_key(SECP256K1_JWT_ALG.to_string(), key_bytes)?) 20 | } 21 | "Multikey" => { 22 | let parsed = parse_multikey(key.public_key_multibase)?; 23 | Some(format_did_key(parsed.jwt_alg, parsed.key_bytes)?) 24 | } 25 | _ => None, 26 | }; 27 | Ok(did_key) 28 | } 29 | -------------------------------------------------------------------------------- /rsky-identity/src/did/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod atproto_data; 2 | pub mod did_resolver; 3 | pub mod plc_resolver; 4 | pub mod web_resolver; 5 | -------------------------------------------------------------------------------- /rsky-identity/src/did/plc_resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::common::encode_uri_component; 2 | use crate::types::DidCache; 3 | use anyhow::{bail, Result}; 4 | use serde_json::Value; 5 | use std::time::Duration; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct DidPlcResolver { 9 | pub plc_url: String, 10 | pub timeout: Duration, 11 | pub cache: Option, 12 | } 13 | 14 | impl DidPlcResolver { 15 | pub fn new(plc_url: String, timeout: Duration, cache: Option) -> Self { 16 | Self { 17 | plc_url, 18 | timeout, 19 | cache, 20 | } 21 | } 22 | 23 | pub async fn resolve_no_check(&self, did: String) -> Result> { 24 | let client = reqwest::Client::new(); 25 | let response = client 26 | .get(format!("{0}/{1}", self.plc_url, encode_uri_component(&did))) 27 | .timeout(self.timeout) 28 | .header("Connection", "Keep-Alive") 29 | .header("Keep-Alive", "timeout=5, max=1000") 30 | .send() 31 | .await?; 32 | let res = &response; 33 | match res.error_for_status_ref() { 34 | Ok(_) => Ok(Some(response.json::().await?)), 35 | // Positively not found, versus due to e.g. network error 36 | Err(error) if error.status() == Some(reqwest::StatusCode::NOT_FOUND) => Ok(None), 37 | Err(error) => bail!(error.to_string()), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rsky-identity/src/errors.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("Could not resolve DID: `{0}`")] 7 | DidNotFoundError(String), 8 | #[error("Poorly formatted DID: `{0}`")] 9 | PoorlyFormattedDidError(String), 10 | #[error("Unsupported DID method: `{0}`")] 11 | UnsupportedDidMethodError(String), 12 | #[error("Poorly formatted DID Document: `{0:#?}`")] 13 | PoorlyFormattedDidDocumentError(Value), 14 | #[error("Unsupported did:web paths: `{0}`")] 15 | UnsupportedDidWebPathError(String), 16 | } 17 | -------------------------------------------------------------------------------- /rsky-identity/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate url; 2 | 3 | use crate::did::did_resolver::DidResolver; 4 | use crate::handle::HandleResolver; 5 | use crate::types::{DidCache, DidResolverOpts, HandleResolverOpts, IdentityResolverOpts}; 6 | use std::time::Duration; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct IdResolver { 10 | pub handle: HandleResolver, 11 | pub did: DidResolver, 12 | } 13 | 14 | impl IdResolver { 15 | pub fn new(opts: IdentityResolverOpts) -> Self { 16 | let IdentityResolverOpts { 17 | timeout, 18 | plc_url, 19 | did_cache, 20 | backup_nameservers, 21 | } = opts; 22 | let timeout = timeout.unwrap_or_else(|| Duration::from_millis(3000)); 23 | let did_cache = did_cache.unwrap_or_else(|| DidCache { 24 | stale_ttl: Default::default(), 25 | max_ttl: Default::default(), 26 | cache: Default::default(), 27 | }); 28 | 29 | Self { 30 | handle: HandleResolver::new(HandleResolverOpts { 31 | timeout: Some(timeout), 32 | backup_nameservers, 33 | }), 34 | did: DidResolver::new(DidResolverOpts { 35 | timeout: Some(timeout), 36 | plc_url, 37 | did_cache, 38 | }), 39 | } 40 | } 41 | } 42 | 43 | pub mod common; 44 | pub mod did; 45 | pub mod errors; 46 | pub mod handle; 47 | pub mod types; 48 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-jetstream-subscriber" 3 | version = "0.1.0" 4 | authors = ["Rudy Fraser ", "Ripperoni "] 5 | description = "Jetstream subscriber service" 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = true 9 | 10 | [dependencies] 11 | tracing = "0.1" 12 | tracing-subscriber = "0.3" 13 | rsky-lexicon = { workspace = true } 14 | futures = "0.3.28" 15 | tokio = { version = "1.28.0", features = ["full"] } 16 | tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } 17 | url = "2.3.1" 18 | chrono = { version = "0.4.24", features = ["serde"] } 19 | reqwest = { version = "0.11.16", features = ["json", "rustls"] } 20 | serde = { version = "1.0.160", features = ["derive"] } 21 | serde_derive = "^1.0" 22 | serde_json = "1.0.96" 23 | dotenvy = "0.15.7" 24 | anyhow = "1.0.81" 25 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Rust image. 2 | # https://hub.docker.com/_/rust 3 | FROM rust AS builder 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /usr/src/rsky 7 | COPY Cargo.toml rust-toolchain ./ 8 | 9 | # Copy only the Cargo.toml from our package 10 | COPY rsky-jetstream-subscriber/Cargo.toml rsky-jetstream-subscriber/Cargo.toml 11 | 12 | # Copy all workspace members except our target package 13 | COPY cypher cypher 14 | COPY rsky-common rsky-common 15 | COPY rsky-crypto rsky-crypto 16 | COPY rsky-feedgen rsky-feedgen 17 | COPY rsky-firehose rsky-firehose 18 | COPY rsky-identity rsky-identity 19 | COPY rsky-labeler rsky-labeler 20 | COPY rsky-lexicon rsky-lexicon 21 | COPY rsky-pds rsky-pds 22 | COPY rsky-relay rsky-relay 23 | COPY rsky-repo rsky-repo 24 | COPY rsky-satnav rsky-satnav 25 | COPY rsky-syntax rsky-syntax 26 | 27 | # Create an empty src directory to trick Cargo into thinking it's a valid Rust project 28 | RUN mkdir -p rsky-jetstream-subscriber/src && echo "fn main() {}" > rsky-jetstream-subscriber/src/main.rs 29 | 30 | ## Install production dependencies and build a release artifact. 31 | RUN cargo build --release --package rsky-jetstream-subscriber 32 | 33 | # Now copy the real source code and build the final binary 34 | COPY rsky-jetstream-subscriber/src rsky-jetstream-subscriber/src 35 | 36 | RUN cargo build --release --package rsky-jetstream-subscriber 37 | 38 | FROM debian:bullseye-slim 39 | WORKDIR /usr/src/rsky 40 | COPY --from=builder /usr/src/rsky/target/release/rsky-jetstream-subscriber rsky-jetstream-subscriber 41 | LABEL org.opencontainers.image.source=https://github.com/blacksky-algorithms/rsky 42 | CMD ["./rsky-jetstream-subscriber"] -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/README.md: -------------------------------------------------------------------------------- 1 | #

Rsky-Jetstream

2 | 3 |

An AT Protocol Jetstream Subscriber

4 | 5 | [![dependency status](https://deps.rs/repo/github/blacksky-algorithms/rsky/status.svg?style=flat-square)](https://deps.rs/repo/github/blacksky-algorithms/rsky) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | extern crate serde; 5 | extern crate serde_json; 6 | 7 | pub mod jetstream; 8 | pub mod models; 9 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/src/models/create_op.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct CreateOp { 3 | #[serde(rename = "uri")] 4 | pub uri: String, 5 | #[serde(rename = "cid")] 6 | pub cid: String, 7 | #[serde(rename = "author")] 8 | pub author: String, 9 | #[serde(rename = "record")] 10 | pub record: T, 11 | } 12 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/src/models/delete_op.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct DeleteOp { 3 | #[serde(rename = "uri")] 4 | pub uri: String, 5 | } 6 | -------------------------------------------------------------------------------- /rsky-jetstream-subscriber/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_op; 2 | pub use self::create_op::CreateOp; 3 | pub mod delete_op; 4 | pub use self::delete_op::DeleteOp; 5 | -------------------------------------------------------------------------------- /rsky-labeler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-labeler" 3 | version = "0.1.3" 4 | authors = ["Rudy Fraser "] 5 | description = "AT Protocol firehose subscriber that labels content for a moderation service." 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-labeler" 11 | documentation = "https://docs.rs/rsky-labeler" 12 | 13 | [dependencies] 14 | rsky-lexicon = { workspace = true } 15 | rsky-common = { workspace = true } 16 | lexicon_cid = {workspace = true} 17 | ciborium = "0.2.0" 18 | futures = "0.3.28" 19 | tokio = { version = "1.28.0", features = ["full"] } 20 | tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } 21 | chrono = { version = "0.4.24", features = ["serde"] } 22 | derive_builder = "0.20.2" 23 | miette = "7.4.0" 24 | parking_lot = "0.12.1" 25 | reqwest = { version = "0.12.9", features = ["json"] } 26 | serde = { version = "1.0.160", features = ["derive"] } 27 | serde_derive = "^1.0" 28 | serde_bytes = "0.11.9" 29 | serde_ipld_dagcbor = "0.6.1" 30 | serde_json = "1.0.96" 31 | serde_cbor = "0.11.2" 32 | thiserror = "2.0.9" 33 | dotenvy = "0.15.7" 34 | retry = "2.0.0" 35 | anyhow = "1.0.81" 36 | atrium-api = { version = "0.24.6", features = ["namespace-toolsozone"] } 37 | atrium-xrpc-client = "0.5.8" 38 | atrium-ipld = {package = "ipld-core", version = "0.4.1"} 39 | multihash = "0.19" 40 | -------------------------------------------------------------------------------- /rsky-labeler/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Rust image. 2 | # https://hub.docker.com/_/rust 3 | FROM rust 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /usr/src/rsky 7 | COPY . . 8 | 9 | # Install production dependencies and build a release artifact. 10 | RUN cargo build --package rsky-labeler 11 | 12 | # Run the web service on container startup. 13 | CMD ["sh", "-c", "cargo run --package rsky-labeler"] -------------------------------------------------------------------------------- /rsky-labeler/README.md: -------------------------------------------------------------------------------- 1 | # rsky-labeler: Labeler 2 | 3 | Firehose consumer that labels content. 4 | 5 | ## License 6 | 7 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-labeler/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | extern crate serde; 5 | extern crate serde_json; 6 | 7 | pub static APP_USER_AGENT: &str = concat!( 8 | env!("CARGO_PKG_HOMEPAGE"), 9 | "@", 10 | env!("CARGO_PKG_NAME"), 11 | "/", 12 | env!("CARGO_PKG_VERSION"), 13 | ); 14 | 15 | pub mod car; 16 | pub mod firehose; 17 | -------------------------------------------------------------------------------- /rsky-lexicon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-lexicon" 3 | version = "0.2.8" 4 | edition = "2021" 5 | publish = true 6 | description = "Bluesky API library" 7 | authors = ["Rudy Fraser "] 8 | homepage = "https://blackskyweb.xyz" 9 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-lexicon" 10 | license = "Apache-2.0" 11 | keywords = ["bluesky", "atproto"] 12 | readme = "README.md" 13 | documentation = "https://docs.rs/rsky-lexicon" 14 | 15 | [dependencies] 16 | chrono = { version = "0.4.24", features = ["serde"] } 17 | derive_builder = "0.12.0" 18 | miette = "5.8.0" 19 | parking_lot = "0.12.1" 20 | serde = {workspace = true} 21 | serde_json = {workspace = true} 22 | serde_cbor = {workspace = true} 23 | serde_derive = "^1.0" 24 | serde_bytes = "0.11.9" 25 | thiserror = "1.0.40" 26 | secp256k1 = {workspace = true} 27 | lexicon_cid = {workspace = true} 28 | anyhow = "1.0.79" # @TODO: Remove anyhow in lib 29 | -------------------------------------------------------------------------------- /rsky-lexicon/README.md: -------------------------------------------------------------------------------- 1 | # rsky-lexicon 2 | 3 | WIP API library for the AT Protocol [`lexicon`](https://atproto.com/guides/lexicon) 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-lexicon?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-lexicon) 6 | 7 | ## License 8 | 9 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/embed/external.rs: -------------------------------------------------------------------------------- 1 | use crate::com::atproto::repo::Blob; 2 | 3 | /// A representation of some externally linked content (eg, a URL and 'card'), 4 | /// embedded in a Bluesky record (eg, a post). 5 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct External { 8 | pub external: ExternalObject, 9 | } 10 | 11 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct ExternalObject { 14 | pub uri: String, 15 | pub title: String, 16 | pub description: String, 17 | pub thumb: Option, 18 | } 19 | 20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 21 | #[serde(tag = "$type")] 22 | #[serde(rename = "app.bsky.embed.external#view")] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct View { 25 | pub external: ViewExternal, 26 | } 27 | 28 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct ViewExternal { 31 | pub uri: String, 32 | pub title: String, 33 | pub description: String, 34 | pub thumb: Option, 35 | } 36 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/embed/images.rs: -------------------------------------------------------------------------------- 1 | use crate::com::atproto::repo::Blob; 2 | 3 | /// A set of images embedded in a Bluesky record (eg, a post). 4 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Images { 7 | pub images: Vec, 8 | } 9 | 10 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 11 | pub struct Image { 12 | pub image: Blob, 13 | /// Alt text description of the image, for accessibility 14 | pub alt: String, 15 | pub aspect_ratio: Option, 16 | } 17 | 18 | /// width:height represents an aspect ratio. It may be approximate, 19 | /// and may not correspond to absolute dimensions in any given unit. 20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 21 | pub struct AspectRatio { 22 | pub width: usize, 23 | pub height: usize, 24 | } 25 | 26 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 27 | #[serde(tag = "$type")] 28 | #[serde(rename = "app.bsky.embed.images#view")] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct View { 31 | pub images: Vec, 32 | } 33 | 34 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct ViewImage { 37 | /// Fully-qualified URL where a thumbnail of the image can be fetched. 38 | /// For example, CDN location provided by the App View. 39 | pub thumb: String, 40 | /// Fully-qualified URL where a large version of the image can be fetched. 41 | /// May or may not be the exact original blob. For example, CDN location provided by the App View. 42 | pub fullsize: String, 43 | /// Alt text description of the image, for accessibility. 44 | pub alt: String, 45 | pub aspect_ratio: Option, 46 | } 47 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/embed/record_with_media.rs: -------------------------------------------------------------------------------- 1 | use crate::app::bsky::embed::record::{Record, View as ViewRecord}; 2 | use crate::app::bsky::embed::{MediaUnion, MediaViewUnion}; 3 | 4 | /// A representation of a record embedded in a Bluesky record (eg, a post), 5 | /// alongside other compatible embeds. For example, a quote post and image, 6 | /// or a quote post and external URL card. 7 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct RecordWithMedia { 10 | pub record: Record, 11 | pub media: MediaUnion, 12 | } 13 | 14 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 15 | #[serde(tag = "$type")] 16 | #[serde(rename = "app.bsky.embed.recordWithMedia#view")] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct View { 19 | pub record: ViewRecord, 20 | pub media: MediaViewUnion, 21 | } 22 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/embed/video.rs: -------------------------------------------------------------------------------- 1 | use crate::app::bsky::embed::images::AspectRatio; 2 | use crate::com::atproto::repo::Blob; 3 | 4 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Video { 7 | pub video: Blob, 8 | pub captions: Option>, 9 | /// Alt text description of video image, for accessibility 10 | pub alt: Option, 11 | pub aspect_ratio: Option, 12 | } 13 | 14 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 15 | pub struct Caption { 16 | pub lang: String, 17 | pub file: Blob, 18 | } 19 | 20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 21 | #[serde(tag = "$type")] 22 | #[serde(rename = "app.bsky.embed.video#view")] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct View { 25 | pub cid: String, 26 | pub playlist: String, 27 | pub thumbnail: Option, 28 | pub alt: Option, 29 | pub aspect_ratio: Option, 30 | } 31 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/feed/like.rs: -------------------------------------------------------------------------------- 1 | use crate::com::atproto::repo::StrongRef; 2 | use chrono::{DateTime, Utc}; 3 | 4 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 5 | #[serde(tag = "$type")] 6 | #[serde(rename = "app.bsky.feed.like")] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct Like { 9 | pub created_at: DateTime, 10 | pub subject: StrongRef, 11 | } 12 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/graph/follow.rs: -------------------------------------------------------------------------------- 1 | /// Record declaring a social 'follow' relationship of another account. 2 | /// Duplicate follows will be ignored by the AppView. 3 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 4 | #[serde(tag = "$type")] 5 | #[serde(rename = "app.bsky.graph.follow")] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct Follow { 8 | pub created_at: String, 9 | pub subject: String, 10 | } 11 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/labeler/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::bsky::actor::ProfileViewBasic; 2 | use crate::com::atproto::label::Label; 3 | 4 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 5 | #[serde(tag = "$type")] 6 | #[serde(rename = "app.bsky.labeler.defs#labelerView")] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct LabelerView { 9 | pub uri: String, 10 | pub cid: String, 11 | pub creator: ProfileViewBasic, 12 | pub like_count: Option, 13 | pub viewer: Option, 14 | pub indexed_at: String, 15 | pub labels: Option>, 16 | } 17 | 18 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 19 | pub struct LabelerViewerState { 20 | pub like: Option, 21 | } 22 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actor; 2 | pub mod embed; 3 | pub mod feed; 4 | pub mod graph; 5 | pub mod labeler; 6 | pub mod notification; 7 | pub mod richtext; 8 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/notification/mod.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 2 | #[serde(rename_all = "camelCase")] 3 | pub struct RegisterPushInput { 4 | pub service_did: String, 5 | pub token: String, 6 | pub platform: String, 7 | pub app_id: String, 8 | } 9 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/bsky/richtext/mod.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 2 | pub struct Facet { 3 | pub index: ByteSlice, 4 | pub features: Vec, 5 | } 6 | 7 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 8 | #[serde(tag = "$type")] 9 | pub enum Features { 10 | #[serde(rename = "app.bsky.richtext.facet#mention")] 11 | Mention(Mention), 12 | #[serde(rename = "app.bsky.richtext.facet#link")] 13 | Link(Link), 14 | #[serde(rename = "app.bsky.richtext.facet#tag")] 15 | Tag(Tag), 16 | } 17 | 18 | /// Facet feature for mention of another account. The text is usually a handle, including a '@' 19 | /// prefix, but the facet reference is a DID. 20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 21 | pub struct Mention { 22 | pub did: String, 23 | } 24 | 25 | /// Facet feature for a URL. The text URL may have been simplified or truncated, but the facet 26 | /// reference should be a complete URL. 27 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 28 | pub struct Link { 29 | pub uri: String, 30 | } 31 | 32 | /// Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference 33 | /// should not (except in the case of 'double hashtags'). 34 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 35 | pub struct Tag { 36 | pub tag: String, 37 | } 38 | 39 | /// Specifies the sub-string range a facet feature applies to. 40 | /// Start index is inclusive, end index is exclusive. 41 | /// Indices are zero-indexed, counting bytes of the UTF-8 encoded text. 42 | /// NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; 43 | /// in these languages, convert to byte arrays before working with facets. 44 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 45 | pub struct ByteSlice { 46 | #[serde(rename = "byteStart")] 47 | pub byte_start: usize, 48 | #[serde(rename = "byteEnd")] 49 | pub byte_end: usize, 50 | } 51 | -------------------------------------------------------------------------------- /rsky-lexicon/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bsky; 2 | -------------------------------------------------------------------------------- /rsky-lexicon/src/chat/bsky/actor/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::bsky::actor::{RefProfileAssociated, ViewerState}; 2 | use crate::com::atproto::label::Label; 3 | 4 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct ProfileViewBasic { 7 | pub did: String, 8 | pub handle: String, 9 | pub display_name: Option, 10 | pub avatar: Option, 11 | pub associated: Option, 12 | pub viewer: Option, 13 | pub labels: Option>, 14 | // Set to true when the actor cannot actively participate in converations 15 | pub chat_disabled: Option, 16 | } 17 | -------------------------------------------------------------------------------- /rsky-lexicon/src/chat/bsky/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actor; 2 | pub mod convo; 3 | pub mod moderation; 4 | -------------------------------------------------------------------------------- /rsky-lexicon/src/chat/bsky/moderation/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rsky-lexicon/src/chat/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bsky; 2 | -------------------------------------------------------------------------------- /rsky-lexicon/src/com/atproto/identity.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value as JsonValue; 3 | use std::collections::BTreeMap; 4 | 5 | #[derive(Debug, Deserialize, Serialize, Clone)] 6 | pub struct ResolveHandleOutput { 7 | pub did: String, 8 | } 9 | 10 | /// Updates the current account's handle. Verifies handle validity, and updates did:plc document if 11 | /// necessary. Implemented by PDS, and requires auth. 12 | #[derive(Debug, Deserialize, Serialize, Clone)] 13 | pub struct UpdateHandleInput { 14 | /// The new handle. 15 | pub handle: String, 16 | } 17 | 18 | #[derive(Clone, Debug, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct SignPlcOperationRequest { 21 | pub token: String, 22 | pub rotation_keys: Option>, 23 | pub also_known_as: Option>, 24 | pub verification_methods: Option>, 25 | pub services: Option, 26 | } 27 | 28 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct GetRecommendedDidCredentialsResponse { 31 | pub also_known_as: Vec, 32 | pub verification_methods: JsonValue, 33 | pub rotation_keys: Vec, 34 | pub services: JsonValue, 35 | } 36 | 37 | #[derive(Clone, Debug, Serialize, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct SubmitPlcOperationRequest { 40 | pub operation: JsonValue, 41 | } 42 | -------------------------------------------------------------------------------- /rsky-lexicon/src/com/atproto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | pub mod identity; 3 | pub mod label; 4 | pub mod repo; 5 | pub mod server; 6 | pub mod sync; 7 | -------------------------------------------------------------------------------- /rsky-lexicon/src/com/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod atproto; 2 | -------------------------------------------------------------------------------- /rsky-lexicon/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | extern crate serde; 5 | extern crate serde_json; 6 | 7 | pub mod app; 8 | pub mod blob_refs; 9 | pub mod chat; 10 | pub mod com; 11 | -------------------------------------------------------------------------------- /rsky-pds/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Rust image. 2 | # https://hub.docker.com/_/rust 3 | FROM rust AS builder 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /usr/src/rsky 7 | COPY Cargo.toml rust-toolchain ./ 8 | 9 | # Copy only the Cargo.toml from our package 10 | COPY rsky-pds/Cargo.toml rsky-pds/Cargo.toml 11 | 12 | # Copy all workspace members except our target package 13 | COPY cypher cypher 14 | COPY rsky-common rsky-common 15 | COPY rsky-crypto rsky-crypto 16 | COPY rsky-feedgen rsky-feedgen 17 | COPY rsky-firehose rsky-firehose 18 | COPY rsky-identity rsky-identity 19 | COPY rsky-jetstream-subscriber rsky-jetstream-subscriber 20 | COPY rsky-labeler rsky-labeler 21 | COPY rsky-lexicon rsky-lexicon 22 | COPY rsky-relay rsky-relay 23 | COPY rsky-repo rsky-repo 24 | COPY rsky-satnav rsky-satnav 25 | COPY rsky-syntax rsky-syntax 26 | 27 | # Create an empty src directory to trick Cargo into thinking it's a valid Rust project 28 | RUN mkdir -p rsky-pds/src && echo "fn main() {}" > rsky-pds/src/main.rs 29 | 30 | # Install production dependencies and build a release artifact. 31 | RUN cargo build --release --package rsky-pds 32 | 33 | # Now copy the real source code and build the final binary 34 | COPY rsky-pds/src rsky-pds/src 35 | COPY rsky-pds/migrations rsky-pds/migrations 36 | COPY rsky-pds/diesel.toml rsky-pds/diesel.toml 37 | 38 | RUN cargo build --release --package rsky-pds 39 | 40 | FROM debian:bullseye-slim 41 | WORKDIR /usr/src/rsky 42 | COPY --from=builder /usr/src/rsky/target/release/rsky-pds rsky-pds 43 | LABEL org.opencontainers.image.source=https://github.com/blacksky-algorithms/rsky 44 | # Run the web service on container startup with the same environment variables 45 | CMD ["sh", "-c", "ROCKET_PORT=$PORT ROCKET_ADDRESS=0.0.0.0", "./rsky-pds"] -------------------------------------------------------------------------------- /rsky-pds/README.md: -------------------------------------------------------------------------------- 1 | # rsky-pds: Personal Data Server (PDS) 2 | 3 | Rust implementation of an atproto PDS. 4 | 5 | ## License 6 | 7 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-pds/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId"] 7 | schema = "pds" 8 | 9 | [migrations_directory] 10 | dir = "migrations" 11 | -------------------------------------------------------------------------------- /rsky-pds/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /rsky-pds/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /rsky-pds/migrations/2023-11-15-004814_pds_init/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE pds.repo_seq; 3 | DROP TABLE pds.did_doc; 4 | DROP TABLE pds.account_pref; 5 | DROP TABLE pds.backlink; 6 | DROP TABLE pds.record_blob; 7 | DROP TABLE pds.blob; 8 | DROP TABLE pds.record; 9 | DROP TABLE pds.repo_block; 10 | DROP TABLE pds.repo_root; 11 | DROP TABLE pds.email_token; 12 | DROP TABLE pds.account; 13 | DROP TABLE pds.actor; 14 | DROP TABLE pds.refresh_token; 15 | DROP TABLE pds.invite_code_use; 16 | DROP TABLE pds.invite_code; 17 | DROP TABLE pds.app_password; 18 | DROP SCHEMA IF EXISTS pds; 19 | -------------------------------------------------------------------------------- /rsky-pds/migrations/2024-03-20-042639_account_deactivation/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | ALTER TABLE pds.actor 3 | DROP COLUMN IF EXISTS deactivatedAt, 4 | DROP COLUMN IF EXISTS deleteAfter; -------------------------------------------------------------------------------- /rsky-pds/migrations/2024-03-20-042639_account_deactivation/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | ALTER TABLE pds.actor 3 | ADD COLUMN "deactivatedAt" character varying, 4 | ADD COLUMN "deleteAfter" character varying; -------------------------------------------------------------------------------- /rsky-pds/src/account_manager/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod auth; 3 | pub mod email_token; 4 | pub mod invite; 5 | pub mod password; 6 | pub mod repo; 7 | -------------------------------------------------------------------------------- /rsky-pds/src/account_manager/helpers/repo.rs: -------------------------------------------------------------------------------- 1 | use crate::db::DbConn; 2 | use anyhow::Result; 3 | use diesel::*; 4 | use lexicon_cid::Cid; 5 | use rsky_common; 6 | 7 | pub async fn update_root(did: String, cid: Cid, rev: String, db: &DbConn) -> Result<()> { 8 | // @TODO balance risk of a race in the case of a long retry 9 | use crate::schema::pds::repo_root::dsl as RepoRootSchema; 10 | 11 | let now = rsky_common::now(); 12 | 13 | db.run(move |conn| { 14 | insert_into(RepoRootSchema::repo_root) 15 | .values(( 16 | RepoRootSchema::did.eq(did), 17 | RepoRootSchema::cid.eq(cid.to_string()), 18 | RepoRootSchema::rev.eq(rev.clone()), 19 | RepoRootSchema::indexedAt.eq(now), 20 | )) 21 | .on_conflict(RepoRootSchema::did) 22 | .do_update() 23 | .set(( 24 | RepoRootSchema::cid.eq(cid.to_string()), 25 | RepoRootSchema::rev.eq(rev), 26 | )) 27 | .execute(conn) 28 | }) 29 | .await?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /rsky-pds/src/actor_store/aws/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod s3; 2 | -------------------------------------------------------------------------------- /rsky-pds/src/actor_store/preference/util.rs: -------------------------------------------------------------------------------- 1 | use crate::auth_verifier::AuthScope; 2 | 3 | const FULL_ACCESS_ONLY_PREFS: [&str; 1] = ["app.bsky.actor.defs#personalDetailsPref"]; 4 | 5 | pub fn pref_in_scope(scope: AuthScope, pref_type: String) -> bool { 6 | if scope == AuthScope::Access { 7 | return true; 8 | } 9 | !FULL_ACCESS_ONLY_PREFS.contains(&&*pref_type) 10 | } 11 | -------------------------------------------------------------------------------- /rsky-pds/src/actor_store/repo/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sql_repo; 2 | pub mod types; 3 | -------------------------------------------------------------------------------- /rsky-pds/src/actor_store/repo/types.rs: -------------------------------------------------------------------------------- 1 | use lexicon_cid::Cid; 2 | use rsky_repo::block_map::BlockMap; 3 | pub struct SyncEvtData { 4 | pub cid: Cid, 5 | pub rev: String, 6 | pub blocks: BlockMap, 7 | } 8 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/actor/get_preferences.rs: -------------------------------------------------------------------------------- 1 | use crate::actor_store::aws::s3::S3BlobStore; 2 | use crate::actor_store::ActorStore; 3 | use crate::apis::ApiError; 4 | use crate::auth_verifier::AccessStandard; 5 | use crate::db::DbConn; 6 | use anyhow::Result; 7 | use aws_config::SdkConfig; 8 | use rocket::serde::json::Json; 9 | use rocket::State; 10 | use rsky_lexicon::app::bsky::actor::{GetPreferencesOutput, RefPreferences}; 11 | 12 | async fn inner_get_preferences( 13 | s3_config: &State, 14 | auth: AccessStandard, 15 | db: DbConn, 16 | ) -> Result { 17 | let auth = auth.access.credentials.unwrap(); 18 | let requester = auth.did.unwrap().clone(); 19 | let actor_store = ActorStore::new( 20 | requester.clone(), 21 | S3BlobStore::new(requester.clone(), s3_config), 22 | db, 23 | ); 24 | let preferences: Vec = actor_store 25 | .pref 26 | .get_preferences(Some("app.bsky".to_string()), auth.scope.unwrap()) 27 | .await?; 28 | 29 | Ok(GetPreferencesOutput { preferences }) 30 | } 31 | 32 | /// Get private preferences attached to the current account. Expected use is synchronization 33 | /// between multiple devices, and import/export during account migration. Requires auth. 34 | #[tracing::instrument(skip_all)] 35 | #[rocket::get("/xrpc/app.bsky.actor.getPreferences")] 36 | pub async fn get_preferences( 37 | s3_config: &State, 38 | auth: AccessStandard, 39 | db: DbConn, 40 | ) -> Result, ApiError> { 41 | match inner_get_preferences(s3_config, auth, db).await { 42 | Ok(res) => Ok(Json(res)), 43 | Err(error) => { 44 | tracing::error!("@LOG: ERROR: {error}"); 45 | Err(ApiError::RuntimeError) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/actor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_preferences; 2 | pub mod get_profile; 3 | pub mod get_profiles; 4 | pub mod put_preferences; 5 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/actor/put_preferences.rs: -------------------------------------------------------------------------------- 1 | use crate::actor_store::aws::s3::S3BlobStore; 2 | use crate::actor_store::ActorStore; 3 | use crate::apis::ApiError; 4 | use crate::auth_verifier::AccessStandard; 5 | use crate::db::DbConn; 6 | use anyhow::Result; 7 | use aws_config::SdkConfig; 8 | use rocket::serde::json::Json; 9 | use rocket::State; 10 | use rsky_lexicon::app::bsky::actor::PutPreferencesInput; 11 | 12 | async fn inner_put_preferences( 13 | body: Json, 14 | s3_config: &State, 15 | auth: AccessStandard, 16 | db: DbConn, 17 | ) -> Result<(), ApiError> { 18 | let PutPreferencesInput { preferences } = body.into_inner(); 19 | let auth = auth.access.credentials.unwrap(); 20 | let requester = auth.did.unwrap().clone(); 21 | let actor_store = ActorStore::new( 22 | requester.clone(), 23 | S3BlobStore::new(requester.clone(), s3_config), 24 | db, 25 | ); 26 | actor_store 27 | .pref 28 | .put_preferences(preferences, "app.bsky".to_string(), auth.scope.unwrap()) 29 | .await?; 30 | Ok(()) 31 | } 32 | 33 | #[tracing::instrument(skip_all)] 34 | #[rocket::post( 35 | "/xrpc/app.bsky.actor.putPreferences", 36 | format = "json", 37 | data = "" 38 | )] 39 | pub async fn put_preferences( 40 | body: Json, 41 | s3_config: &State, 42 | auth: AccessStandard, 43 | db: DbConn, 44 | ) -> Result<(), ApiError> { 45 | match inner_put_preferences(body, s3_config, auth, db).await { 46 | Ok(_) => Ok(()), 47 | Err(error) => Err(error), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/feed/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_actor_likes; 2 | pub mod get_author_feed; 3 | pub mod get_feed; 4 | pub mod get_post_thread; 5 | pub mod get_timeline; 6 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actor; 2 | pub mod feed; 3 | pub mod notification; 4 | pub mod util; 5 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/notification/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod register_push; 2 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/bsky/util/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::SharedIdResolver; 2 | use anyhow::{bail, Result}; 3 | use rocket::State; 4 | use rsky_identity::errors::Error; 5 | use rsky_identity::types::DidDocument; 6 | 7 | // provides http-friendly errors during did resolution 8 | pub async fn get_did_doc( 9 | id_resolver: &State, 10 | did: &String, 11 | ) -> Result { 12 | let mut lock = id_resolver.id_resolver.write().await; 13 | match lock.did.resolve(did.clone(), None).await { 14 | Err(err) => match err.downcast_ref() { 15 | Some(Error::PoorlyFormattedDidDocumentError(_)) => bail!("invalid did document: {did}"), 16 | _ => bail!("could not resolve did document: {did}"), 17 | }, 18 | Ok(Some(resolved)) => Ok(resolved), 19 | _ => bail!("could not resolve did document: {did}"), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bsky; 2 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/disable_account_invites.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::Moderator; 4 | use anyhow::Result; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::admin::DisableAccountInvitesInput; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::post( 10 | "/xrpc/com.atproto.admin.disableAccountInvites", 11 | format = "json", 12 | data = "" 13 | )] 14 | pub async fn disable_account_invites( 15 | body: Json, 16 | _auth: Moderator, 17 | account_manager: AccountManager, 18 | ) -> Result<(), ApiError> { 19 | let DisableAccountInvitesInput { account, .. } = body.into_inner(); 20 | match account_manager 21 | .set_account_invites_disabled(&account, true) 22 | .await 23 | { 24 | Ok(_) => Ok(()), 25 | Err(error) => { 26 | tracing::error!("@LOG: ERROR: {error}"); 27 | Err(ApiError::RuntimeError) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/disable_invite_codes.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::{AccountManager, DisableInviteCodesOpts}; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::Moderator; 4 | use anyhow::{bail, Result}; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::admin::DisableInviteCodesInput; 7 | 8 | async fn inner_disable_invite_codes( 9 | body: Json, 10 | account_manager: AccountManager, 11 | ) -> Result<()> { 12 | let DisableInviteCodesInput { codes, accounts } = body.into_inner(); 13 | let codes: Vec = codes.unwrap_or_else(Vec::new); 14 | let accounts: Vec = accounts.unwrap_or_else(Vec::new); 15 | 16 | if accounts.contains(&"admin".to_string()) { 17 | bail!("cannot disable admin invite codes") 18 | } 19 | 20 | account_manager 21 | .disable_invite_codes(DisableInviteCodesOpts { codes, accounts }) 22 | .await 23 | } 24 | 25 | #[tracing::instrument(skip_all)] 26 | #[rocket::post( 27 | "/xrpc/com.atproto.admin.disableInviteCodes", 28 | format = "json", 29 | data = "" 30 | )] 31 | pub async fn disable_invite_codes( 32 | body: Json, 33 | _auth: Moderator, 34 | account_manager: AccountManager, 35 | ) -> Result<(), ApiError> { 36 | match inner_disable_invite_codes(body, account_manager).await { 37 | Ok(_) => Ok(()), 38 | Err(error) => { 39 | tracing::error!("@LOG: ERROR: {error}"); 40 | Err(ApiError::RuntimeError) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/enable_account_invites.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::Moderator; 4 | use anyhow::Result; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::admin::EnableAccountInvitesInput; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::post( 10 | "/xrpc/com.atproto.admin.enableAccountInvites", 11 | format = "json", 12 | data = "" 13 | )] 14 | pub async fn enable_account_invites( 15 | body: Json, 16 | _auth: Moderator, 17 | account_manager: AccountManager, 18 | ) -> Result<(), ApiError> { 19 | let EnableAccountInvitesInput { account, .. } = body.into_inner(); 20 | match account_manager 21 | .set_account_invites_disabled(&account, false) 22 | .await 23 | { 24 | Ok(_) => Ok(()), 25 | Err(error) => { 26 | tracing::error!("@LOG: ERROR: {error}"); 27 | Err(ApiError::RuntimeError) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delete_account; 2 | pub mod disable_account_invites; 3 | pub mod disable_invite_codes; 4 | pub mod enable_account_invites; 5 | pub mod get_account_info; 6 | pub mod get_invite_codes; 7 | pub mod get_subject_status; 8 | pub mod send_email; 9 | pub mod update_account_email; 10 | pub mod update_account_handle; 11 | pub mod update_account_password; 12 | pub mod update_subject_status; 13 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/update_account_email.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::helpers::account::AvailabilityFlags; 2 | use crate::account_manager::{AccountManager, UpdateEmailOpts}; 3 | use crate::apis::ApiError; 4 | use crate::auth_verifier::AdminToken; 5 | use anyhow::{bail, Result}; 6 | use rocket::serde::json::Json; 7 | use rsky_lexicon::com::atproto::admin::UpdateAccountEmailInput; 8 | 9 | async fn inner_update_account_email( 10 | body: Json, 11 | account_manager: AccountManager, 12 | ) -> Result<()> { 13 | let account = account_manager 14 | .get_account( 15 | &body.account, 16 | Some(AvailabilityFlags { 17 | include_deactivated: Some(true), 18 | include_taken_down: Some(true), 19 | }), 20 | ) 21 | .await?; 22 | match account { 23 | None => bail!("Account does not exist: {}", body.account), 24 | Some(account) => { 25 | account_manager 26 | .update_email(UpdateEmailOpts { 27 | did: account.did, 28 | email: body.email.clone(), 29 | }) 30 | .await 31 | } 32 | } 33 | } 34 | 35 | #[tracing::instrument(skip_all)] 36 | #[rocket::post( 37 | "/xrpc/com.atproto.admin.updateAccountEmail", 38 | format = "json", 39 | data = "" 40 | )] 41 | pub async fn update_account_email( 42 | body: Json, 43 | _auth: AdminToken, 44 | account_manager: AccountManager, 45 | ) -> Result<(), ApiError> { 46 | match inner_update_account_email(body, account_manager).await { 47 | Ok(_) => Ok(()), 48 | Err(error) => { 49 | tracing::error!("@LOG: ERROR: {error}"); 50 | Err(ApiError::RuntimeError) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/admin/update_account_password.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::{AccountManager, UpdateAccountPasswordOpts}; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AdminToken; 4 | use anyhow::Result; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::admin::UpdateAccountPasswordInput; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::post( 10 | "/xrpc/com.atproto.admin.updateAccountPassword", 11 | format = "json", 12 | data = "" 13 | )] 14 | pub async fn update_account_password( 15 | body: Json, 16 | _auth: AdminToken, 17 | account_manager: AccountManager, 18 | ) -> Result<(), ApiError> { 19 | let UpdateAccountPasswordInput { did, password } = body.into_inner(); 20 | match account_manager 21 | .update_account_password(UpdateAccountPasswordOpts { did, password }) 22 | .await 23 | { 24 | Ok(_) => Ok(()), 25 | Err(error) => { 26 | tracing::error!("@LOG: ERROR: {error}"); 27 | Err(ApiError::RuntimeError) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/identity/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_recommended_did_credentials; 2 | pub mod request_plc_operation_signature; 3 | pub mod resolve_handle; 4 | pub mod sign_plc_operation; 5 | pub mod submit_plc_operation; 6 | pub mod update_handle; 7 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | pub mod identity; 3 | pub mod repo; 4 | pub mod server; 5 | pub mod sync; 6 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/repo/list_missing_blobs.rs: -------------------------------------------------------------------------------- 1 | use crate::actor_store::aws::s3::S3BlobStore; 2 | use crate::actor_store::blob::ListMissingBlobsOpts; 3 | use crate::actor_store::ActorStore; 4 | use crate::apis::ApiError; 5 | use crate::auth_verifier::AccessFull; 6 | use crate::db::DbConn; 7 | use anyhow::Result; 8 | use aws_config::SdkConfig; 9 | use rocket::serde::json::Json; 10 | use rocket::State; 11 | use rsky_lexicon::com::atproto::repo::ListMissingBlobsOutput; 12 | 13 | #[tracing::instrument(skip_all)] 14 | #[rocket::get("/xrpc/com.atproto.repo.listMissingBlobs?&")] 15 | pub async fn list_missing_blobs( 16 | limit: Option, 17 | cursor: Option, 18 | auth: AccessFull, 19 | db: DbConn, 20 | s3_config: &State, 21 | ) -> Result, ApiError> { 22 | let did = auth.access.credentials.unwrap().did.unwrap(); 23 | let limit: u16 = limit.unwrap_or(500); 24 | 25 | let actor_store = ActorStore::new(did.clone(), S3BlobStore::new(did.clone(), s3_config), db); 26 | 27 | match actor_store 28 | .blob 29 | .list_missing_blobs(ListMissingBlobsOpts { cursor, limit }) 30 | .await 31 | { 32 | Ok(blobs) => { 33 | let cursor = match blobs.last() { 34 | Some(last_blob) => Some(last_blob.cid.clone()), 35 | None => None, 36 | }; 37 | Ok(Json(ListMissingBlobsOutput { cursor, blobs })) 38 | } 39 | Err(error) => { 40 | tracing::error!("{error:?}"); 41 | Err(ApiError::RuntimeError) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/repo/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::helpers::account::{ActorAccount, AvailabilityFlags}; 2 | use crate::account_manager::AccountManager; 3 | use anyhow::{bail, Result}; 4 | 5 | pub async fn assert_repo_availability( 6 | did: &String, 7 | is_admin_of_self: bool, 8 | account_manager: &AccountManager, 9 | ) -> Result { 10 | let account = account_manager 11 | .get_account( 12 | did, 13 | Some(AvailabilityFlags { 14 | include_deactivated: Some(true), 15 | include_taken_down: Some(true), 16 | }), 17 | ) 18 | .await?; 19 | match account { 20 | None => bail!("RepoNotFound: Could not find repo for DID: {did}"), 21 | Some(account) => { 22 | if is_admin_of_self { 23 | return Ok(account); 24 | } 25 | if account.takedown_ref.is_some() { 26 | bail!("RepoTakendown: Repo has been takendown: {did}"); 27 | } 28 | if account.deactivated_at.is_some() { 29 | bail!("RepoDeactivated: Repo has been deactivated: {did}"); 30 | } 31 | Ok(account) 32 | } 33 | } 34 | } 35 | 36 | pub mod apply_writes; 37 | pub mod create_record; 38 | pub mod delete_record; 39 | pub mod describe_repo; 40 | pub mod get_record; 41 | pub mod import_repo; 42 | pub mod list_missing_blobs; 43 | pub mod list_records; 44 | pub mod put_record; 45 | pub mod upload_blob; 46 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/create_app_password.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AccessFull; 4 | use rocket::serde::json::Json; 5 | use rsky_lexicon::com::atproto::server::{CreateAppPasswordInput, CreateAppPasswordOutput}; 6 | 7 | #[tracing::instrument(skip_all)] 8 | #[rocket::post( 9 | "/xrpc/com.atproto.server.createAppPassword", 10 | format = "json", 11 | data = "" 12 | )] 13 | pub async fn create_app_password( 14 | body: Json, 15 | auth: AccessFull, 16 | account_manager: AccountManager, 17 | ) -> Result, ApiError> { 18 | let CreateAppPasswordInput { name } = body.into_inner(); 19 | match account_manager 20 | .create_app_password(auth.access.credentials.unwrap().did.unwrap(), name) 21 | .await 22 | { 23 | Ok(app_password) => Ok(Json(app_password)), 24 | Err(error) => { 25 | tracing::error!("Internal Error: {error}"); 26 | Err(ApiError::RuntimeError) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/create_invite_code.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AdminToken; 4 | use account_manager::AccountManager; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::server::{ 7 | AccountCodes, CreateInviteCodeInput, CreateInviteCodeOutput, 8 | }; 9 | 10 | #[tracing::instrument(skip_all)] 11 | #[rocket::post( 12 | "/xrpc/com.atproto.server.createInviteCode", 13 | format = "json", 14 | data = "" 15 | )] 16 | pub async fn create_invite_code( 17 | body: Json, 18 | _auth: AdminToken, 19 | account_manager: AccountManager, 20 | ) -> Result, ApiError> { 21 | // @TODO: verify admin auth token 22 | let CreateInviteCodeInput { 23 | use_count, 24 | for_account, 25 | } = body.into_inner(); 26 | let code = super::gen_invite_code(); 27 | 28 | match account_manager 29 | .create_invite_codes( 30 | vec![AccountCodes { 31 | codes: vec![code.clone()], 32 | account: for_account.unwrap_or("admin".to_owned()), 33 | }], 34 | use_count, 35 | ) 36 | .await 37 | { 38 | Ok(_) => Ok(Json(CreateInviteCodeOutput { code })), 39 | Err(error) => { 40 | tracing::error!("Internal Error: {error}"); 41 | Err(ApiError::RuntimeError) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/create_invite_codes.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AdminToken; 4 | use rocket::serde::json::Json; 5 | use rsky_lexicon::com::atproto::server::{ 6 | AccountCodes, CreateInviteCodesInput, CreateInviteCodesOutput, 7 | }; 8 | 9 | #[tracing::instrument(skip_all)] 10 | #[rocket::post( 11 | "/xrpc/com.atproto.server.createInviteCodes", 12 | format = "json", 13 | data = "" 14 | )] 15 | pub async fn create_invite_codes( 16 | body: Json, 17 | _auth: AdminToken, 18 | account_manager: AccountManager, 19 | ) -> Result, ApiError> { 20 | // @TODO: verify admin auth token 21 | let CreateInviteCodesInput { 22 | use_count, 23 | code_count, 24 | for_accounts, 25 | } = body.into_inner(); 26 | let for_accounts = for_accounts.unwrap_or_else(|| vec!["admin".to_owned()]); 27 | 28 | let mut account_codes: Vec = Vec::new(); 29 | for account in for_accounts { 30 | let codes = super::gen_invite_codes(code_count); 31 | account_codes.push(AccountCodes { account, codes }); 32 | } 33 | 34 | match account_manager 35 | .create_invite_codes(account_codes.clone(), use_count) 36 | .await 37 | { 38 | Ok(_) => Ok(Json(CreateInviteCodesOutput { 39 | codes: account_codes, 40 | })), 41 | Err(error) => { 42 | tracing::error!("Internal Error: {error}"); 43 | Err(ApiError::RuntimeError) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/deactivate_account.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AccessFull; 4 | use anyhow::Result; 5 | use rocket::serde::json::Json; 6 | use rsky_lexicon::com::atproto::server::DeactivateAccountInput; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::post( 10 | "/xrpc/com.atproto.server.deactivateAccount", 11 | format = "json", 12 | data = "" 13 | )] 14 | pub async fn deactivate_account( 15 | body: Json, 16 | auth: AccessFull, 17 | account_manager: AccountManager, 18 | ) -> Result<(), ApiError> { 19 | let did = auth.access.credentials.unwrap().did.unwrap(); 20 | let DeactivateAccountInput { delete_after } = body.into_inner(); 21 | match account_manager.deactivate_account(&did, delete_after).await { 22 | Ok(()) => Ok(()), 23 | Err(error) => { 24 | tracing::error!("Internal Error: {error}"); 25 | Err(ApiError::RuntimeError) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/delete_session.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::RevokeRefreshToken; 4 | 5 | #[tracing::instrument(skip_all)] 6 | #[rocket::post("/xrpc/com.atproto.server.deleteSession")] 7 | pub async fn delete_session( 8 | auth: RevokeRefreshToken, 9 | account_manager: AccountManager, 10 | ) -> Result<(), ApiError> { 11 | match account_manager.revoke_refresh_token(auth.id).await { 12 | Ok(_) => Ok(()), 13 | Err(error) => { 14 | tracing::error!("@LOG: ERROR: {error}"); 15 | Err(ApiError::RuntimeError) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/describe_server.rs: -------------------------------------------------------------------------------- 1 | use crate::apis::ApiError; 2 | use rocket::serde::json::Json; 3 | use rsky_common::env::{env_bool, env_list, env_str}; 4 | use rsky_lexicon::com::atproto::server::{ 5 | DescribeServerOutput, DescribeServerRefContact, DescribeServerRefLinks, 6 | }; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::get("/xrpc/com.atproto.server.describeServer")] 10 | pub async fn describe_server() -> Result, ApiError> { 11 | let available_user_domains = env_list("PDS_SERVICE_HANDLE_DOMAINS"); 12 | let invite_code_required = env_bool("PDS_INVITE_REQUIRED"); 13 | let privacy_policy = env_str("PDS_PRIVACY_POLICY_URL"); 14 | let terms_of_service = env_str("PDS_TERMS_OF_SERVICE_URL"); 15 | let contact_email_address = env_str("PDS_CONTACT_EMAIL_ADDRESS"); 16 | 17 | Ok(Json(DescribeServerOutput { 18 | did: env_str("PDS_SERVICE_DID").unwrap(), 19 | available_user_domains, 20 | invite_code_required, 21 | phone_verification_required: None, 22 | links: DescribeServerRefLinks { 23 | privacy_policy, 24 | terms_of_service, 25 | }, 26 | contact: DescribeServerRefContact { 27 | email: contact_email_address, 28 | }, 29 | })) 30 | } 31 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/get_session.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AccessStandard; 4 | use rocket::serde::json::Json; 5 | use rsky_lexicon::com::atproto::server::GetSessionOutput; 6 | use rsky_syntax::handle::INVALID_HANDLE; 7 | 8 | #[tracing::instrument(skip_all)] 9 | #[rocket::get("/xrpc/com.atproto.server.getSession")] 10 | pub async fn get_session( 11 | auth: AccessStandard, 12 | account_manager: AccountManager, 13 | ) -> Result, ApiError> { 14 | let did = auth.access.credentials.unwrap().did.unwrap(); 15 | match account_manager.get_account(&did, None).await { 16 | Ok(Some(user)) => Ok(Json(GetSessionOutput { 17 | handle: user.handle.unwrap_or(INVALID_HANDLE.to_string()), 18 | did: user.did, 19 | email: user.email, 20 | did_doc: None, 21 | email_confirmed: Some(user.email_confirmed_at.is_some()), 22 | })), 23 | _ => Err(ApiError::AccountNotFound), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/list_app_passwords.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AccessFull; 4 | use rocket::serde::json::Json; 5 | use rsky_lexicon::com::atproto::server::{AppPassword, ListAppPasswordsOutput}; 6 | 7 | #[tracing::instrument(skip_all)] 8 | #[rocket::get("/xrpc/com.atproto.server.listAppPasswords")] 9 | pub async fn list_app_passwords( 10 | auth: AccessFull, 11 | account_manager: AccountManager, 12 | ) -> Result, ApiError> { 13 | let did = auth.access.credentials.unwrap().did.unwrap(); 14 | match account_manager.list_app_passwords(&did).await { 15 | Ok(passwords) => { 16 | let passwords: Vec = passwords 17 | .into_iter() 18 | .map(|password| AppPassword { 19 | name: password.0, 20 | created_at: password.1, 21 | }) 22 | .collect(); 23 | Ok(Json(ListAppPasswordsOutput { passwords })) 24 | } 25 | Err(error) => { 26 | tracing::error!("Internal Error: {error}"); 27 | return Err(ApiError::RuntimeError); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/request_account_delete.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::helpers::account::AvailabilityFlags; 2 | use crate::account_manager::AccountManager; 3 | use crate::apis::ApiError; 4 | use crate::auth_verifier::AccessStandardIncludeChecks; 5 | use crate::mailer; 6 | use crate::mailer::TokenParam; 7 | use crate::models::models::EmailTokenPurpose; 8 | use anyhow::{bail, Result}; 9 | 10 | async fn inner_request_account_delete( 11 | auth: AccessStandardIncludeChecks, 12 | account_manager: AccountManager, 13 | ) -> Result<()> { 14 | let did = auth.access.credentials.unwrap().did.unwrap(); 15 | let account = account_manager 16 | .get_account( 17 | &did, 18 | Some(AvailabilityFlags { 19 | include_deactivated: Some(true), 20 | include_taken_down: Some(true), 21 | }), 22 | ) 23 | .await?; 24 | if let Some(account) = account { 25 | if let Some(email) = account.email { 26 | let token = account_manager 27 | .create_email_token(&did, EmailTokenPurpose::DeleteAccount) 28 | .await?; 29 | mailer::send_account_delete(email, TokenParam { token }).await?; 30 | Ok(()) 31 | } else { 32 | bail!("Account does not have an email address") 33 | } 34 | } else { 35 | bail!("Account not found") 36 | } 37 | } 38 | 39 | #[tracing::instrument(skip_all)] 40 | #[rocket::post("/xrpc/com.atproto.server.requestAccountDelete")] 41 | pub async fn request_account_delete( 42 | auth: AccessStandardIncludeChecks, 43 | account_manager: AccountManager, 44 | ) -> Result<(), ApiError> { 45 | match inner_request_account_delete(auth, account_manager).await { 46 | Ok(_) => Ok(()), 47 | Err(error) => { 48 | tracing::error!("@LOG: ERROR: {error}"); 49 | Err(ApiError::RuntimeError) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/request_email_confirmation.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::helpers::account::AvailabilityFlags; 2 | use crate::account_manager::AccountManager; 3 | use crate::apis::ApiError; 4 | use crate::auth_verifier::AccessStandardIncludeChecks; 5 | use crate::mailer; 6 | use crate::mailer::TokenParam; 7 | use crate::models::models::EmailTokenPurpose; 8 | use anyhow::{bail, Result}; 9 | 10 | async fn inner_request_email_confirmation( 11 | auth: AccessStandardIncludeChecks, 12 | account_manager: AccountManager, 13 | ) -> Result<()> { 14 | let did = auth.access.credentials.unwrap().did.unwrap(); 15 | let account = account_manager 16 | .get_account( 17 | &did, 18 | Some(AvailabilityFlags { 19 | include_deactivated: Some(true), 20 | include_taken_down: Some(true), 21 | }), 22 | ) 23 | .await?; 24 | if let Some(account) = account { 25 | if let Some(email) = account.email { 26 | let token = account_manager 27 | .create_email_token(&did, EmailTokenPurpose::ConfirmEmail) 28 | .await?; 29 | mailer::send_confirm_email(email, TokenParam { token }).await?; 30 | Ok(()) 31 | } else { 32 | bail!("Account does not have an email address") 33 | } 34 | } else { 35 | bail!("Account not found") 36 | } 37 | } 38 | 39 | #[tracing::instrument(skip_all)] 40 | #[rocket::post("/xrpc/com.atproto.server.requestEmailConfirmation")] 41 | pub async fn request_email_confirmation( 42 | auth: AccessStandardIncludeChecks, 43 | account_manager: AccountManager, 44 | ) -> Result<(), ApiError> { 45 | match inner_request_email_confirmation(auth, account_manager).await { 46 | Ok(_) => Ok(()), 47 | Err(error) => { 48 | tracing::error!("@LOG: ERROR: {error}"); 49 | Err(ApiError::RuntimeError) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/reserve_signing_key.rs: -------------------------------------------------------------------------------- 1 | #[rocket::post("/xrpc/com.atproto.server.reserveSigningKey")] 2 | pub async fn reserve_signing_key() { 3 | unimplemented!(); 4 | } 5 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/reset_password.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::{AccountManager, ResetPasswordOpts}; 2 | use crate::apis::ApiError; 3 | use rocket::serde::json::Json; 4 | use rsky_lexicon::com::atproto::server::ResetPasswordInput; 5 | 6 | #[tracing::instrument(skip_all)] 7 | #[rocket::post( 8 | "/xrpc/com.atproto.server.resetPassword", 9 | format = "json", 10 | data = "" 11 | )] 12 | pub async fn reset_password( 13 | body: Json, 14 | account_manager: AccountManager, 15 | ) -> Result<(), ApiError> { 16 | let ResetPasswordInput { token, password } = body.into_inner(); 17 | match account_manager 18 | .reset_password(ResetPasswordOpts { token, password }) 19 | .await 20 | { 21 | Ok(_) => Ok(()), 22 | Err(error) => { 23 | tracing::error!("@LOG: ERROR: {error}"); 24 | Err(ApiError::RuntimeError) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/server/revoke_app_password.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::AccountManager; 2 | use crate::apis::ApiError; 3 | use crate::auth_verifier::AccessFull; 4 | use rocket::serde::json::Json; 5 | use rsky_lexicon::com::atproto::server::RevokeAppPasswordInput; 6 | 7 | #[tracing::instrument(skip_all)] 8 | #[rocket::post( 9 | "/xrpc/com.atproto.server.revokeAppPassword", 10 | format = "json", 11 | data = "" 12 | )] 13 | pub async fn revoke_app_password( 14 | body: Json, 15 | auth: AccessFull, 16 | account_manager: AccountManager, 17 | ) -> Result<(), ApiError> { 18 | let RevokeAppPasswordInput { name } = body.into_inner(); 19 | let requester = auth.access.credentials.unwrap().did.unwrap(); 20 | 21 | match account_manager.revoke_app_password(requester, name).await { 22 | Ok(_) => Ok(()), 23 | Err(error) => { 24 | tracing::error!("@LOG: ERROR: {error}"); 25 | Err(ApiError::RuntimeError) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/atproto/sync/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_blob; 2 | pub mod get_blocks; 3 | pub mod get_latest_commit; 4 | pub mod get_record; 5 | pub mod get_repo; 6 | pub mod get_repo_status; 7 | pub mod list_blobs; 8 | pub mod list_repos; 9 | pub mod subscribe_repos; 10 | -------------------------------------------------------------------------------- /rsky-pds/src/apis/com/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod atproto; 2 | -------------------------------------------------------------------------------- /rsky-pds/src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::account_manager::helpers::auth::ServiceJwtParams; 2 | use crate::xrpc_server::auth::create_service_auth_headers; 3 | use anyhow::Result; 4 | use reqwest::header::HeaderMap; 5 | use secp256k1::SecretKey; 6 | use std::env; 7 | 8 | pub async fn service_auth_headers(did: &str, aud: &str, lxm: &str) -> Result { 9 | let private_key = env::var("PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX")?; 10 | let keypair = SecretKey::from_slice(&hex::decode(private_key.as_bytes())?)?; 11 | create_service_auth_headers(ServiceJwtParams { 12 | iss: did.to_owned(), 13 | aud: aud.to_owned(), 14 | exp: None, 15 | lxm: Some(lxm.to_owned()), 16 | jti: None, 17 | keypair, 18 | }) 19 | .await 20 | } 21 | -------------------------------------------------------------------------------- /rsky-pds/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use diesel::pg::PgConnection; 3 | use diesel::prelude::*; 4 | use dotenvy::dotenv; 5 | use rocket_sync_db_pools::database; 6 | use std::env; 7 | use std::fmt::{Debug, Formatter}; 8 | 9 | #[database("pg_db")] 10 | pub struct DbConn(PgConnection); 11 | 12 | impl Debug for DbConn { 13 | fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { 14 | todo!() 15 | } 16 | } 17 | 18 | #[tracing::instrument(skip_all)] 19 | pub fn establish_connection_for_sequencer() -> Result { 20 | dotenv().ok(); 21 | tracing::debug!("Establishing database connection for Sequencer"); 22 | let database_url = env::var("DATABASE_URL").unwrap_or("".into()); 23 | let result = PgConnection::establish(&database_url).map_err(|error| { 24 | let context = format!("Error connecting to {database_url:?}"); 25 | anyhow::Error::new(error).context(context) 26 | })?; 27 | 28 | Ok(result) 29 | } 30 | -------------------------------------------------------------------------------- /rsky-pds/src/handle/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum ErrorKind { 5 | #[error("Invalid handle")] 6 | InvalidHandle, 7 | #[error("Handle not available")] 8 | HandleNotAvailable, 9 | #[error("Unsupported domain")] 10 | UnsupportedDomain, 11 | #[error("Internal error")] 12 | InternalError, 13 | } 14 | 15 | #[derive(Error, Debug)] 16 | #[error("{kind}: {message}")] 17 | pub struct Error { 18 | pub kind: ErrorKind, 19 | pub message: String, 20 | } 21 | 22 | impl Error { 23 | pub fn new(kind: ErrorKind, message: &str) -> Self { 24 | Self { 25 | kind, 26 | message: message.to_string(), 27 | } 28 | } 29 | } 30 | 31 | pub type Result = std::result::Result; 32 | -------------------------------------------------------------------------------- /rsky-pds/src/image/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use image::ImageReader; 3 | use image::{guess_format, GenericImageView}; 4 | use std::io::Cursor; 5 | 6 | pub struct ImageInfo { 7 | pub height: u32, 8 | pub width: u32, 9 | pub size: Option, 10 | pub mime: String, 11 | } 12 | 13 | pub async fn mime_type_from_bytes(bytes: Vec) -> Result> { 14 | match infer::get(bytes.as_slice()) { 15 | Some(kind) => Ok(Some(kind.mime_type().to_string())), 16 | None => Ok(None), 17 | } 18 | } 19 | 20 | pub async fn maybe_get_info(bytes: Vec) -> Result> { 21 | let process_image = || -> Result> { 22 | let img = ImageReader::new(Cursor::new(bytes.clone())) 23 | .with_guessed_format()? 24 | .decode()?; 25 | let (width, height) = img.dimensions(); 26 | let mime = guess_format(bytes.as_slice())?.to_mime_type().to_string(); 27 | let size: Option = None; 28 | Ok(Some(ImageInfo { 29 | height, 30 | width, 31 | size, 32 | mime, 33 | })) 34 | }; 35 | 36 | match process_image() { 37 | Ok(info) => Ok(info), 38 | Err(_) => Ok(None), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rsky-pds/src/lexicon/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::lexicon::lexicons::Root; 2 | use lazy_static::lazy_static; 3 | use std::fs; 4 | 5 | lazy_static! { 6 | pub static ref LEXICONS: Root = { 7 | let toml_str = fs::read_to_string("lexicons.toml").expect("Failed to open lexicon file"); 8 | let cargo_toml: Root = 9 | toml::from_str(&toml_str).expect("Failed to deserialize lexicons.toml"); 10 | cargo_toml 11 | }; 12 | } 13 | 14 | pub mod lexicons; 15 | -------------------------------------------------------------------------------- /rsky-pds/src/mailer/moderation.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use mailgun_rs::{EmailAddress, Mailgun, MailgunRegion, Message}; 3 | use std::env; 4 | 5 | pub struct HtmlMailOpts { 6 | pub to: String, 7 | pub subject: String, 8 | pub html: String, 9 | } 10 | 11 | pub struct ModerationMailer {} 12 | 13 | impl ModerationMailer { 14 | pub async fn send_html(opts: HtmlMailOpts) -> Result<()> { 15 | let HtmlMailOpts { to, subject, html } = opts; 16 | 17 | let recipient = EmailAddress::address(&to); 18 | let message = Message { 19 | to: vec![recipient], 20 | subject, 21 | html, 22 | ..Default::default() 23 | }; 24 | 25 | let client = Mailgun { 26 | api_key: env::var("PDS_MAILGUN_API_KEY").unwrap(), 27 | domain: env::var("PDS_MAILGUN_DOMAIN").unwrap(), 28 | message, 29 | }; 30 | let sender = EmailAddress::name_address( 31 | &env::var("PDS_MODERATION_EMAIL_FROM_NAME").unwrap(), 32 | &env::var("PDS_MODERATION_EMAIL_FROM_ADDRESS").unwrap(), 33 | ); 34 | 35 | client.async_send(MailgunRegion::US, &sender).await?; 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rsky-pds/src/main.rs: -------------------------------------------------------------------------------- 1 | use rsky_pds::build_rocket; 2 | 3 | #[rocket::main] 4 | async fn main() { 5 | let subscriber = tracing_subscriber::FmtSubscriber::new(); 6 | tracing::subscriber::set_global_default(subscriber).unwrap(); 7 | let _ = build_rocket(None).await.launch().await; 8 | } 9 | -------------------------------------------------------------------------------- /rsky-pds/src/models/error_message_response.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct ErrorMessageResponse { 3 | #[serde(rename = "code", skip_serializing_if = "Option::is_none")] 4 | pub code: Option, 5 | #[serde(rename = "message", skip_serializing_if = "Option::is_none")] 6 | pub message: Option, 7 | } 8 | 9 | impl ErrorMessageResponse { 10 | pub fn new() -> ErrorMessageResponse { 11 | ErrorMessageResponse { 12 | code: None, 13 | message: None, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rsky-pds/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub use self::models::Account; 3 | pub use self::models::AccountPref; 4 | pub use self::models::Actor; 5 | pub use self::models::AppPassword; 6 | pub use self::models::Backlink; 7 | pub use self::models::Blob; 8 | pub use self::models::DidDoc; 9 | pub use self::models::EmailToken; 10 | pub use self::models::InviteCode; 11 | pub use self::models::InviteCodeUse; 12 | pub use self::models::Record; 13 | pub use self::models::RecordBlob; 14 | pub use self::models::RefreshToken; 15 | pub use self::models::RepoBlock; 16 | pub use self::models::RepoRoot; 17 | pub use self::models::RepoSeq; 18 | pub mod error_code; 19 | pub use self::error_code::ErrorCode; 20 | pub mod error_message_response; 21 | pub use self::error_message_response::ErrorMessageResponse; 22 | pub mod server_version; 23 | pub use self::server_version::ServerVersion; 24 | -------------------------------------------------------------------------------- /rsky-pds/src/models/server_version.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 2 | pub struct ServerVersion { 3 | pub version: String, 4 | } 5 | -------------------------------------------------------------------------------- /rsky-pds/src/read_after_write/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | pub mod util; 3 | pub mod viewer; 4 | -------------------------------------------------------------------------------- /rsky-pds/src/read_after_write/types.rs: -------------------------------------------------------------------------------- 1 | use lexicon_cid::Cid; 2 | use rsky_lexicon::app::bsky::actor::Profile; 3 | use rsky_lexicon::app::bsky::feed::Post; 4 | use rsky_syntax::aturi::AtUri; 5 | 6 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 7 | pub struct LocalRecords { 8 | pub count: i64, 9 | pub profile: Option>, 10 | pub posts: Vec>, 11 | } 12 | 13 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 14 | pub struct RecordDescript { 15 | pub uri: AtUri, 16 | pub cid: Cid, 17 | #[serde(rename = "indexedAt")] 18 | pub indexed_at: String, 19 | pub record: T, 20 | } 21 | -------------------------------------------------------------------------------- /rsky-pds/src/repo/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod prepare; 2 | -------------------------------------------------------------------------------- /rsky-pds/src/xrpc_server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod stream; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /rsky-pds/src/xrpc_server/stream/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod frames; 2 | pub mod types; 3 | -------------------------------------------------------------------------------- /rsky-pds/src/xrpc_server/stream/types.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Serialize_repr, Deserialize_repr)] 4 | #[repr(i8)] 5 | pub enum FrameType { 6 | Message = 1, 7 | Error = -1, 8 | } 9 | 10 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 11 | pub struct MessageFrameHeader { 12 | pub op: FrameType, // Frame op 13 | pub t: Option, // Message body type discriminator 14 | } 15 | 16 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 17 | pub struct ErrorFrameHeader { 18 | pub op: FrameType, // Frame op 19 | // `t` Should not be included in header if op is -1. 20 | } 21 | 22 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 23 | pub struct ErrorFrameBody { 24 | pub error: String, // Error code 25 | pub message: Option, // Error message 26 | } 27 | 28 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 29 | pub enum FrameHeader { 30 | MessageFrameHeader(MessageFrameHeader), 31 | ErrorFrameHeader(ErrorFrameHeader), 32 | } 33 | 34 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 35 | pub enum CloseCode { 36 | Normal = 1000, 37 | Abnormal = 1006, 38 | Policy = 1008, 39 | } 40 | -------------------------------------------------------------------------------- /rsky-pds/src/xrpc_server/types.rs: -------------------------------------------------------------------------------- 1 | use crate::auth_verifier::AuthError; 2 | use reqwest::header::HeaderMap; 3 | use std::collections::BTreeMap; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Serialize)] 7 | pub struct HandlerPipeThrough { 8 | pub encoding: String, 9 | #[serde(with = "serde_bytes")] 10 | pub buffer: Vec, 11 | pub headers: Option>, 12 | } 13 | 14 | #[derive(Debug, Clone, PartialEq, Serialize)] 15 | pub struct HandlerPipeThroughProcedure { 16 | pub encoding: String, 17 | #[serde(with = "serde_bytes")] 18 | pub buffer: Vec, 19 | pub headers: Option>, 20 | pub body: Option, 21 | } 22 | 23 | #[derive(Error, Debug)] 24 | pub enum XRPCError { 25 | #[error("pipethrough network error")] 26 | UpstreamFailure, 27 | #[error("failed request {status:?}")] 28 | FailedResponse { 29 | status: String, 30 | error: Option, 31 | message: Option, 32 | headers: HeaderMap, 33 | }, 34 | } 35 | 36 | #[derive(Error, Debug)] 37 | pub enum InvalidRequestError { 38 | #[error("could not resolve proxy did service url")] 39 | CannotResolveServiceUrl, 40 | #[error("could not resolve proxy did")] 41 | CannotResolveProxyDid, 42 | #[error("Invalid service url: `{0}")] 43 | InvalidServiceUrl(String), 44 | #[error("Method not found")] 45 | MethodNotFound, 46 | #[error("no service id specified")] 47 | NoServiceId, 48 | #[error("No service configured for `{0}`")] 49 | NoServiceConfigured(String), 50 | #[error("AuthError: `{0}`")] 51 | AuthError(AuthError), 52 | #[error("XRPCError: {0}")] 53 | XRPCError(XRPCError), 54 | } 55 | -------------------------------------------------------------------------------- /rsky-pdsadmin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | 4 | [package] 5 | name = "rsky-pdsadmin" 6 | version = "0.1.0" 7 | edition = "2024" 8 | description = "Administrative CLI tool for rsky-pds" 9 | authors = ["RSKY Team"] 10 | license = "MIT" 11 | 12 | [lib] 13 | name = "rsky_pdsadmin" 14 | path = "src/lib.rs" 15 | 16 | [[bin]] 17 | name = "pdsadmin" 18 | path = "src/main.rs" 19 | 20 | [dependencies] 21 | anyhow = "1.0" 22 | base64 = "0.21" 23 | chrono = { version = "0.4", features = ["serde"] } 24 | clap = { version = "4.5.38", features = ["derive", "env"] } 25 | dialoguer = "0.11" 26 | diesel = { version = "2.2.10", features = ["postgres"] } 27 | diesel_cli = { version = "2.2.10", features = ["postgres"], optional = true } 28 | diesel_migrations = { version = "2.1.0", features = ["postgres"] } 29 | dirs = "5.0" 30 | dotenv = "0.15" 31 | indicatif = "0.17" 32 | rand = "0.8" 33 | reqwest = { version = "0.11", features = ["json", "blocking"] } 34 | serde = { version = "1.0", features = ["derive"] } 35 | serde_json = "1.0" 36 | shellexpand = "3.1" 37 | thiserror = "1.0" 38 | tokio = { version = "1.36", features = ["full"] } 39 | uuid = { version = "1.6", features = ["v4", "serde"] } 40 | which = "5.0" 41 | 42 | [dev-dependencies] 43 | mockito = "1.2" 44 | tempfile = "3.9" 45 | 46 | [features] 47 | default = [] 48 | db_cli = ["diesel_cli"] -------------------------------------------------------------------------------- /rsky-pdsadmin/pds.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres 2 | AWS_ENDPOINT="yoho.digitaloceanspaces.com" 3 | PDS_ADMIN_PASSWORD=change-me 4 | PDS_HOST=0.0.0.0 5 | PDS_HOSTNAME=127.0.0.1:8000 6 | PDS_PORT=8000 7 | PORT=8000 8 | PDS_SERVICE_HANDLE_DOMAINS=test_dot_com 9 | PDS_SERVICE_DID=did:web:rsky-pds 10 | AWS_EC2_METADATA_DISABLED=true 11 | LOG_LEVEL=debug 12 | RUST_BACKTRACE=1 13 | PDS_PROTOCOL=http -------------------------------------------------------------------------------- /rsky-pdsadmin/src/commands/create_invite_code/tests/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::commands::create_invite_code::execute; 4 | use mockito::{mock, server_url}; 5 | use std::env; 6 | 7 | #[test] 8 | fn test_create_invite_code() { 9 | // Save original env vars 10 | let orig_hostname = env::var("PDS_HOSTNAME").ok(); 11 | let orig_password = env::var("PDS_ADMIN_PASSWORD").ok(); 12 | 13 | // Set test env vars 14 | env::set_var( 15 | "PDS_HOSTNAME", 16 | server_url() 17 | .strip_prefix("http://") 18 | .unwrap_or("localhost:1234"), 19 | ); 20 | env::set_var("PDS_ADMIN_PASSWORD", "test-password"); 21 | 22 | // Mock the API response 23 | let invite_mock = mock("POST", "/xrpc/com.atproto.server.createInviteCode") 24 | .with_status(200) 25 | .with_header("content-type", "application/json") 26 | .with_body(r#"{"code":"test-invite-code"}"#) 27 | .create(); 28 | 29 | // Execute the command 30 | let result = execute(); 31 | 32 | // Verify mock was called 33 | invite_mock.assert(); 34 | 35 | // Check that the command executed successfully 36 | assert!(result.is_ok()); 37 | 38 | // Restore original env vars 39 | match orig_hostname { 40 | Some(val) => env::set_var("PDS_HOSTNAME", val), 41 | None => env::remove_var("PDS_HOSTNAME"), 42 | } 43 | 44 | match orig_password { 45 | Some(val) => env::set_var("PDS_ADMIN_PASSWORD", val), 46 | None => env::remove_var("PDS_ADMIN_PASSWORD"), 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /rsky-pdsadmin/src/commands/rsky_pds/tests/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::commands::rsky_pds::{RskyPdsCommands, execute}; 4 | use std::env; 5 | 6 | // Note: These tests are more difficult to implement without mocking the database 7 | // connection or diesel migrations. In a real project, we'd use dependency injection 8 | // or mock the database interactions. 9 | 10 | #[test] 11 | fn test_init_db_command_structure() { 12 | // This test just verifies that the command can be constructed correctly 13 | let command = RskyPdsCommands::InitDb; 14 | assert!(matches!(command, RskyPdsCommands::InitDb)); 15 | } 16 | 17 | // A real test would mock the database connection and verify that 18 | // migrations are run correctly. Since that would require substantial 19 | // changes to the code structure to enable testing, we'll just note 20 | // that those tests would be important in a production codebase. 21 | } -------------------------------------------------------------------------------- /rsky-pdsadmin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod util; 3 | 4 | use anyhow::Result; 5 | 6 | /// The main entry point for the library functionality 7 | /// This is primarily used for library consumers 8 | pub fn run() -> Result<()> { 9 | commands::execute() 10 | } 11 | -------------------------------------------------------------------------------- /rsky-pdsadmin/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod env; 2 | pub mod http_client; 3 | -------------------------------------------------------------------------------- /rsky-pdsadmin/tests/file_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod file_tests { 3 | use std::fs; 4 | use tempfile::TempDir; 5 | 6 | #[test] 7 | fn test_file_operations() { 8 | // Create a temporary directory for testing 9 | let temp_dir = TempDir::new().unwrap(); 10 | let temp_path = temp_dir.path(); 11 | 12 | // Create two test files 13 | let file1_path = temp_path.join("file1.txt"); 14 | let file2_path = temp_path.join("file2.txt"); 15 | 16 | // Write content to the files 17 | fs::write(&file1_path, "Test content 1").unwrap(); 18 | fs::write(&file2_path, "Test content 2").unwrap(); 19 | 20 | // Read back and verify 21 | let content1 = fs::read_to_string(&file1_path).unwrap(); 22 | let content2 = fs::read_to_string(&file2_path).unwrap(); 23 | 24 | assert_eq!(content1, "Test content 1"); 25 | assert_eq!(content2, "Test content 2"); 26 | assert_ne!(content1, content2); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rsky-pdsadmin/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | 4 | #[test] 5 | fn test_env_loading() { 6 | // Test that environment loading functionality works correctly 7 | assert!(true); 8 | } 9 | 10 | #[test] 11 | fn test_basic_functionality() { 12 | // This is a basic test that doesn't rely on external dependencies 13 | assert!(true); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rsky-relay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-relay" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # external 8 | bytes = "1" 9 | chrono = { version = "0.4", features = ["serde"] } 10 | ciborium = "0.2" 11 | cid = { version = "0.10", features = ["serde-codec"] } 12 | clap = { version = "4", features = ["derive", "env"] } 13 | color-eyre = "0.6" 14 | exponential-backoff = "2" 15 | file-rotate = "0.8" 16 | fjall = "2" 17 | futures = { version = "0.3", default-features = false, features = ["std"] } 18 | hashbrown = "0.15" 19 | http = "1" 20 | httparse = "1" 21 | ipld-core = "0.4" 22 | k256 = "0.13" 23 | libc = "0.2" 24 | lru = "0.14" 25 | magnetic = "2" 26 | mimalloc = "0.1" 27 | mio = { version = "1", features = ["os-ext", "os-poll"] } 28 | multibase = "0.9" 29 | p256 = "0.13" 30 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "gzip", "hickory-dns", "http2", "json", "rustls-tls-webpki-roots-no-provider"] } 31 | rs-car-sync = "0.4" 32 | rtrb = "0.3" 33 | rusqlite = { version = "0.36", features = ["bundled", "chrono"] } 34 | rustls = "0.23" 35 | rustls-pemfile = "2" 36 | serde = { version = "1", features = ["derive"] } 37 | serde_bytes = "0.11" 38 | serde_ipld_dagcbor = "0.6" 39 | serde_json = { version = "1", features = ["raw_value"] } 40 | sha2 = "0.10" 41 | signal-hook = { version = "0.3", features = ["extended-siginfo"] } 42 | socket2 = "0.5" 43 | thingbuf = "0.1" 44 | thiserror = "2" 45 | tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } 46 | tracing = { version = "0.1", features = ["release_max_level_debug"] } 47 | tracing-appender = "0.2" 48 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 49 | tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots", "url"] } 50 | url = "2" 51 | urlencoding = "2" 52 | 53 | # internal 54 | rsky-common = { workspace = true } 55 | rsky-identity = { workspace = true } 56 | 57 | [package.metadata.cargo-machete] 58 | ignored = ["serde_bytes"] 59 | -------------------------------------------------------------------------------- /rsky-relay/rustfmt.toml: -------------------------------------------------------------------------------- 1 | condense_wildcard_suffixes = true 2 | edition = "2024" 3 | fn_params_layout = "Compressed" 4 | format_code_in_doc_comments = true 5 | format_macro_matchers = true 6 | hex_literal_case = "Lower" 7 | imports_granularity = "Module" 8 | normalize_comments = true 9 | normalize_doc_attributes = true 10 | reorder_impl_items = true 11 | style_edition = "2024" 12 | unstable_features = true 13 | use_field_init_shorthand = true 14 | use_small_heuristics = "Max" 15 | use_try_shorthand = true 16 | -------------------------------------------------------------------------------- /rsky-relay/src/crawler/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod connection; 3 | mod manager; 4 | mod types; 5 | mod worker; 6 | 7 | pub use manager::{Manager, ManagerError}; 8 | pub use types::{RequestCrawl, RequestCrawlSender}; 9 | -------------------------------------------------------------------------------- /rsky-relay/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | deprecated_safe, 3 | future_incompatible, 4 | let_underscore, 5 | keyword_idents, 6 | nonstandard_style, 7 | refining_impl_trait, 8 | rust_2018_compatibility, 9 | rust_2018_idioms, 10 | rust_2021_compatibility, 11 | rust_2024_compatibility, 12 | unused, 13 | warnings, 14 | clippy::all, 15 | clippy::cargo, 16 | clippy::dbg_macro, 17 | clippy::expect_used, 18 | clippy::iter_over_hash_type, 19 | clippy::nursery, 20 | clippy::pathbuf_init_then_push, 21 | clippy::pedantic, 22 | clippy::print_stderr, 23 | clippy::print_stdout, 24 | clippy::renamed_function_params, 25 | clippy::str_to_string, 26 | clippy::string_to_string, 27 | clippy::unused_result_ok, 28 | clippy::unwrap_used 29 | )] 30 | #![allow( 31 | clippy::cargo_common_metadata, 32 | clippy::missing_errors_doc, 33 | clippy::missing_panics_doc, 34 | clippy::missing_safety_doc, 35 | clippy::multiple_crate_versions 36 | )] 37 | 38 | mod crawler; 39 | mod publisher; 40 | mod server; 41 | mod types; 42 | mod validator; 43 | 44 | use std::sync::atomic::AtomicBool; 45 | 46 | use thiserror::Error; 47 | 48 | pub static SHUTDOWN: AtomicBool = AtomicBool::new(false); 49 | 50 | pub use crawler::Manager as CrawlerManager; 51 | pub use publisher::Manager as PublisherManager; 52 | pub use server::Server; 53 | pub use types::MessageRecycle; 54 | pub use validator::Manager as ValidatorManager; 55 | 56 | #[derive(Debug, Error)] 57 | pub enum RelayError { 58 | #[error("crawler error: {0}")] 59 | Crawler(#[from] crawler::ManagerError), 60 | #[error("publisher error: {0}")] 61 | Publisher(#[from] publisher::ManagerError), 62 | #[error("validator error: {0}")] 63 | Validator(#[from] validator::ManagerError), 64 | #[error("server error: {0}")] 65 | Server(#[from] server::ServerError), 66 | } 67 | -------------------------------------------------------------------------------- /rsky-relay/src/publisher/mod.rs: -------------------------------------------------------------------------------- 1 | mod connection; 2 | mod manager; 3 | mod types; 4 | mod worker; 5 | 6 | pub use manager::{Manager, ManagerError}; 7 | pub use types::{MaybeTlsStream, SubscribeRepos, SubscribeReposSender}; 8 | -------------------------------------------------------------------------------- /rsky-relay/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | #[expect(clippy::module_inception)] 2 | mod server; 3 | mod types; 4 | 5 | pub use server::{Server, ServerError}; 6 | -------------------------------------------------------------------------------- /rsky-relay/src/server/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct ListHosts { 5 | pub cursor: Option, 6 | pub hosts: Vec, 7 | } 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct Host { 12 | pub account_count: u64, 13 | pub hostname: String, 14 | pub seq: u64, 15 | pub status: HostStatus, 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize)] 19 | #[serde(rename_all = "snake_case")] 20 | pub enum HostStatus { 21 | Active, 22 | Idle, 23 | Offline, 24 | Throttled, 25 | Banned, 26 | } 27 | -------------------------------------------------------------------------------- /rsky-relay/src/validator/mod.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | mod manager; 3 | mod resolver; 4 | mod types; 5 | mod utils; 6 | 7 | pub use manager::{Manager, ManagerError}; 8 | -------------------------------------------------------------------------------- /rsky-relay/ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: Must supply a domain" 5 | exit 1 6 | fi 7 | 8 | DOMAIN=$1 9 | 10 | openssl genrsa -des3 -out cacert.key 2048 11 | openssl req -x509 -new -nodes -key cacert.key -sha256 -days 1825 -out cacert.pem 12 | openssl genrsa -out "$DOMAIN".key 2048 13 | openssl req -new -key "$DOMAIN".key -out "$DOMAIN".csr 14 | 15 | cat >"$DOMAIN".ext <"] 5 | description = "Rust crate for atproto repositories, an in particular the Merkle Search Tree (MST) data structure." 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = true 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-repo" 11 | documentation = "https://docs.rs/rsky-repo" 12 | 13 | [dependencies] 14 | rsky-syntax = { workspace = true } 15 | rsky-common = { workspace = true } 16 | rsky-crypto = {workspace = true} 17 | rsky-lexicon = {workspace = true} 18 | serde = {workspace = true} 19 | serde_derive = {workspace = true} 20 | serde_cbor = { workspace = true } 21 | serde_ipld_dagcbor = {workspace = true} 22 | serde_json = {workspace = true} 23 | serde_bytes = {workspace = true} 24 | lexicon_cid = {workspace = true} 25 | tokio = {workspace = true} 26 | async-recursion = "1.1.1" 27 | sha2 = {workspace = true} 28 | anyhow = "1.0.79" # @TODO: Remove anyhow in lib 29 | futures = "0.3.28" 30 | thiserror = "1.0.40" 31 | async-stream = "0.3.5" 32 | async-trait = "0.1.86" 33 | integer-encoding = { version = "3", features = ["tokio_async"] } 34 | rand = "0.8.5" 35 | rand_core = "0.6.4" 36 | secp256k1 = {workspace = true} 37 | ipld-core = {workspace = true} 38 | iroh-car = "0.5.1" 39 | 40 | regex = "1.10.3" 41 | lazy_static = "1.4.0" 42 | 43 | [dev-dependencies] 44 | glob = "0.3" 45 | indexmap = "2" -------------------------------------------------------------------------------- /rsky-repo/README.md: -------------------------------------------------------------------------------- 1 | # rsky-repo: Repository and MST 2 | 3 | Rust crate for atproto repositories, and in particular the Merkle Search Tree (MST) data structure. 4 | 5 | ## License 6 | 7 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-repo/resources/test/valid_repo.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksky-algorithms/rsky/458f3826bf857f8d05a6fc42a6822000c36a60a1/rsky-repo/resources/test/valid_repo.car -------------------------------------------------------------------------------- /rsky-repo/src/cid_set.rs: -------------------------------------------------------------------------------- 1 | use lexicon_cid::Cid; 2 | use std::collections::HashSet; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 6 | pub struct CidSet { 7 | pub set: HashSet, 8 | } 9 | impl CidSet { 10 | pub fn new(arr: Option>) -> Self { 11 | let str_arr: Vec = arr 12 | .unwrap_or(Vec::new()) 13 | .into_iter() 14 | .map(|cid| cid.to_string()) 15 | .collect::>(); 16 | CidSet { 17 | set: HashSet::from_iter(str_arr), 18 | } 19 | } 20 | 21 | pub fn add(&mut self, cid: Cid) -> () { 22 | let _ = &self.set.insert(cid.to_string()); 23 | () 24 | } 25 | 26 | pub fn add_set(&mut self, to_merge: CidSet) -> () { 27 | for cid in to_merge.to_list() { 28 | let _ = &self.add(cid); 29 | } 30 | () 31 | } 32 | 33 | pub fn subtract_set(&mut self, to_subtract: CidSet) -> () { 34 | for cid in to_subtract.to_list() { 35 | self.delete(cid); 36 | } 37 | () 38 | } 39 | 40 | pub fn delete(&mut self, cid: Cid) -> () { 41 | self.set.remove(&cid.to_string()); 42 | () 43 | } 44 | 45 | pub fn has(&self, cid: Cid) -> bool { 46 | self.set.contains(&cid.to_string()) 47 | } 48 | 49 | pub fn size(&self) -> usize { 50 | self.set.len() 51 | } 52 | 53 | pub fn clear(mut self) -> () { 54 | self.set.clear(); 55 | () 56 | } 57 | 58 | pub fn to_list(&self) -> Vec { 59 | self.set 60 | .clone() 61 | .into_iter() 62 | .filter_map(|cid| match Cid::from_str(&cid) { 63 | Ok(r) => Some(r), 64 | Err(_) => None, 65 | }) 66 | .collect::>() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rsky-repo/src/error.rs: -------------------------------------------------------------------------------- 1 | use lexicon_cid::Cid; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum DataStoreError { 6 | #[error("missing block `{0}`")] 7 | MissingBlock(String), 8 | #[error("missing `{0}` blocks: `{1:?}`")] 9 | MissingBlocks(String, Vec), 10 | #[error("unexpected object at `{0}`")] 11 | UnexpectedObject(Cid), 12 | #[error("unknown data store error")] 13 | Unknown, 14 | } 15 | 16 | #[derive(Error, Debug)] 17 | pub enum RepoError { 18 | #[error("Commit was at`{0}`")] 19 | BadCommitSwapError(Cid), 20 | #[error("Record was at`{0:?}`")] 21 | BadRecordSwapError(Option), 22 | #[error("Invalid record error")] 23 | InvalidRecordError, 24 | } 25 | 26 | #[derive(Error, Debug)] 27 | pub enum BlobError { 28 | #[error("Blob not found")] 29 | BlobNotFoundError, 30 | } 31 | -------------------------------------------------------------------------------- /rsky-repo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | extern crate core; 4 | extern crate serde; 5 | 6 | pub mod block_map; 7 | pub mod car; 8 | pub mod cid_set; 9 | pub mod data_diff; 10 | pub mod error; 11 | pub mod mst; 12 | pub mod parse; 13 | pub mod readable_repo; 14 | pub mod repo; 15 | pub mod storage; 16 | pub mod sync; 17 | pub mod types; 18 | pub mod util; 19 | -------------------------------------------------------------------------------- /rsky-repo/src/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::block_map::BlockMap; 2 | use crate::error::DataStoreError; 3 | use crate::storage::ObjAndBytes; 4 | use crate::types::RepoRecord; 5 | use crate::util::cbor_to_lex_record; 6 | use anyhow::Result; 7 | use lexicon_cid::Cid; 8 | use serde_cbor::Value as CborValue; 9 | 10 | pub struct RecordAndBytes { 11 | pub record: RepoRecord, 12 | pub bytes: Vec, 13 | } 14 | 15 | pub fn get_and_parse_record(blocks: &BlockMap, cid: Cid) -> Result { 16 | let bytes = blocks.get(cid); 17 | return if let Some(b) = bytes { 18 | let record = cbor_to_lex_record(b.clone())?; 19 | Ok(RecordAndBytes { 20 | record, 21 | bytes: b.clone(), 22 | }) 23 | } else { 24 | Err(anyhow::Error::new(DataStoreError::MissingBlock( 25 | cid.to_string(), 26 | ))) 27 | }; 28 | } 29 | 30 | pub fn get_and_parse_by_kind( 31 | blocks: &BlockMap, 32 | cid: Cid, 33 | check: impl FnOnce(CborValue) -> bool, 34 | ) -> Result { 35 | let bytes = blocks.get(cid); 36 | return if let Some(b) = bytes { 37 | Ok(parse_obj_by_kind(b.clone(), cid, check)?) 38 | } else { 39 | Err(anyhow::Error::new(DataStoreError::MissingBlock( 40 | cid.to_string(), 41 | ))) 42 | }; 43 | } 44 | 45 | pub fn parse_obj_by_kind( 46 | bytes: Vec, 47 | cid: Cid, 48 | check: impl FnOnce(CborValue) -> bool, 49 | ) -> Result { 50 | let obj: CborValue = serde_ipld_dagcbor::from_slice(bytes.as_slice()).map_err(|error| { 51 | anyhow::Error::new(DataStoreError::UnexpectedObject(cid)).context(error) 52 | })?; 53 | if check(obj.clone()) { 54 | Ok(ObjAndBytes { obj, bytes }) 55 | } else { 56 | Err(anyhow::Error::new(DataStoreError::UnexpectedObject(cid))) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rsky-repo/src/readable_repo.rs: -------------------------------------------------------------------------------- 1 | use crate::mst::MST; 2 | use crate::storage::types::RepoStorage; 3 | use crate::types::{Commit, VersionedCommit}; 4 | use crate::util::ensure_v3_commit; 5 | use anyhow::Result; 6 | use lexicon_cid::Cid; 7 | use serde_cbor::Value as CborValue; 8 | use std::sync::Arc; 9 | use tokio::sync::RwLock; 10 | 11 | pub struct ReadableRepo { 12 | pub storage: Arc>, 13 | pub data: MST, 14 | pub commit: Commit, 15 | pub cid: Cid, 16 | } 17 | 18 | impl ReadableRepo { 19 | // static 20 | pub fn new(storage: Arc>, data: MST, commit: Commit, cid: Cid) -> Self { 21 | Self { 22 | storage, 23 | data, 24 | commit, 25 | cid, 26 | } 27 | } 28 | 29 | pub async fn load(storage: Arc>, commit_cid: Cid) -> Result { 30 | let commit: CborValue = { 31 | let storage_guard = storage.read().await; 32 | storage_guard 33 | .read_obj( 34 | &commit_cid, 35 | Box::new(|obj: CborValue| { 36 | match serde_cbor::value::from_value::(obj.clone()) { 37 | Ok(_) => true, 38 | Err(_) => false, 39 | } 40 | }), 41 | ) 42 | .await? 43 | }; 44 | let commit: VersionedCommit = serde_cbor::value::from_value(commit)?; 45 | let data = MST::load(storage.clone(), commit.data(), None)?; 46 | Ok(Self::new( 47 | storage, 48 | data, 49 | ensure_v3_commit(commit), 50 | commit_cid, 51 | )) 52 | } 53 | 54 | pub fn did(&self) -> &String { 55 | &self.commit.did 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rsky-repo/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | use lexicon_cid::Cid; 2 | use serde_cbor::Value as CborValue; 3 | use serde_json::Value as JsonValue; 4 | use std::collections::BTreeMap; 5 | use thiserror::Error; 6 | 7 | /// Ipld 8 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 9 | #[serde(untagged)] 10 | pub enum Ipld { 11 | /// Represents a Cid. 12 | Link(Cid), 13 | /// Represents a list. 14 | List(Vec), 15 | /// Represents a map of strings to objects. 16 | Map(BTreeMap), 17 | /// String 18 | String(String), 19 | /// Represents a sequence of bytes. 20 | #[serde(with = "serde_bytes")] 21 | Bytes(Vec), 22 | /// Represents a Json Value 23 | Json(JsonValue), 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize)] 27 | pub struct ObjAndBytes { 28 | pub obj: CborValue, 29 | #[serde(with = "serde_bytes")] 30 | pub bytes: Vec, 31 | } 32 | 33 | #[derive(Debug, Clone, Deserialize, Serialize)] 34 | pub struct CidAndRev { 35 | pub cid: Cid, 36 | pub rev: String, 37 | } 38 | 39 | #[derive(Error, Debug)] 40 | pub enum RepoRootError { 41 | #[error("Repo root not found")] 42 | RepoRootNotFoundError, 43 | } 44 | 45 | pub mod memory_blockstore; 46 | pub mod readable_blockstore; 47 | pub mod sync_storage; 48 | pub mod types; 49 | -------------------------------------------------------------------------------- /rsky-repo/src/storage/types.rs: -------------------------------------------------------------------------------- 1 | use crate::block_map::BlockMap; 2 | use crate::storage::readable_blockstore::ReadableBlockstore; 3 | use crate::types::CommitData; 4 | use anyhow::Result; 5 | use lexicon_cid::Cid; 6 | use std::fmt::Debug; 7 | use std::future::Future; 8 | use std::pin::Pin; 9 | 10 | pub trait RepoStorage: ReadableBlockstore + Send + Sync + Debug { 11 | // Writeable 12 | fn get_root<'a>(&'a self) -> Pin> + Send + Sync + 'a>>; 13 | fn put_block<'a>( 14 | &'a self, 15 | cid: Cid, 16 | bytes: Vec, 17 | rev: String, 18 | ) -> Pin> + Send + Sync + 'a>>; 19 | fn put_many<'a>( 20 | &'a self, 21 | to_put: BlockMap, 22 | rev: String, 23 | ) -> Pin> + Send + Sync + 'a>>; 24 | fn update_root<'a>( 25 | &'a self, 26 | cid: Cid, 27 | rev: String, 28 | is_create: Option, 29 | ) -> Pin> + Send + Sync + 'a>>; 30 | fn apply_commit<'a>( 31 | &'a self, 32 | commit: CommitData, 33 | is_create: Option, 34 | ) -> Pin> + Send + Sync + 'a>>; 35 | } 36 | -------------------------------------------------------------------------------- /rsky-repo/src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod consumer; 2 | pub mod provider; 3 | -------------------------------------------------------------------------------- /rsky-satnav/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | .DS_Store 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | -------------------------------------------------------------------------------- /rsky-satnav/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-satnav" 3 | version = "0.1.0" 4 | authors = ["Rudy Fraser "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | getrandom = { version = "0.2", features = ["js"] } 11 | dioxus = { version = "0.6.0", features = [] } 12 | wasm-bindgen = "0.2.100" 13 | web-sys = { version = "0.3.77",features = [ 14 | "File", 15 | "FileList", 16 | "HtmlInputElement", 17 | "EventTarget", 18 | "Event"] } 19 | wasm-bindgen-futures = "0.4.50" 20 | gloo-file = { version = "0.3.0",features = ["futures"] } 21 | anyhow = "1.0.97" 22 | dioxus-web = "0.6.3" 23 | iroh-car = "0.5.1" 24 | serde_ipld_dagcbor = {workspace = true} 25 | serde_json = {workspace = true} 26 | base64 = "0.22.1" 27 | ipld-core = "0.4.2" 28 | hex = "0.4" 29 | cid = "0.11" 30 | 31 | [features] 32 | default = ["web"] 33 | web = ["dioxus/web"] 34 | desktop = ["dioxus/desktop"] 35 | mobile = ["dioxus/mobile"] 36 | -------------------------------------------------------------------------------- /rsky-satnav/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | [web.app] 4 | 5 | # HTML title tag content 6 | title = "rsky-satnav" 7 | 8 | # include `assets` in web platform 9 | [web.resource] 10 | 11 | # Additional CSS style files 12 | style = [] 13 | 14 | # Additional JavaScript files 15 | script = [] 16 | 17 | [web.resource.dev] 18 | 19 | # Javascript code file 20 | # serve: [dev-server] only 21 | script = [] 22 | -------------------------------------------------------------------------------- /rsky-satnav/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksky-algorithms/rsky/458f3826bf857f8d05a6fc42a6822000c36a60a1/rsky-satnav/assets/favicon.ico -------------------------------------------------------------------------------- /rsky-satnav/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f1ecec; 3 | color: #000; 4 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 5 | margin: 20px; 6 | } 7 | 8 | #hero { 9 | margin: 0; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: left; 13 | align-items: start; 14 | } 15 | 16 | #header { 17 | max-width: 1200px; 18 | } 19 | 20 | details.folder > summary::before { 21 | content: "📁 "; 22 | margin-right: 0.25em; 23 | } 24 | 25 | details.file > summary::before { 26 | content: "📄 "; 27 | margin-right: 0.25em; 28 | } 29 | 30 | details > summary { 31 | cursor: pointer; 32 | font-weight: bold; 33 | } 34 | 35 | ul { 36 | list-style: none; 37 | padding-left: 1em; 38 | margin: 0.25em 0; 39 | } 40 | 41 | li { 42 | margin-left: 0.5em; 43 | } 44 | 45 | h1, h2 { 46 | color: #3642e3; 47 | } 48 | .car-content { 49 | margin-top: 20px; 50 | } 51 | .car-block { 52 | margin-bottom: 1em; 53 | padding: 0.5em; 54 | border: 1px solid #ccc; 55 | border-radius: 6px; 56 | } 57 | .repo-structure { 58 | margin-top: 2em; 59 | padding: 1em; 60 | background: #f3f3f3; 61 | border-radius: 6px; 62 | } 63 | .collection-group { 64 | margin-bottom: 1em; 65 | padding: 0.5em; 66 | border: 1px solid #aaa; 67 | border-radius: 4px; 68 | background: #fff; 69 | } -------------------------------------------------------------------------------- /rsky-satnav/input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; -------------------------------------------------------------------------------- /rsky-satnav/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@tailwindcss/cli": "^4.0.12", 4 | "tailwindcss": "^4.0.12" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /rsky-satnav/src/components/hero.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | const HEADER_SVG: Asset = asset!("/assets/header.svg"); 4 | 5 | #[component] 6 | pub fn Hero() -> Element { 7 | rsx! { 8 | div { 9 | id: "hero", 10 | img { src: HEADER_SVG, id: "header" } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rsky-satnav/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod hero; 2 | pub use hero::Hero; 3 | -------------------------------------------------------------------------------- /rsky-satnav/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "all", 4 | content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /rsky-syntax/ATURI_VALIDATION.md: -------------------------------------------------------------------------------- 1 | # Notes on AT-URI Validation 2 | 3 | At the time of writing this, this rsky-syntax validation of AT-URI reflects the Typescript implementation of the [syntax package](https://github.com/bluesky-social/atproto/tree/main/packages/syntax) instead of the atproto.com [specification](https://atproto.com/specs/at-uri-scheme). There are some differences with the typescript and rust validation implementations and the specification for AT URIs: 4 | 5 | - The validation conforms more to the "Full AT URI Syntax" form than the "Restricted AT URI Syntax" that is used in the [lexicon](https://github.com/bluesky-social/atproto/tree/main/packages/lexicon). 6 | - The Rust AT-URI validation does admit some invalid syntax. 7 | - The Rust DID and Handle validation adheres to the specification. 8 | - The Rust NSID and TID validation adheres to the specification. 9 | - The Rust Record Key validation adheres to the specification. 10 | 11 | # References 12 | 13 | 1. [rsky-syntax PR](https://github.com/blacksky-algorithms/rsky/pull/39). -------------------------------------------------------------------------------- /rsky-syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsky-syntax" 3 | version = "0.1.0" 4 | authors = ["Rudy Fraser "] 5 | description = "Rust library of validation helpers for identifier strings" 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | homepage = "https://blackskyweb.xyz" 10 | repository = "https://github.com/blacksky-algorithms/rsky/tree/main/rsky-syntax" 11 | documentation = "https://docs.rs/rsky-syntax" 12 | 13 | [dependencies] 14 | anyhow = "1.0.86" 15 | chrono = "0.4.39" 16 | lazy_static = "1.5.0" 17 | regex = "1.10.5" 18 | serde = { version = "1.0.160", features = ["derive"] } 19 | serde_derive = "^1.0" 20 | thiserror = "1.0.40" 21 | url = "2.5.2" 22 | -------------------------------------------------------------------------------- /rsky-syntax/README.md: -------------------------------------------------------------------------------- 1 | # rsky-syntax: validation helpers for identifier strings 2 | 3 | Validation logic for [atproto](https://atproto.com) identifiers - DIDs, Handles, NSIDs, and AT URIs. 4 | 5 | [![Crate](https://img.shields.io/crates/v/rsky-syntax?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44)](https://crates.io/crates/rsky-syntax) 6 | 7 | ## License 8 | 9 | rsky is released under the [Apache License 2.0](../LICENSE). -------------------------------------------------------------------------------- /rsky-syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | extern crate serde; 4 | 5 | pub mod aturi; 6 | pub mod aturi_validation; 7 | pub mod datetime; 8 | pub mod did; 9 | pub mod handle; 10 | pub mod nsid; 11 | pub mod record_key; 12 | pub mod tid; 13 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-01-03" 3 | components = ["clippy", "rustfmt"] 4 | --------------------------------------------------------------------------------