Compare commits
15 Commits
v0.1.0
...
432f39cbb4
| Author | SHA1 | Date | |
|---|---|---|---|
| 432f39cbb4 | |||
| 2c509cbf88 | |||
| 52614d406a | |||
| 1949fce620 | |||
| 699258f830 | |||
| 9412a9739a | |||
| 13111c10b9 | |||
| 2e3b6d5cd4 | |||
| bc857b2c08 | |||
| 7901b29f7c | |||
|
|
a604e1bd40 | ||
|
|
f5374ec861 | ||
|
|
cc30582a1c | ||
|
|
f8dc20c026 | ||
|
|
630cffe33f |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1368,7 +1368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "k-ap"
|
||||
version = "0.1.0"
|
||||
version = "0.1.9"
|
||||
dependencies = [
|
||||
"activitypub_federation",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "k-ap"
|
||||
version = "0.1.0"
|
||||
version = "0.1.9"
|
||||
edition = "2024"
|
||||
description = "Generic ActivityPub protocol layer"
|
||||
license = "MIT"
|
||||
|
||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# k-ap
|
||||
|
||||
Generic ActivityPub protocol layer for Rust services. Extracted from the `thoughts` and `movies-diary` projects.
|
||||
|
||||
Wraps [`activitypub_federation`](https://crates.io/crates/activitypub_federation) and provides the plumbing that every AP-enabled service needs: actor management, inbox/outbox routing, follower tracking, WebFinger, NodeInfo, and HTTP signature handling.
|
||||
|
||||
Not domain-specific — no opinions about what your content type looks like.
|
||||
|
||||
## Add as dependency
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.0" }
|
||||
```
|
||||
|
||||
## What you implement
|
||||
|
||||
Three traits wire your data layer into `k-ap`:
|
||||
|
||||
```rust
|
||||
// Your database layer for follows, keypairs, remote actors, blocks
|
||||
impl FederationRepository for MyFederationRepo { ... }
|
||||
|
||||
// Your user lookup (id, username, bio, avatar)
|
||||
impl ApUserRepository for MyUserRepo { ... }
|
||||
|
||||
// Dispatch incoming AP objects to the right handler
|
||||
impl ApObjectHandler for MyObjectHandler { ... }
|
||||
```
|
||||
|
||||
## Wire up the service
|
||||
|
||||
```rust
|
||||
use k_ap::{ActivityPubService, FederationRepository, ApUserRepository, ApObjectHandler};
|
||||
|
||||
let service = ActivityPubService::builder(
|
||||
Arc::new(my_federation_repo),
|
||||
Arc::new(my_user_repo),
|
||||
Arc::new(my_object_handler),
|
||||
"https://example.com",
|
||||
)
|
||||
.allow_registration(true)
|
||||
.software_name("my-app")
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Mount the AP routes onto your axum router
|
||||
let router = Router::new().merge(service.router());
|
||||
```
|
||||
|
||||
## What the service handles for you
|
||||
|
||||
- **Actor** — `GET /users/:id` serves the AP Person object with public key
|
||||
- **Inbox** — `POST /users/:id/inbox` + `POST /inbox` (shared), verifies HTTP signatures, dispatches to your `ApObjectHandler`
|
||||
- **Outbox** — `GET /users/:id/outbox` with OrderedCollection pagination
|
||||
- **Followers / Following** — `GET /users/:id/followers` and `/following`
|
||||
- **WebFinger** — `GET /.well-known/webfinger`
|
||||
- **NodeInfo** — `GET /.well-known/nodeinfo` + `GET /nodeinfo/2.1`
|
||||
|
||||
## Broadcast from your domain layer
|
||||
|
||||
```rust
|
||||
// Fan out a new note to all accepted followers
|
||||
service.broadcast_create_note(user_id, ¬e_json).await?;
|
||||
service.broadcast_update_note(user_id, ¬e_json).await?;
|
||||
|
||||
// Announce / Undo Announce
|
||||
service.broadcast_announce_to_followers(user_id, object_ap_id).await?;
|
||||
service.broadcast_undo_announce_to_followers(user_id, object_ap_id, object_url).await?;
|
||||
|
||||
// Like / Unlike to a remote inbox
|
||||
service.broadcast_like_to_inbox(user_id, object_ap_id, inbox_url).await?;
|
||||
service.broadcast_undo_like_to_inbox(user_id, object_ap_id, inbox_url).await?;
|
||||
|
||||
// Follow / Unfollow / Accept / Reject
|
||||
service.follow(local_user_id, remote_actor_url, handle).await?;
|
||||
service.unfollow(local_user_id, remote_actor_url).await?;
|
||||
service.accept_follower(local_user_id, remote_actor_url).await?;
|
||||
service.reject_follower(local_user_id, remote_actor_url).await?;
|
||||
```
|
||||
|
||||
## Project-specific ports
|
||||
|
||||
`k-ap` does not define port traits tied to your domain (e.g. `OutboundFederationPort`, `ActivityPubRepository<Thought>`). Those belong in your adapter layer and are wired up there. See `crates/adapters/activitypub/src/port.rs` in `thoughts` for a reference implementation.
|
||||
|
||||
## Key public types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `ActivityPubService` | Central service — build once, share via `Arc` |
|
||||
| `FederationData` | Request-scoped data passed through the federation layer |
|
||||
| `FederationRepository` | Trait: follows, keypairs, remote actors, blocks |
|
||||
| `ApUserRepository` | Trait: user lookup by id / username |
|
||||
| `ApObjectHandler` | Trait: dispatch incoming AP objects |
|
||||
| `RemoteActor` | A federated actor record |
|
||||
| `Follower` / `FollowerStatus` | Follower with pending/accepted/rejected state |
|
||||
| `ApUser` | AP-serializable local user |
|
||||
| `ApFederationConfig` | Wraps the `activitypub_federation` config |
|
||||
| `Error` | AP-layer error type |
|
||||
@@ -65,12 +65,20 @@ impl Activity for FollowActivity {
|
||||
)));
|
||||
}
|
||||
};
|
||||
if target_domain != data.domain {
|
||||
return Err(Error::bad_request(anyhow::anyhow!(
|
||||
"follow target is not a local actor"
|
||||
)));
|
||||
if target_domain == data.domain {
|
||||
return Ok(());
|
||||
}
|
||||
Ok(())
|
||||
// Domain mismatch — still accept if the UUID resolves to a local user.
|
||||
// This handles domain migrations where remote servers have cached the old actor URL.
|
||||
if let Some(uuid) = crate::urls::extract_user_id_from_url(target_url) {
|
||||
if data.user_repo.find_by_id(uuid).await.ok().flatten().is_some() {
|
||||
tracing::debug!(target = %target_url, local_domain = %data.domain, "accepting follow for migrated actor URL");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(Error::bad_request(anyhow::anyhow!(
|
||||
"follow target is not a local actor"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
@@ -829,10 +837,86 @@ impl Activity for MoveActivity {
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fetch the target actor via signed request.
|
||||
let target = ObjectId::<DbActor>::from(self.target.clone())
|
||||
.dereference(data)
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||
|
||||
// Verify the new actor claims the old identity via alsoKnownAs.
|
||||
let old_url = self.object.as_str();
|
||||
if target.also_known_as.as_deref() != Some(old_url) {
|
||||
return Err(Error::bad_request(anyhow::anyhow!(
|
||||
"Move target alsoKnownAs does not reference old actor"
|
||||
)));
|
||||
}
|
||||
|
||||
// Migrate DB records; get user IDs that need a re-follow.
|
||||
let affected = data
|
||||
.federation_repo
|
||||
.migrate_follower_actor(old_url, self.target.as_str())
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||
|
||||
let affected_count = affected.len();
|
||||
|
||||
// Re-follow on behalf of each affected local user.
|
||||
for local_user_id in &affected {
|
||||
let local_actor = match crate::actors::get_local_actor(*local_user_id, data).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, %local_user_id, "Move: failed to load local actor for re-follow");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let follow_id = match crate::urls::activity_url(&data.base_url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to generate follow activity URL");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let follow = FollowActivity {
|
||||
id: follow_id,
|
||||
kind: Default::default(),
|
||||
actor: activitypub_federation::fetch::object_id::ObjectId::from(
|
||||
local_actor.ap_id.clone(),
|
||||
),
|
||||
object: activitypub_federation::fetch::object_id::ObjectId::from(
|
||||
self.target.clone(),
|
||||
),
|
||||
};
|
||||
|
||||
let sends = match activitypub_federation::activity_sending::SendActivityTask::prepare(
|
||||
&activitypub_federation::protocol::context::WithContext::new_default(follow),
|
||||
&local_actor,
|
||||
vec![target.inbox_url.clone()],
|
||||
data,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to prepare re-follow");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for send in sends {
|
||||
if let Err(e) = send.sign_and_send(data).await {
|
||||
tracing::warn!(error = %e, %local_user_id, "Move: re-follow delivery failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
actor = %self.actor.inner(),
|
||||
target = %self.target,
|
||||
"received Move (account migration) — target noted"
|
||||
affected = affected_count,
|
||||
"received Move — migrated follower relationships"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
http_signatures::generate_actor_keypair,
|
||||
kinds::actor::PersonType,
|
||||
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
||||
traits::{Actor, Object},
|
||||
};
|
||||
@@ -19,6 +18,7 @@ use crate::user::ApProfileField;
|
||||
pub struct DbActor {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub public_key_pem: String,
|
||||
pub private_key_pem: Option<String>,
|
||||
pub inbox_url: Url,
|
||||
@@ -57,18 +57,39 @@ pub struct ProfileFieldObject {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Accepts any AP actor type on inbound JSON; always serializes as "Person" for local actors.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ApActorType {
|
||||
Person,
|
||||
Service,
|
||||
Application,
|
||||
Organization,
|
||||
Group,
|
||||
}
|
||||
|
||||
impl Default for ApActorType {
|
||||
fn default() -> Self {
|
||||
Self::Person
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Person {
|
||||
#[serde(rename = "type")]
|
||||
kind: PersonType,
|
||||
kind: ApActorType,
|
||||
id: ObjectId<DbActor>,
|
||||
#[serde(default)]
|
||||
preferred_username: String,
|
||||
inbox: Url,
|
||||
outbox: Url,
|
||||
followers: Url,
|
||||
following: Url,
|
||||
public_key: PublicKey,
|
||||
#[serde(default)]
|
||||
outbox: Option<Url>,
|
||||
#[serde(default)]
|
||||
followers: Option<Url>,
|
||||
#[serde(default)]
|
||||
following: Option<Url>,
|
||||
pub public_key: PublicKey,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
summary: Option<String>,
|
||||
@@ -78,6 +99,7 @@ pub struct Person {
|
||||
url: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
discoverable: Option<bool>,
|
||||
#[serde(default)]
|
||||
manually_approves_followers: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
updated: Option<DateTime<Utc>>,
|
||||
@@ -152,6 +174,7 @@ pub async fn get_local_actor(
|
||||
Ok(DbActor {
|
||||
user_id,
|
||||
username: user.username,
|
||||
display_name: None,
|
||||
public_key_pem: public_key,
|
||||
private_key_pem: Some(private_key),
|
||||
inbox_url,
|
||||
@@ -170,6 +193,11 @@ pub async fn get_local_actor(
|
||||
})
|
||||
}
|
||||
|
||||
fn apex_domain(url: &Url) -> String {
|
||||
let host = url.host_str().unwrap_or("");
|
||||
host.strip_prefix("www.").unwrap_or(host).to_owned()
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for DbActor {
|
||||
type DataType = FederationData;
|
||||
@@ -219,6 +247,7 @@ impl Object for DbActor {
|
||||
Ok(Some(DbActor {
|
||||
user_id,
|
||||
username: user.username,
|
||||
display_name: None,
|
||||
public_key_pem: public_key,
|
||||
private_key_pem: private_key,
|
||||
inbox_url,
|
||||
@@ -228,12 +257,12 @@ impl Object for DbActor {
|
||||
following_url,
|
||||
ap_id,
|
||||
last_refreshed_at: Utc::now(),
|
||||
bio: None,
|
||||
avatar_url: None,
|
||||
banner_url: None,
|
||||
also_known_as: None,
|
||||
profile_url: None,
|
||||
attachment: vec![],
|
||||
bio: user.bio,
|
||||
avatar_url: user.avatar_url,
|
||||
banner_url: user.banner_url,
|
||||
also_known_as: user.also_known_as,
|
||||
profile_url: user.profile_url,
|
||||
attachment: user.attachment,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -272,9 +301,9 @@ impl Object for DbActor {
|
||||
id: self.ap_id.clone().into(),
|
||||
preferred_username: self.username.clone(),
|
||||
inbox: self.inbox_url.clone(),
|
||||
outbox: self.outbox_url.clone(),
|
||||
followers: self.followers_url.clone(),
|
||||
following: self.following_url.clone(),
|
||||
outbox: Some(self.outbox_url.clone()),
|
||||
followers: Some(self.followers_url.clone()),
|
||||
following: Some(self.following_url.clone()),
|
||||
public_key,
|
||||
name: Some(self.username.clone()),
|
||||
summary: self.bio.clone(),
|
||||
@@ -295,11 +324,26 @@ impl Object for DbActor {
|
||||
expected_domain: &Url,
|
||||
_data: &Data<Self::DataType>,
|
||||
) -> Result<(), Self::Error> {
|
||||
verify_domains_match(json.id.inner(), expected_domain)?;
|
||||
Ok(())
|
||||
if verify_domains_match(json.id.inner(), expected_domain).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
if apex_domain(json.id.inner()) == apex_domain(expected_domain) {
|
||||
tracing::debug!(
|
||||
actor_id = %json.id.inner(),
|
||||
expected = %expected_domain,
|
||||
"domain verified via www-apex equivalence"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
verify_domains_match(json.id.inner(), expected_domain).map_err(Error::from)
|
||||
}
|
||||
|
||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||
tracing::debug!(
|
||||
actor_id = %json.id.inner(),
|
||||
username = %json.preferred_username,
|
||||
"ingesting remote actor"
|
||||
);
|
||||
let shared_inbox_url = json.endpoints.as_ref().map(|e| e.shared_inbox.to_string());
|
||||
let actor = RemoteActor {
|
||||
url: json.id.inner().to_string(),
|
||||
@@ -308,7 +352,7 @@ impl Object for DbActor {
|
||||
shared_inbox_url,
|
||||
display_name: json.name.clone(),
|
||||
avatar_url: json.icon.as_ref().map(|i| i.url.to_string()),
|
||||
outbox_url: Some(json.outbox.to_string()),
|
||||
outbox_url: json.outbox.as_ref().map(|u| u.to_string()),
|
||||
};
|
||||
data.federation_repo.upsert_remote_actor(actor).await?;
|
||||
|
||||
@@ -320,13 +364,17 @@ impl Object for DbActor {
|
||||
.endpoints
|
||||
.as_ref()
|
||||
.and_then(|e| Url::parse(e.shared_inbox.as_str()).ok());
|
||||
let outbox_url = json.outbox.clone();
|
||||
let followers_url = json.followers.clone();
|
||||
let following_url = json.following.clone();
|
||||
let fallback = |suffix: &str| {
|
||||
Url::parse(&format!("{}{}", ap_id, suffix)).unwrap_or_else(|_| ap_id.clone())
|
||||
};
|
||||
let outbox_url = json.outbox.clone().unwrap_or_else(|| fallback("/outbox"));
|
||||
let followers_url = json.followers.clone().unwrap_or_else(|| fallback("/followers"));
|
||||
let following_url = json.following.clone().unwrap_or_else(|| fallback("/following"));
|
||||
|
||||
Ok(DbActor {
|
||||
user_id,
|
||||
username: json.preferred_username.clone(),
|
||||
display_name: json.name.clone(),
|
||||
public_key_pem: json.public_key.public_key_pem,
|
||||
private_key_pem: None,
|
||||
inbox_url,
|
||||
|
||||
@@ -25,4 +25,4 @@ pub use repository::{
|
||||
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||
};
|
||||
pub use service::ActivityPubService;
|
||||
pub use user::{ApProfileField, ApUser, ApUserRepository};
|
||||
pub use user::{ApProfileField, ApUser, ApUserRepository, LookedUpActor};
|
||||
|
||||
@@ -131,4 +131,12 @@ pub trait FederationRepository: Send + Sync {
|
||||
async fn remove_blocked_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> Result<()>;
|
||||
async fn get_blocked_actors(&self, local_user_id: uuid::Uuid) -> Result<Vec<String>>;
|
||||
async fn is_actor_blocked(&self, local_user_id: uuid::Uuid, actor_url: &str) -> Result<bool>;
|
||||
/// Migrate all local following records from old_actor_url to new_actor_url.
|
||||
/// Returns the local user IDs whose records were migrated (excludes users
|
||||
/// already following the new actor — they need no re-follow).
|
||||
async fn migrate_follower_actor(
|
||||
&self,
|
||||
old_actor_url: &str,
|
||||
new_actor_url: &str,
|
||||
) -> Result<Vec<uuid::Uuid>>;
|
||||
}
|
||||
|
||||
282
src/service.rs
282
src/service.rs
@@ -173,6 +173,10 @@ impl ActivityPubService {
|
||||
self.federation_config.to_request_data()
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
/// Returns `(local_actor, deduplicated_inboxes)` for all accepted followers,
|
||||
/// excluding blocked actors and blocked domains.
|
||||
/// Returns `None` if there are no eligible followers.
|
||||
@@ -220,6 +224,98 @@ impl ActivityPubService {
|
||||
Ok(Some((local_actor, collect_inboxes(&accepted))))
|
||||
}
|
||||
|
||||
/// Build an OrderedCollection or OrderedCollectionPage JSON for the local
|
||||
/// user's followers list. Pass `page = None` for the root collection.
|
||||
pub async fn followers_collection_json(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String> {
|
||||
const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||
const PAGE_SIZE: usize = 20;
|
||||
let data = self.federation_config.to_request_data();
|
||||
let collection_id = format!("{}/users/{}/followers", self.base_url, user_id);
|
||||
let total = data.federation_repo.count_followers(user_id).await?;
|
||||
let obj = if let Some(p) = page {
|
||||
let p = p.max(1);
|
||||
let offset = (p.saturating_sub(1) as usize) * PAGE_SIZE;
|
||||
let followers = data
|
||||
.federation_repo
|
||||
.get_followers_page(user_id, offset as u32, PAGE_SIZE)
|
||||
.await?;
|
||||
let has_next = offset + followers.len() < total;
|
||||
let items: Vec<String> = followers.into_iter().map(|f| f.actor.url).collect();
|
||||
let mut obj = serde_json::json!({
|
||||
"@context": AP_CONTEXT,
|
||||
"type": "OrderedCollectionPage",
|
||||
"id": format!("{}?page={}", collection_id, p),
|
||||
"partOf": collection_id,
|
||||
"totalItems": total,
|
||||
"orderedItems": items,
|
||||
});
|
||||
if has_next {
|
||||
obj["next"] =
|
||||
serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||
}
|
||||
obj
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"@context": AP_CONTEXT,
|
||||
"type": "OrderedCollection",
|
||||
"id": collection_id,
|
||||
"totalItems": total,
|
||||
"first": format!("{}?page=1", collection_id),
|
||||
})
|
||||
};
|
||||
Ok(serde_json::to_string(&obj)?)
|
||||
}
|
||||
|
||||
/// Build an OrderedCollection or OrderedCollectionPage JSON for the local
|
||||
/// user's following list. Pass `page = None` for the root collection.
|
||||
pub async fn following_collection_json(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String> {
|
||||
const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||
const PAGE_SIZE: usize = 20;
|
||||
let data = self.federation_config.to_request_data();
|
||||
let collection_id = format!("{}/users/{}/following", self.base_url, user_id);
|
||||
let total = data.federation_repo.count_following(user_id).await?;
|
||||
let obj = if let Some(p) = page {
|
||||
let p = p.max(1);
|
||||
let offset = (p.saturating_sub(1) as usize) * PAGE_SIZE;
|
||||
let following = data
|
||||
.federation_repo
|
||||
.get_following_page(user_id, offset as u32, PAGE_SIZE)
|
||||
.await?;
|
||||
let has_next = offset + following.len() < total;
|
||||
let items: Vec<String> = following.into_iter().map(|a| a.url).collect();
|
||||
let mut obj = serde_json::json!({
|
||||
"@context": AP_CONTEXT,
|
||||
"type": "OrderedCollectionPage",
|
||||
"id": format!("{}?page={}", collection_id, p),
|
||||
"partOf": collection_id,
|
||||
"totalItems": total,
|
||||
"orderedItems": items,
|
||||
});
|
||||
if has_next {
|
||||
obj["next"] =
|
||||
serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||
}
|
||||
obj
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"@context": AP_CONTEXT,
|
||||
"type": "OrderedCollection",
|
||||
"id": collection_id,
|
||||
"totalItems": total,
|
||||
"first": format!("{}?page=1", collection_id),
|
||||
})
|
||||
};
|
||||
Ok(serde_json::to_string(&obj)?)
|
||||
}
|
||||
|
||||
pub async fn actor_json(&self, user_id_str: &str) -> anyhow::Result<String> {
|
||||
use activitypub_federation::traits::Object;
|
||||
let uuid = uuid::Uuid::parse_str(user_id_str)?;
|
||||
@@ -234,6 +330,36 @@ impl ActivityPubService {
|
||||
Ok(serde_json::to_string(&WithContext::new_default(person))?)
|
||||
}
|
||||
|
||||
/// Resolve a `@user@domain` handle to actor data using a signed HTTP request.
|
||||
/// Unlike a plain unauthenticated fetch, this works with instances (e.g. Threads)
|
||||
/// that require HTTP signatures before returning full actor JSON.
|
||||
pub async fn lookup_actor_by_handle(
|
||||
&self,
|
||||
handle: &str,
|
||||
) -> anyhow::Result<crate::user::LookedUpActor> {
|
||||
tracing::info!(handle, "looking up remote actor");
|
||||
let data = self.federation_config.to_request_data();
|
||||
let actor = Self::webfinger_https(handle, &data).await
|
||||
.inspect_err(|e| tracing::warn!(handle, error = %e, "actor lookup failed"))?;
|
||||
let domain = actor.ap_id.host_str().unwrap_or("").to_string();
|
||||
let handle = format!("{}@{}", actor.username, domain);
|
||||
tracing::info!(handle, ap_url = %actor.ap_id, "remote actor resolved");
|
||||
Ok(crate::user::LookedUpActor {
|
||||
handle,
|
||||
display_name: actor.display_name,
|
||||
bio: actor.bio,
|
||||
avatar_url: actor.avatar_url,
|
||||
banner_url: actor.banner_url,
|
||||
ap_url: actor.ap_id,
|
||||
outbox_url: Some(actor.outbox_url),
|
||||
followers_url: Some(actor.followers_url),
|
||||
following_url: Some(actor.following_url),
|
||||
also_known_as: actor.also_known_as,
|
||||
profile_url: actor.profile_url,
|
||||
attachment: actor.attachment,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the ActivityPub router compatible with any outer state `S`.
|
||||
/// Handlers only use `Data<FederationData>` injected by the middleware layer,
|
||||
/// so the router is independent of the application state type.
|
||||
@@ -474,6 +600,7 @@ impl ActivityPubService {
|
||||
"https://{}/.well-known/webfinger?resource=acct:{}@{}",
|
||||
domain_str, user, domain_str
|
||||
);
|
||||
tracing::debug!(handle, wf_url, "resolving webfinger");
|
||||
let wf: serde_json::Value = reqwest::Client::new()
|
||||
.get(&wf_url)
|
||||
.header("Accept", "application/jrd+json, application/json")
|
||||
@@ -492,6 +619,7 @@ impl ActivityPubService {
|
||||
.and_then(|l| l["href"].as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("no self link in WebFinger response"))?
|
||||
.to_owned();
|
||||
tracing::debug!(handle, self_href, "webfinger resolved, fetching actor with signature");
|
||||
let self_url = url::Url::parse(&self_href)?;
|
||||
let actor: DbActor = ObjectId::from(self_url)
|
||||
.dereference(data)
|
||||
@@ -914,6 +1042,92 @@ impl ActivityPubService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fan out a Create(Note) activity to all accepted followers.
|
||||
/// `note` is the fully-formed Note JSON (including id, type, content, etc.).
|
||||
/// The activity ID is derived deterministically from the note's `id` field.
|
||||
pub async fn broadcast_create_note(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
note: serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let note_id_str = note["id"].as_str().unwrap_or("");
|
||||
let create_id = Url::parse(&format!(
|
||||
"{}/activities/create/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, note_id_str.as_bytes())
|
||||
))
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let create = crate::activities::CreateActivity {
|
||||
id: create_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: note,
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
bto: vec![],
|
||||
bcc: vec![],
|
||||
};
|
||||
let sends = SendActivityTask::prepare(
|
||||
&WithContext::new_default(create),
|
||||
&local_actor,
|
||||
inboxes,
|
||||
&data,
|
||||
)
|
||||
.await?;
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(count = failures.len(), "some Create(Note) deliveries failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fan out an Update(Note) activity to all accepted followers.
|
||||
/// `note` is the fully-formed Note JSON.
|
||||
pub async fn broadcast_update_note(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
note: serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let update_id = crate::urls::activity_url(&self.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let update = crate::activities::UpdateActivity {
|
||||
id: update_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: note,
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
};
|
||||
let sends = SendActivityTask::prepare(
|
||||
&WithContext::new_default(update),
|
||||
&local_actor,
|
||||
inboxes,
|
||||
&data,
|
||||
)
|
||||
.await?;
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(count = failures.len(), "some Update(Note) deliveries failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn broadcast_actor_update(&self, user_id: uuid::Uuid) -> anyhow::Result<()> {
|
||||
use activitypub_federation::traits::Object;
|
||||
|
||||
@@ -989,6 +1203,74 @@ impl ActivityPubService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Broadcast a Move activity to all accepted followers, signalling that this
|
||||
/// actor is migrating to `new_actor_url`.
|
||||
///
|
||||
/// **Pre-condition (caller's responsibility):**
|
||||
/// Before calling this, the application must persist `also_known_as = [new_actor_url]`
|
||||
/// in the local actor's row so the old actor JSON already advertises the new URL
|
||||
/// when remote servers fetch it to verify the cross-reference.
|
||||
pub async fn broadcast_move(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
new_actor_url: url::Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let followers = data.federation_repo.get_followers(user_id).await?;
|
||||
let accepted: Vec<_> = followers
|
||||
.into_iter()
|
||||
.filter(|f| f.status == FollowerStatus::Accepted)
|
||||
.collect();
|
||||
|
||||
if accepted.is_empty() {
|
||||
tracing::info!(
|
||||
%user_id,
|
||||
"broadcast_move: no accepted followers, nothing to send"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let inboxes = collect_inboxes(&accepted);
|
||||
|
||||
let move_id =
|
||||
crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let move_activity = crate::activities::MoveActivity {
|
||||
id: move_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: local_actor.ap_id.clone(),
|
||||
target: new_actor_url.clone(),
|
||||
};
|
||||
|
||||
let sends = SendActivityTask::prepare(
|
||||
&WithContext::new_default(move_activity),
|
||||
&local_actor,
|
||||
inboxes,
|
||||
&data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(
|
||||
count = failures.len(),
|
||||
"some Move deliveries failed permanently"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
%user_id,
|
||||
target = %new_actor_url,
|
||||
"broadcast_move: delivered to all accepted followers"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn block_actor(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
|
||||
18
src/user.rs
18
src/user.rs
@@ -7,6 +7,24 @@ pub struct ApProfileField {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Resolved actor data returned by [`crate::service::ActivityPubService::lookup_actor_by_handle`].
|
||||
/// Fetched via a signed HTTP request so strict instances (e.g. Threads) return full data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LookedUpActor {
|
||||
pub handle: String,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<Url>,
|
||||
pub banner_url: Option<Url>,
|
||||
pub ap_url: Url,
|
||||
pub outbox_url: Option<Url>,
|
||||
pub followers_url: Option<Url>,
|
||||
pub following_url: Option<Url>,
|
||||
pub also_known_as: Option<String>,
|
||||
pub profile_url: Option<Url>,
|
||||
pub attachment: Vec<ApProfileField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApUser {
|
||||
pub id: uuid::Uuid,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc_fingerprint":4275805307536006394,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""},"5468857489155631480":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/gabriel/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}
|
||||
@@ -1,3 +0,0 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
0f0ed912fd25f00c
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"actix-web\", \"axum\", \"default\"]","declared_features":"[\"actix-web\", \"axum\", \"axum-original-uri\", \"default\"]","target":2470822176505301216,"profile":12180060394202874717,"path":16773581499955891084,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[1528297757488249563,"url",false,2457272931386651845],[2251399859588827949,"pin_project_lite",false,6437443398776185794],[2418510928275672574,"enum_delegate",false,14178355267538590022],[2448563160050429386,"thiserror",false,5835067204734588653],[2620434475832828286,"http",false,3370793153962724875],[2658281360762422938,"activitystreams_kinds",false,16179837750537744730],[3060617276495132295,"http_signature_normalization",false,9587483780600414498],[3632162862999675140,"tower",false,12161969508488465641],[3856126590694406759,"chrono",false,9609708685167666877],[3870702314125662939,"bytes",false,15350839981501461348],[4093251733041599906,"futures",false,6991339276905393917],[4405182208873388884,"http02",false,10910201937286035227],[6219554740863759696,"derive_builder",false,14563889090776156164],[6304235478050270880,"httpdate",false,13911813288858346058],[6982418085031928086,"dyn_clone",false,8559067598306657257],[8460377374586444205,"rand",false,15289698773557955499],[8671427718380982638,"reqwest_middleware",false,9535098756087623018],[9394460649638301237,"tokio",false,16442308348071044546],[9842033052731393846,"axum",false,9394045860505783427],[9857275760291862238,"sha2",false,5635134353885060067],[12170264697963848012,"either",false,7480745390017824672],[12418418585137322150,"moka",false,17811586912375144398],[13077212702700853852,"base64",false,5049792279070600818],[13138620675979108443,"reqwest",false,1260053099221825628],[13548984313718623784,"serde",false,1154945713008201369],[13625083211600950770,"http_signature_normalization_reqwest",false,6856149699292714941],[13795362694956882968,"serde_json",false,12567268217397669682],[13944207109742299874,"actix_web",false,14372468198395249160],[14757622794040968908,"tracing",false,11349016552846746643],[16326338539882746041,"itertools",false,7555620366252786507],[16611674984963787466,"async_trait",false,5373047119521193723],[16929602831338881533,"rsa",false,11425772456796996860],[17109794424245468765,"regex",false,10618044767385116091]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/activitypub_federation-fa1ea3f12d144e43/dep-lib-activitypub_federation","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
5af141acb5548ae0
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\", \"url\"]","declared_features":"[\"default\", \"iri-string\", \"url\"]","target":13888284593456104629,"profile":2241668132362809309,"path":705792193781126522,"deps":[[1528297757488249563,"url",false,2457272931386651845],[13548984313718623784,"serde",false,1154945713008201369]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/activitystreams-kinds-57d253df96dc1d3f/dep-lib-activitystreams_kinds","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
4c8b95399043bb08
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":17825769038046261360,"profile":2241668132362809309,"path":12479565271949421205,"deps":[[270634688040536827,"futures_sink",false,1149328200498591310],[302948626015856208,"futures_core",false,12288416603081472165],[1363051979936526615,"memchr",false,585725167301201018],[2251399859588827949,"pin_project_lite",false,6437443398776185794],[2571033484697105782,"bitflags",false,248905266910842046],[3163899731817361221,"tokio_util",false,5057862522590980337],[3870702314125662939,"bytes",false,15350839981501461348],[9394460649638301237,"tokio",false,16442308348071044546],[14757622794040968908,"tracing",false,11349016552846746643]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-codec-b9379e254aa41bb3/dep-lib-actix_codec","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
a0821e877283e60e
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\"]","declared_features":"[\"__compress\", \"__tls\", \"actix-tls\", \"compress-brotli\", \"compress-gzip\", \"compress-zstd\", \"default\", \"http2\", \"openssl\", \"rustls\", \"rustls-0_20\", \"rustls-0_21\", \"rustls-0_22\", \"rustls-0_23\", \"ws\"]","target":4427038891525048573,"profile":3133228388854823247,"path":12642938007707680944,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[595566797399950287,"derive_more",false,6271040502569514124],[2251399859588827949,"pin_project_lite",false,6437443398776185794],[2464271856383924494,"bytestring",false,11928832580233448895],[2571033484697105782,"bitflags",false,248905266910842046],[3064692270587553479,"actix_service",false,6066366771012098175],[3163899731817361221,"tokio_util",false,5057862522590980337],[3666196340704888985,"smallvec",false,13942546746312191476],[3870702314125662939,"bytes",false,15350839981501461348],[4405182208873388884,"http",false,10910201937286035227],[5384016313853579615,"actix_utils",false,14800976629038891558],[5532778797167691009,"itoa",false,6186202510566694238],[6163892036024256188,"httparse",false,1037811929920180245],[6304235478050270880,"httpdate",false,13911813288858346058],[6803352382179706244,"percent_encoding",false,15274310794087272089],[9394460649638301237,"tokio",false,16442308348071044546],[10229185211513642314,"mime",false,11227694820619906685],[10842263908529601448,"foldhash",false,16121307840052673950],[11094608732914737535,"actix_rt",false,13757417807091910916],[13648953096965186997,"actix_codec",false,629170859668769612],[14564311161534545801,"encoding_rs",false,9086530780560730121],[14757622794040968908,"tracing",false,11349016552846746643],[17331556883491080683,"language_tags",false,4130686828052123393]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-http-78e7a91a2b9559b2/dep-lib-actix_http","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
5182b754d86b5fb5
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"http\"]","declared_features":"[\"default\", \"http\", \"unicode\"]","target":5816441226683462542,"profile":3133228388854823247,"path":13778785067889628751,"deps":[[2464271856383924494,"bytestring",false,11928832580233448895],[4405182208873388884,"http",false,10910201937286035227],[7667230146095136825,"cfg_if",false,8246195043185055782],[7758745775150479896,"regex_lite",false,8085648085395873201],[13548984313718623784,"serde",false,1154945713008201369],[14757622794040968908,"tracing",false,11349016552846746643]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-router-c8ce26c22dbba3d1/dep-lib-actix_router","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
04c9580a9b2aecbe
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"actix-macros\", \"default\", \"io-uring\", \"macros\", \"tokio-uring\"]","target":11467906722111896043,"profile":3906840514083873863,"path":2147250437741273592,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[9394460649638301237,"tokio",false,16442308348071044546]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-rt-652c9c260ecace1d/dep-lib-actix_rt","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
0312225b64784954
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\"]","declared_features":"[\"default\", \"io-uring\", \"tokio-uring\"]","target":7486425883630722659,"profile":18362114993302267858,"path":7185016881975598547,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[3064692270587553479,"actix_service",false,6066366771012098175],[5384016313853579615,"actix_utils",false,14800976629038891558],[5675930438384443948,"mio",false,1339801658642837885],[5898568623609459682,"futures_util",false,7974877432333173807],[9394460649638301237,"tokio",false,16442308348071044546],[11094608732914737535,"actix_rt",false,13757417807091910916],[12614995553916589825,"socket2",false,9840711553693307500],[14757622794040968908,"tracing",false,11349016552846746643]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-server-2b59da478fcd73e4/dep-lib-actix_server","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
7fd868ef6f103054
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":15098614942180125221,"profile":18362114993302267858,"path":11943216201661577318,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[2251399859588827949,"pin_project_lite",false,6437443398776185794]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-service-873a607d058b9f05/dep-lib-actix_service","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
26c2b83ed3a167cd
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":10635421866110932485,"profile":2241668132362809309,"path":10635128780053389363,"deps":[[2083946343206318420,"local_waker",false,4285014003435318770],[2251399859588827949,"pin_project_lite",false,6437443398776185794]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-utils-25eec20101fe5d0d/dep-lib-actix_utils","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
0882f286ad4375c7
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"__compress\", \"__tls\", \"actix-tls\", \"compat\", \"compat-routing-macros-force-pub\", \"compress-brotli\", \"compress-gzip\", \"compress-zstd\", \"cookies\", \"default\", \"experimental-introspection\", \"experimental-io-uring\", \"http2\", \"macros\", \"openssl\", \"rustls\", \"rustls-0_20\", \"rustls-0_21\", \"rustls-0_22\", \"rustls-0_23\", \"secure-cookies\", \"unicode\", \"ws\"]","target":10874021801110526175,"profile":3133228388854823247,"path":5343881741294641425,"deps":[[302948626015856208,"futures_core",false,12288416603081472165],[595566797399950287,"derive_more",false,6271040502569514124],[1528297757488249563,"url",false,2457272931386651845],[2251399859588827949,"pin_project_lite",false,6437443398776185794],[2464271856383924494,"bytestring",false,11928832580233448895],[3014460723014940652,"actix_server",false,6073517944906846723],[3064692270587553479,"actix_service",false,6066366771012098175],[3666196340704888985,"smallvec",false,13942546746312191476],[3870702314125662939,"bytes",false,15350839981501461348],[5384016313853579615,"actix_utils",false,14800976629038891558],[5532778797167691009,"itoa",false,6186202510566694238],[5855319743879205494,"once_cell",false,9317269918508772267],[5898568623609459682,"futures_util",false,7974877432333173807],[7667230146095136825,"cfg_if",false,8246195043185055782],[7758745775150479896,"regex_lite",false,8085648085395873201],[8763336154670875101,"actix_http",false,1073690089090876064],[10229185211513642314,"mime",false,11227694820619906685],[10630857666389190470,"log",false,11810301620611768972],[10842263908529601448,"foldhash",false,16121307840052673950],[10947645248417156337,"socket2",false,18403540095509011799],[11094608732914737535,"actix_rt",false,13757417807091910916],[11432222519274906849,"time",false,1609409879086680227],[13548984313718623784,"serde",false,1154945713008201369],[13648953096965186997,"actix_codec",false,629170859668769612],[13795362694956882968,"serde_json",false,12567268217397669682],[14373001148831857156,"impl_more",false,3335858555249608201],[14564311161534545801,"encoding_rs",false,9086530780560730121],[14757622794040968908,"tracing",false,11349016552846746643],[16542808166767769916,"serde_urlencoded",false,13156382631774393973],[17331556883491080683,"language_tags",false,4130686828052123393],[17584815051554192320,"actix_router",false,13069283220530889297]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/actix-web-c9b0df37d5768de0/dep-lib-actix_web","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
@@ -1 +0,0 @@
|
||||
383efe19efc3481a
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[12478428894219133322,"build_script_build",false,14636711343951660640]],"local":[{"RerunIfChanged":{"output":"debug/build/anyhow-0f55cb3fae6edabf/output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":0,"compile_kind":0}
|
||||
@@ -1 +0,0 @@
|
||||
60462b306b0b20cb
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":5408242616063297496,"profile":2225463790103693989,"path":17547525999849753218,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-17d01bb684fba012/dep-build-script-build-script-build","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
bc7700438333a93d
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":1563897884725121975,"profile":2241668132362809309,"path":8136882599531902006,"deps":[[12478428894219133322,"build_script_build",false,1893979075009986104]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-531636fb6b8d8f5d/dep-lib-anyhow","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
e4fc0d182a1b7231
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"std\"]","target":4686383084901058664,"profile":13827760451848848284,"path":5120213175319746872,"deps":[[2251399859588827949,"pin_project_lite",false,6437443398776185794],[14474722528862052230,"event_listener",false,18099563584703664452],[17148897597675491682,"event_listener_strategy",false,6164869924565250032]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-lock-41eae0b9af02062d/dep-lib-async_lock","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
fb1e7f38e3e5904a
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":5116616278641129243,"profile":2225463790103693989,"path":4752685656130913642,"deps":[[4289358735036141001,"proc_macro2",false,14802044410649402508],[10420560437213941093,"syn",false,15790606275185302303],[13111758008314797071,"quote",false,16857015384950168359]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-trait-c48a4b7d28a0b527/dep-lib-async_trait","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
1f1ff49729d450a2
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":2241668132362809309,"path":4245238797433841345,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-3e62ee2eb28c1783/dep-lib-atomic_waker","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
246e81328321f7ff
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":2225463790103693989,"path":8797102772932910657,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-a70cfd1ea272cf40/dep-lib-autocfg","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
72c8fe2e8a2bc7a7
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"aws-lc-sys\", \"prebuilt-nasm\"]","declared_features":"[\"alloc\", \"asan\", \"aws-lc-sys\", \"bindgen\", \"default\", \"dev-tests-only\", \"fips\", \"legacy-des\", \"non-fips\", \"prebuilt-nasm\", \"ring-io\", \"ring-sig-verify\", \"test_logging\", \"unstable\"]","target":18300691495230371829,"profile":2241668132362809309,"path":14360813989608357860,"deps":[[7886471800061524671,"build_script_build",false,3054087683372998411],[12857944478329125325,"aws_lc_sys",false,16732228847163352266],[12865141776541797048,"zeroize",false,6369300111375613971]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aws-lc-rs-52228dec93f2da8c/dep-lib-aws_lc_rs","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
@@ -1 +0,0 @@
|
||||
d5dc985783dd009d
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"aws-lc-sys\", \"prebuilt-nasm\"]","declared_features":"[\"alloc\", \"asan\", \"aws-lc-sys\", \"bindgen\", \"default\", \"dev-tests-only\", \"fips\", \"legacy-des\", \"non-fips\", \"prebuilt-nasm\", \"ring-io\", \"ring-sig-verify\", \"test_logging\", \"unstable\"]","target":5408242616063297496,"profile":2225463790103693989,"path":2311100187817589231,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aws-lc-rs-6935a5ed7254cbca/dep-build-script-build-script-build","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
0bdf5718914c622a
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[7886471800061524671,"build_script_build",false,11313285820134776021],[12857944478329125325,"build_script_main",false,4461329072602765900]],"local":[{"RerunIfEnvChanged":{"var":"AWS_LC_RS_DISABLE_SLOW_TESTS","val":null}},{"RerunIfEnvChanged":{"var":"AWS_LC_RS_DEV_TESTS_ONLY","val":null}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":0,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
ca24b73f7cd134e8
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"prebuilt-nasm\"]","declared_features":"[\"all-bindings\", \"asan\", \"bindgen\", \"default\", \"disable-prebuilt-nasm\", \"fips\", \"prebuilt-nasm\", \"ssl\"]","target":9251307146641742440,"profile":2241668132362809309,"path":5276624135659922443,"deps":[[12857944478329125325,"build_script_main",false,4461329072602765900]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aws-lc-sys-2d4f378da38c6005/dep-lib-aws_lc_sys","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
@@ -1 +0,0 @@
|
||||
c20d09cfcf0add2b
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":7458672600737419911,"features":"[\"prebuilt-nasm\"]","declared_features":"[\"all-bindings\", \"asan\", \"bindgen\", \"default\", \"disable-prebuilt-nasm\", \"fips\", \"prebuilt-nasm\", \"ssl\"]","target":10419965325687163515,"profile":2225463790103693989,"path":16638629239944049461,"deps":[[6778462791484060249,"cmake",false,13996189653408809957],[9957297873626937772,"cc",false,3955065759738468941],[11989259058781683633,"dunce",false,1171461063385170067],[13866570822711233627,"fs_extra",false,11003487061889747544]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aws-lc-sys-3af20d4781754ddc/dep-build-script-build-script-main","checksum":false}}],"rustflags":["-C","link-arg=-fuse-ld=mold"],"config":9185878174080762935,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
4c5a9c8627d3e93d
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user