1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! HTTP API responders for ActivityPub objects, plus some endpoints specific to
//! this server (like a registration endpoint, [`users::register`]).
//!
//! ActivityPub endpoints:
//! - POSTs to inboxes (`/u/<user>/inbox.json`): [`users::post_inbox`]
//! - POSTs to outboxes (`/u/<user>/outbox.json`): [`users::post_outbox`]
//! - GETs to any local ActivityPub id: [`get_ap_object`]
//!
//! WebFinger endpoints:
//! - `/.well-known/webfinger`: [`webfinger::get`]
//!
//! Other:
//! - Registration endpoint (POST to `/u/<user>`): [`users::register`]
//!
//! This module also contains the Activity Streams Content-Type and Accept
//! guards, and the media type itself, in [`activity_json`].

use crate::database::Database;
use crate::snowflake;
use actix_web::http::Uri;
use actix_web::{web, HttpResponse};
use serde::{Deserialize, Serialize};

pub mod activity_json;
pub mod users;
pub mod webfinger;

/// A string containing the base url for the running server. This is the url
/// used in ActivityPub id's created by this server.
#[derive(Clone)]
pub struct CanonicalServerUrl(pub String);

impl CanonicalServerUrl {
    // Returns the part of the base url after "https://", which should be the
    // domain, and possibly a port.
    pub fn host(&self) -> &str {
        self.0.split_once("://").unwrap().1
    }
}

/// Returns the id of an actor named `username` on this server.
pub fn actor_id(base_url: &CanonicalServerUrl, username: &str) -> String {
    format!("{}/u/{username}", base_url.0)
}

/// Returns a new ActivityPub id for activities created by the given actor.
pub fn activity_id(actor_id: &str) -> String {
    let id = snowflake::generate_snowflake();
    format!("{actor_id}/activities/{id}")
}

/// Returns a new ActivityPub id for notes created by the given actor.
pub fn note_id(actor_id: &str) -> String {
    let id = snowflake::generate_snowflake();
    format!("{actor_id}/notes/{id}")
}

/// Returns the ActivityPub id for a CollectionPage belonging to the given
/// Collection, with the given page number.
pub fn collection_page_id(collection_id: &str, page: u32) -> String {
    format!("{collection_id}?page={page}")
}

/// Query parameters for queries fetching ActivityPub objects by their id.
#[derive(Serialize, Deserialize)]
pub struct ApRetrievalParams {
    page: Option<u32>,
}

/// Http responder for GETs to ActivityPub ids.
///
/// Combines the path and query from `uri` to the base url in `server_url` to
/// create an id, then attempts to fetch it from the db. Returns a 404 if
/// there's no such object, and a 503 if the database query fails (generally
/// caused by database downtime).
pub async fn get_ap_object(
    uri: Uri,
    db: web::Data<&'static Database>,
    server_url: web::Data<CanonicalServerUrl>,
    query: web::Query<ApRetrievalParams>,
) -> HttpResponse {
    if let Some(path_and_query) = uri.path_and_query() {
        tracing::trace!("Looking up: {}{}", server_url.0, path_and_query);
    }
    let id = format!("{}{}", server_url.0, uri.path());
    match db.get_object(&id, query.page).await {
        Ok(Some(obj)) => activity_json::response(obj),
        Ok(None) => HttpResponse::NotFound().finish(),
        Err(err) => {
            tracing::error!("Failed to get activitypub object from db: {err}");
            HttpResponse::ServiceUnavailable().finish()
        }
    }
}