mirror of
https://github.com/nullishamy/ferri.git
synced 2025-04-30 04:39:20 +00:00
feat: first pass at mastoapi stuff
This commit is contained in:
parent
ce3a9bfb26
commit
244cb8b7e6
13 changed files with 368 additions and 83 deletions
|
@ -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>,
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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,7 +37,7 @@ 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())),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue