feat: first pass at mastoapi stuff

This commit is contained in:
nullishamy 2025-04-12 11:27:03 +01:00
parent ce3a9bfb26
commit 244cb8b7e6
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
13 changed files with 368 additions and 83 deletions

View file

@ -47,6 +47,34 @@ impl User {
&self.actor &self.actor
} }
pub async fn from_id(
uuid: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>,
) -> User {
let user = sqlx::query!(
r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox
FROM user u
INNER JOIN actor a ON u.actor_id = a.id
WHERE u.id = ?1
"#,
uuid
)
.fetch_one(conn)
.await
.unwrap();
User {
id: user.id,
username: user.username,
actor: Actor {
id: user.actor_own_id,
inbox: user.inbox,
outbox: user.outbox,
},
display_name: user.display_name,
}
}
pub async fn from_username( pub async fn from_username(
username: &str, username: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>, conn: impl sqlx::Executor<'_, Database = Sqlite>,

View file

@ -4,12 +4,15 @@ use rocket::{
FromForm, State, FromForm, State,
form::Form, form::Form,
post, post,
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize, json::Json},
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use uuid::Uuid; use uuid::Uuid;
use crate::timeline::TimelineStatus;
use crate::{AuthenticatedUser, Db, types::content}; use crate::{AuthenticatedUser, Db, types::content};
use crate::api::user::CredentialAcount;
#[derive(Serialize, Deserialize, Debug, FromForm)] #[derive(Serialize, Deserialize, Debug, FromForm)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Status { pub struct Status {
@ -26,7 +29,7 @@ pub async fn new_status(
let user = ap::User::from_actor_id(&user.actor_id, &mut **db).await; let user = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
let outbox = ap::Outbox::for_user(user.clone(), http); let outbox = ap::Outbox::for_user(user.clone(), http);
let post_id = Uuid::new_v4(); let post_id = Uuid::new_v4().to_string();
let uri = format!( let uri = format!(
"https://ferri.amy.mov/users/{}/posts/{}", "https://ferri.amy.mov/users/{}/posts/{}",
@ -38,10 +41,11 @@ pub async fn new_status(
let post = sqlx::query!( let post = sqlx::query!(
r#" r#"
INSERT INTO post (id, user_id, content, created_at) INSERT INTO post (id, uri, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4) VALUES (?1, ?2, ?3, ?4, ?5)
RETURNING * RETURNING *
"#, "#,
post_id,
uri, uri,
id, id,
status.status, status.status,
@ -102,3 +106,137 @@ pub async fn new_status(
outbox.post(req).await; outbox.post(req).await;
} }
} }
#[post("/statuses", data = "<status>", rank = 2)]
pub async fn new_status_json(
mut db: Connection<Db>,
http: &State<HttpClient>,
status: Json<Status>,
user: AuthenticatedUser,
) -> Json<TimelineStatus> {
dbg!(&user);
let user = ap::User::from_id(&user.username, &mut **db).await;
let outbox = ap::Outbox::for_user(user.clone(), http);
let post_id = Uuid::new_v4().to_string();
let uri = format!(
"https://ferri.amy.mov/users/{}/posts/{}",
user.id(),
post_id
);
let id = user.id();
let now = Local::now().to_rfc3339();
let post = sqlx::query!(
r#"
INSERT INTO post (id, uri, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
RETURNING *
"#,
post_id,
uri,
id,
status.status,
now
)
.fetch_one(&mut **db)
.await
.unwrap();
let actors = sqlx::query!("SELECT * FROM actor")
.fetch_all(&mut **db)
.await
.unwrap();
for record in actors {
// Don't send to ourselves
if &record.id == user.actor_id() {
continue
}
let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4());
let activity = ap::Activity {
id: create_id,
ty: ap::ActivityType::Create,
object: content::Post {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: uri.clone(),
content: status.status.clone(),
ty: "Note".to_string(),
ts: Local::now().to_rfc3339(),
to: vec![format!(
"https://ferri.amy.mov/users/{}/followers",
user.username()
)],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
},
to: vec![format!(
"https://ferri.amy.mov/users/{}/followers",
user.username()
)],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
..Default::default()
};
let actor = ap::Actor::from_raw(
record.id.clone(),
record.inbox.clone(),
record.outbox.clone(),
);
let req = ap::OutgoingActivity {
req: activity,
signed_by: format!("https://ferri.amy.mov/users/{}#main-key", user.username()),
to: actor,
};
req.save(&mut **db).await;
outbox.post(req).await;
}
let user_uri = format!(
"https://ferri.amy.mov/users/{}",
user.id(),
);
Json(TimelineStatus {
id: post.id.clone(),
created_at: post.created_at.clone(),
in_reply_to_id: None,
in_reply_to_account_id: None,
content: post.content.clone(),
visibility: "public".to_string(),
spoiler_text: "".to_string(),
sensitive: false,
uri: post.uri.clone(),
url: post.uri.clone(),
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
favourited: false,
reblogged: false,
muted: false,
bookmarked: false,
media_attachments: vec![],
account: CredentialAcount {
id: user.id().to_string(),
username: user.username().to_string(),
acct: user.username().to_string(),
display_name: user.display_name().to_string(),
locked: false,
bot: false,
created_at: "2025-04-10T22:12:09Z".to_string(),
attribution_domains: vec![],
note: "".to_string(),
url: user_uri,
avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
followers_count: 1,
following_count: 1,
statuses_count: 1,
last_status_at: "2025-04-10T22:14:34Z".to_string(),
}
})
}

View file

@ -10,32 +10,33 @@ pub type TimelineAccount = CredentialAcount;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct TimelineStatus { pub struct TimelineStatus {
id: String, pub id: String,
created_at: String, pub created_at: String,
in_reply_to_id: Option<String>, pub in_reply_to_id: Option<String>,
in_reply_to_account_id: Option<String>, pub in_reply_to_account_id: Option<String>,
content: String, pub content: String,
visibility: String, pub visibility: String,
spoiler_text: String, pub spoiler_text: String,
sensitive: bool, pub sensitive: bool,
uri: String, pub uri: String,
url: String, pub url: String,
replies_count: i64, pub replies_count: i64,
reblogs_count: i64, pub reblogs_count: i64,
favourites_count: i64, pub favourites_count: i64,
favourited: bool, pub favourited: bool,
reblogged: bool, pub reblogged: bool,
muted: bool, pub muted: bool,
bookmarked: bool, pub bookmarked: bool,
media_attachments: Vec<()>, pub media_attachments: Vec<()>,
account: TimelineAccount, pub account: TimelineAccount,
} }
#[get("/timelines/home?<limit>")] #[get("/timelines/home?<limit>")]
pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus>> { pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus>> {
let posts = sqlx::query!( let posts = sqlx::query!(
r#" r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, u.username, u.display_name, u.actor_id FROM post p SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri", u.username, u.display_name, u.actor_id, p.created_at
FROM post p
INNER JOIN user u on p.user_id = u.id INNER JOIN user u on p.user_id = u.id
"# "#
) )
@ -45,17 +46,18 @@ pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus
let mut out = Vec::<TimelineStatus>::new(); let mut out = Vec::<TimelineStatus>::new();
for record in posts { for record in posts {
let user_uri = format!("https://ferri.amy.mov/users/{}", record.username);
out.push(TimelineStatus { out.push(TimelineStatus {
id: record.post_id.clone(), id: record.post_id.clone(),
created_at: "2025-04-10T22:12:09Z".to_string(), created_at: record.created_at.clone(),
in_reply_to_id: None, in_reply_to_id: None,
in_reply_to_account_id: None, in_reply_to_account_id: None,
content: record.content.clone(), content: record.content.clone(),
visibility: "public".to_string(), visibility: "public".to_string(),
spoiler_text: "".to_string(), spoiler_text: "".to_string(),
sensitive: false, sensitive: false,
uri: record.post_id.clone(), uri: record.post_uri.clone(),
url: record.post_id.clone(), url: record.post_uri.clone(),
replies_count: 0, replies_count: 0,
reblogs_count: 0, reblogs_count: 0,
favourites_count: 0, favourites_count: 0,
@ -65,7 +67,7 @@ pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus
bookmarked: false, bookmarked: false,
media_attachments: vec![], media_attachments: vec![],
account: CredentialAcount { account: CredentialAcount {
id: record.actor_id.clone(), id: record.user_id.clone(),
username: record.username.clone(), username: record.username.clone(),
acct: record.username.clone(), acct: record.username.clone(),
display_name: record.display_name.clone(), display_name: record.display_name.clone(),
@ -74,7 +76,7 @@ pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus
created_at: "2025-04-10T22:12:09Z".to_string(), created_at: "2025-04-10T22:12:09Z".to_string(),
attribution_domains: vec![], attribution_domains: vec![],
note: "".to_string(), note: "".to_string(),
url: record.actor_id.clone(), url: user_uri,
avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(), avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://ferri.amy.mov/assets/pfp.png".to_string(), header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
@ -86,6 +88,6 @@ pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus
}, },
}); });
} }
Json(out) Json(out)
} }

View file

@ -7,6 +7,7 @@ use rocket_db_pools::Connection;
use uuid::Uuid; use uuid::Uuid;
use crate::{AuthenticatedUser, Db, http::HttpClient}; use crate::{AuthenticatedUser, Db, http::HttpClient};
use crate::timeline::{TimelineStatus, TimelineAccount};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -44,10 +45,10 @@ pub async fn verify_credentials() -> Json<CredentialAcount> {
attribution_domains: vec![], attribution_domains: vec![],
note: "".to_string(), note: "".to_string(),
url: "https://ferri.amy.mov/@amy".to_string(), url: "https://ferri.amy.mov/@amy".to_string(),
avatar: "https://i.sstatic.net/l60Hf.png".to_string(), avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://i.sstatic.net/l60Hf.png".to_string(), avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://i.sstatic.net/l60Hf.png".to_string(), header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header_static: "https://i.sstatic.net/l60Hf.png".to_string(), header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
followers_count: 1, followers_count: 1,
following_count: 1, following_count: 1,
statuses_count: 1, statuses_count: 1,
@ -55,15 +56,15 @@ pub async fn verify_credentials() -> Json<CredentialAcount> {
}) })
} }
#[post("/accounts/<account>/follow")] #[post("/accounts/<uuid>/follow")]
pub async fn new_follow( pub async fn new_follow(
mut db: Connection<Db>, mut db: Connection<Db>,
http: &State<HttpClient>, http: &State<HttpClient>,
account: &str, uuid: &str,
user: AuthenticatedUser, user: AuthenticatedUser,
) { ) {
let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await; let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
let followed = ap::User::from_username(account, &mut **db).await; let followed = ap::User::from_id(uuid, &mut **db).await;
let outbox = ap::Outbox::for_user(follower.clone(), http.inner()); let outbox = ap::Outbox::for_user(follower.clone(), http.inner());
@ -86,3 +87,98 @@ pub async fn new_follow(
req.save(&mut **db).await; req.save(&mut **db).await;
outbox.post(req).await; outbox.post(req).await;
} }
#[get("/accounts/<uuid>")]
pub async fn account(mut db: Connection<Db>, uuid: &str, user: AuthenticatedUser) -> Json<TimelineAccount> {
let user = ap::User::from_id(uuid, &mut **db).await;
let user_uri = format!("https://ferri.amy.mov/users/{}", user.username());
Json(CredentialAcount {
id: user.id().to_string(),
username: user.username().to_string(),
acct: user.username().to_string(),
display_name: user.display_name().to_string(),
locked: false,
bot: false,
created_at: "2025-04-10T22:12:09Z".to_string(),
attribution_domains: vec![],
note: "".to_string(),
url: user_uri,
avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
followers_count: 1,
following_count: 1,
statuses_count: 1,
last_status_at: "2025-04-10T22:14:34Z".to_string(),
})
}
#[get("/accounts/<uuid>/statuses?<limit>")]
pub async fn statuses(
mut db: Connection<Db>,
uuid: &str,
limit: Option<i64>,
user: AuthenticatedUser,
) -> Json<Vec<TimelineStatus>> {
let user = ap::User::from_id(uuid, &mut **db).await;
let uid = user.id();
let posts = sqlx::query!(
r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri", u.username, u.display_name, u.actor_id, p.created_at
FROM post p
INNER JOIN user u on p.user_id = u.id
WHERE u.id = ?1
"#, uid)
.fetch_all(&mut **db)
.await
.unwrap();
let mut out = Vec::<TimelineStatus>::new();
for record in posts {
let user_uri = format!("https://ferri.amy.mov/users/{}", record.username);
out.push(TimelineStatus {
id: record.post_id.clone(),
created_at: record.created_at.clone(),
in_reply_to_id: None,
in_reply_to_account_id: None,
content: record.content.clone(),
visibility: "public".to_string(),
spoiler_text: "".to_string(),
sensitive: false,
uri: record.post_uri.clone(),
url: record.post_uri.clone(),
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
favourited: false,
reblogged: false,
muted: false,
bookmarked: false,
media_attachments: vec![],
account: CredentialAcount {
id: record.user_id.clone(),
username: record.username.clone(),
acct: record.username.clone(),
display_name: record.display_name.clone(),
locked: false,
bot: false,
created_at: "2025-04-10T22:12:09Z".to_string(),
attribution_domains: vec![],
note: "".to_string(),
url: user_uri,
avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
followers_count: 1,
following_count: 1,
statuses_count: 1,
last_status_at: "2025-04-10T22:14:34Z".to_string(),
},
});
}
Json(out)
}

View file

@ -111,6 +111,13 @@ async fn handle_follow_activity(followed_account: String, activity: activity::Fo
outbox.post(req).await; outbox.post(req).await;
} }
async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection<Db>) {
let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object)
.fetch_one(&mut **db)
.await.unwrap();
dbg!(&target_post);
}
async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpClient, mut db: Connection<Db>) { async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpClient, mut db: Connection<Db>) {
assert!(&activity.object.ty == "Note"); assert!(&activity.object.ty == "Note");
let user = http let user = http
@ -128,21 +135,17 @@ async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpCl
let user = ap::User::from_actor_id(&activity.actor, &mut **db).await; let user = ap::User::from_actor_id(&activity.actor, &mut **db).await;
let post_id = Uuid::new_v4(); let user_id = user.id();
let uri = format!(
"https://ferri.amy.mov/users/{}/posts/{}",
user.username(),
post_id
);
let id = user.id();
let now = Local::now().to_rfc3339(); let now = Local::now().to_rfc3339();
let content = activity.object.content.clone(); let content = activity.object.content.clone();
let post_id = Uuid::new_v4().to_string();
let uri = activity.id;
sqlx::query!(r#" sqlx::query!(r#"
INSERT INTO post (id, user_id, content, created_at) INSERT INTO post (id, uri, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4) VALUES (?1, ?2, ?3, ?4, ?5)
"#, uri, id, content, now) "#, post_id, uri, user_id, content, now)
.execute(&mut **db) .execute(&mut **db)
.await.unwrap(); .await.unwrap();
} }
@ -163,6 +166,10 @@ pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: String, b
let activity = serde_json::from_str::<activity::CreateActivity>(&body).unwrap(); let activity = serde_json::from_str::<activity::CreateActivity>(&body).unwrap();
handle_create_activity(activity, http.inner(), db).await; handle_create_activity(activity, http.inner(), db).await;
}, },
"Like" => {
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap();
handle_like_activity(activity, db).await;
},
unknown => { unknown => {
eprintln!("WARN: Unknown activity '{}' - {}", unknown, body); eprintln!("WARN: Unknown activity '{}' - {}", unknown, body);
} }

View file

@ -26,7 +26,7 @@ pub struct Token {
#[post("/oauth/token")] #[post("/oauth/token")]
pub async fn new_token() -> Json<Token> { pub async fn new_token() -> Json<Token> {
Json(Token { Json(Token {
access_token: "access-token".to_string(), access_token: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(),
token_type: "Bearer".to_string(), token_type: "Bearer".to_string(),
expires_in: 3600, expires_in: 3600,
scope: "read write follow push".to_string(), scope: "read write follow push".to_string(),

View file

@ -31,9 +31,9 @@ pub async fn outbox(user: String) -> Json<OrderedCollection> {
}) })
} }
#[get("/users/<user>/followers")] #[get("/users/<uuid>/followers")]
pub async fn followers(mut db: Connection<Db>, user: String) -> Json<OrderedCollection> { pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollection> {
let target = ap::User::from_username(&user, &mut **db).await; let target = ap::User::from_id(uuid, &mut **db).await;
let actor_id = target.actor_id(); let actor_id = target.actor_id();
let followers = sqlx::query!( let followers = sqlx::query!(
@ -49,7 +49,7 @@ pub async fn followers(mut db: Connection<Db>, user: String) -> Json<OrderedColl
Json(OrderedCollection { Json(OrderedCollection {
ty: "OrderedCollection".to_string(), ty: "OrderedCollection".to_string(),
summary: format!("Followers for {}", user), summary: format!("Followers for {}", uuid),
total_items: 1, total_items: 1,
ordered_items: followers ordered_items: followers
.into_iter() .into_iter()
@ -58,9 +58,9 @@ pub async fn followers(mut db: Connection<Db>, user: String) -> Json<OrderedColl
}) })
} }
#[get("/users/<user>/following")] #[get("/users/<uuid>/following")]
pub async fn following(mut db: Connection<Db>, user: String) -> Json<OrderedCollection> { pub async fn following(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollection> {
let target = ap::User::from_username(&user, &mut **db).await; let target = ap::User::from_id(uuid, &mut **db).await;
let actor_id = target.actor_id(); let actor_id = target.actor_id();
let following = sqlx::query!( let following = sqlx::query!(
@ -76,7 +76,7 @@ pub async fn following(mut db: Connection<Db>, user: String) -> Json<OrderedColl
Json(OrderedCollection { Json(OrderedCollection {
ty: "OrderedCollection".to_string(), ty: "OrderedCollection".to_string(),
summary: format!("Following for {}", user), summary: format!("Following for {}", uuid),
total_items: 1, total_items: 1,
ordered_items: following ordered_items: following
.into_iter() .into_iter()
@ -85,40 +85,47 @@ pub async fn following(mut db: Connection<Db>, user: String) -> Json<OrderedColl
}) })
} }
#[get("/users/<user>/posts/<post>")] #[get("/users/<uuid>/posts/<post>")]
pub async fn post(user: String, post: String) -> (ContentType, Json<content::Post>) { pub async fn post(mut db: Connection<Db>, uuid: &str, post: String) -> (ContentType, Json<content::Post>) {
let post = sqlx::query!(r#"
SELECT * FROM post WHERE id = ?1
"#, post)
.fetch_one(&mut **db)
.await.unwrap();
( (
activity_type(), activity_type(),
Json(content::Post { Json(content::Post {
id: format!("https://ferri.amy.mov/users/{}/posts/{}", user, post),
context: "https://www.w3.org/ns/activitystreams".to_string(), context: "https://www.w3.org/ns/activitystreams".to_string(),
id: format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id),
ty: "Note".to_string(), ty: "Note".to_string(),
content: "My first post".to_string(), content: post.content,
ts: "2025-04-10T10:48:11Z".to_string(), ts: post.created_at,
to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()], to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
}), }),
) )
} }
#[get("/users/<user>")] #[get("/users/<uuid>")]
pub async fn user(user: String) -> (ContentType, Json<Person>) { pub async fn user(mut db: Connection<Db>, uuid: &str) -> (ContentType, Json<Person>) {
let user = ap::User::from_id(uuid, &mut **db).await;
( (
activity_type(), activity_type(),
Json(Person { Json(Person {
context: "https://www.w3.org/ns/activitystreams".to_string(), context: "https://www.w3.org/ns/activitystreams".to_string(),
ty: "Person".to_string(), ty: "Person".to_string(),
id: format!("https://ferri.amy.mov/users/{}", user), id: user.id().to_string(),
name: user.clone(), name: user.username().to_string(),
preferred_username: user.clone(), preferred_username: user.display_name().to_string(),
followers: format!("https://ferri.amy.mov/users/{}/followers", user), followers: format!("https://ferri.amy.mov/users/{}/followers", uuid),
following: format!("https://ferri.amy.mov/users/{}/following", user), following: format!("https://ferri.amy.mov/users/{}/following", uuid),
summary: format!("ferri {}", user), summary: format!("ferri {}", user.username()),
inbox: format!("https://ferri.amy.mov/users/{}/inbox", user), inbox: format!("https://ferri.amy.mov/users/{}/inbox", uuid),
outbox: format!("https://ferri.amy.mov/users/{}/outbox", user), outbox: format!("https://ferri.amy.mov/users/{}/outbox", uuid),
public_key: Some(UserKey { public_key: Some(UserKey {
id: format!("https://ferri.amy.mov/users/{}#main-key", user), id: format!("https://ferri.amy.mov/users/{}#main-key", uuid),
owner: format!("https://ferri.amy.mov/users/{}", user), owner: format!("https://ferri.amy.mov/users/{}", uuid),
public_key: include_str!("../../../public.pem").to_string(), public_key: include_str!("../../../public.pem").to_string(),
}), }),
}), }),

View file

@ -25,7 +25,7 @@ pub async fn webfinger(mut db: Connection<Db>, resource: &str) -> Json<Webfinger
Json(WebfingerResponse { Json(WebfingerResponse {
subject: resource.to_string(), subject: resource.to_string(),
aliases: vec![ aliases: vec![
format!("https://ferri.amy.mov/users/{}", user.username()), format!("https://ferri.amy.mov/users/{}", user.id()),
format!("https://ferri.amy.mov/@{}", user.username()), format!("https://ferri.amy.mov/@{}", user.username()),
], ],
links: vec![ links: vec![
@ -37,8 +37,8 @@ pub async fn webfinger(mut db: Connection<Db>, resource: &str) -> Json<Webfinger
Link { Link {
rel: "self".to_string(), rel: "self".to_string(),
ty: Some("application/activity+json".to_string()), ty: Some("application/activity+json".to_string()),
href: Some(format!("https://ferri.amy.mov/users/{}", user.username())), href: Some(format!("https://ferri.amy.mov/users/{}", user.id())),
}, },
], ],
}) })
} }

View file

@ -41,6 +41,7 @@ impl<'a> FromRequest<'a> for AuthenticatedUser {
type Error = LoginError; type Error = LoginError;
async fn from_request(request: &'a Request<'_>) -> Outcome<AuthenticatedUser, LoginError> { async fn from_request(request: &'a Request<'_>) -> Outcome<AuthenticatedUser, LoginError> {
let token = request.headers().get_one("Authorization").unwrap(); let token = request.headers().get_one("Authorization").unwrap();
let token = token.strip_prefix("Bearer").map(|s| s.trim()).unwrap_or(token);
Outcome::Success(AuthenticatedUser { Outcome::Success(AuthenticatedUser {
username: token.to_string(), username: token.to_string(),
actor_id: format!("https://ferri.amy.mov/users/{}", token) actor_id: format!("https://ferri.amy.mov/users/{}", token)
@ -80,7 +81,10 @@ pub fn launch() -> Rocket<Build> {
"/api/v1", "/api/v1",
routes![ routes![
api::status::new_status, api::status::new_status,
api::status::new_status_json,
api::user::new_follow, api::user::new_follow,
api::user::statuses,
api::user::account,
api::apps::new_app, api::apps::new_app,
api::preferences::preferences, api::preferences::preferences,
api::user::verify_credentials, api::user::verify_credentials,

View file

@ -10,9 +10,12 @@ pub struct MinimalActivity {
pub ty: String, pub ty: String,
} }
pub type DeleteActivity = BasicActivity;
pub type LikeActivity = BasicActivity;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct DeleteActivity { pub struct BasicActivity {
pub id: String, pub id: String,
#[serde(rename = "type")] #[serde(rename = "type")]
pub ty: String, pub ty: String,
@ -56,4 +59,4 @@ pub struct AcceptActivity {
pub object: String, pub object: String,
pub actor: String, pub actor: String,
} }

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS actor CREATE TABLE IF NOT EXISTS actor
( (
-- URI -- URI
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
inbox TEXT NOT NULL, inbox TEXT NOT NULL,
outbox TEXT NOT NULL outbox TEXT NOT NULL
); );

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS post CREATE TABLE IF NOT EXISTS post
( (
-- Uri
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
uri TEXT NOT NULL,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,

View file

@ -2,8 +2,8 @@ CREATE TABLE IF NOT EXISTS activity
( (
-- UUID -- UUID
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
ty TEXT NOT NULL, ty TEXT NOT NULL,
actor_id TEXT NOT NULL, actor_id TEXT NOT NULL,
FOREIGN KEY(actor_id) REFERENCES actor(id) FOREIGN KEY(actor_id) REFERENCES actor(id)
); );