"]
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 | [](https://deps.rs/repo/github/blacksky-algorithms/rsky) [](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 | [](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