fix: misc cleanup

This commit is contained in:
nullishamy 2025-05-06 18:36:31 +01:00
parent 90be7d570e
commit fafaf243c5
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
13 changed files with 640 additions and 750 deletions

View file

@ -1,3 +1,4 @@
use main::types::{db, make, ObjectUri, ObjectUuid};
use server::launch; use server::launch;
extern crate rocket; extern crate rocket;
@ -24,6 +25,10 @@ pub fn read_config(path: impl AsRef<Path>) -> config::Config {
toml::from_str(&content).unwrap() toml::from_str(&content).unwrap()
} }
fn s(st: &'static str) -> String {
st.to_string()
}
#[rocket::main] #[rocket::main]
async fn main() { async fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
@ -36,42 +41,30 @@ async fn main() {
.unwrap(); .unwrap();
let mut conn = pool.acquire().await.unwrap(); let mut conn = pool.acquire().await.unwrap();
sqlx::query!( let actor = db::Actor {
r#" id: ObjectUri(s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9")),
INSERT INTO actor (id, inbox, outbox) inbox: s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/inbox"),
VALUES (?1, ?2, ?3) outbox: s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/outbox")
"#, };
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9",
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/inbox",
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/outbox"
)
.execute(&mut *conn)
.await
.unwrap();
let ts = main::ap::new_ts(); make::new_actor(actor.clone(), &mut *conn).await.unwrap();
sqlx::query!( let user = db::User {
r#" id: ObjectUuid(s("9b9d497b-2731-435f-a929-e609ca69dac9")),
INSERT INTO user ( actor,
id, acct, url, remote, username, username: s("amy"),
actor_id, display_name, created_at, icon_url display_name: s("amy (display)"),
) acct: s("amy"),
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) remote: false,
"#, url: s("https://ferri.amy.mov/@amy"),
"9b9d497b-2731-435f-a929-e609ca69dac9", created_at: main::ap::now(),
"amy", icon_url: s("https://ferri.amy.mov/assets/pfp.png"),
"https://ferri.amy.mov/@amy", posts: db::UserPosts {
false, last_post_at: None
"amy", }
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9", };
"amy",
ts, make::new_user(user, &mut *conn).await.unwrap();
"https://ferri.amy.mov/assets/pfp.png"
)
.execute(&mut *conn)
.await
.unwrap();
} else { } else {
let _ = launch(config).launch().await; let _ = launch(config).launch().await;
} }

223
ferri-main/src/types/ap.rs Normal file
View file

@ -0,0 +1,223 @@
use super::*;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum ActivityType {
Reject,
Create,
Note,
Delete,
Undo,
Accept,
Announce,
Person,
Like,
Follow,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MinimalActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
}
pub type DeleteActivity = BasicActivity;
pub type LikeActivity = BasicActivity;
#[derive(Serialize, Deserialize, Debug)]
pub struct BasicActivity {
#[serde(flatten)]
pub obj: Object,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: Post,
pub actor: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "published")]
pub ts: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FollowActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AcceptActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BoostActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub actor: String,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub object: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum PostAttachmentType {
Document
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PostAttachment {
#[serde(rename = "type")]
pub ty: PostAttachmentType,
pub media_type: String,
pub url: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub name: String,
pub summary: Option<String>,
#[serde(default)]
pub sensitive: bool
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Post {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
#[serde(rename = "published")]
pub ts: String,
pub content: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub attachment: Vec<PostAttachment>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Actor {
#[serde(flatten)]
pub obj: Object,
pub inbox: String,
pub outbox: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum IconType {
Image
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct PersonIcon {
#[serde(rename = "type")]
pub ty: IconType,
pub url: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub width: i64,
#[serde(default)]
pub height: i64
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub following: String,
pub followers: String,
pub summary: String,
pub inbox: String,
pub outbox: String,
pub preferred_username: String,
pub name: String,
pub public_key: Option<UserKey>,
pub icon: Option<PersonIcon>
}
pub struct RemoteInfo {
pub is_remote: bool,
pub web_url: String,
pub acct: String
}
impl Person {
pub fn remote_info(&self) -> RemoteInfo {
let url = Url::parse(&self.obj.id.0).unwrap();
let host = url.host_str().unwrap();
let (acct, remote) = if host != "ferri.amy.mov" {
(format!("{}@{}", self.preferred_username, host), true)
} else {
(self.preferred_username.clone(), false)
};
let url = format!("https://ferri.amy.mov/{}", acct);
RemoteInfo {
acct: acct.to_string(),
web_url: url,
is_remote: remote,
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct UserKey {
pub id: String,
pub owner: String,
#[serde(rename = "publicKeyPem")]
pub public_key: String,
}

231
ferri-main/src/types/api.rs Normal file
View file

@ -0,0 +1,231 @@
use super::*;
use serde::{Deserialize, Serialize};
// API will not really use actors so treat them as DB actors
// until we require specificity
pub type Actor = db::Actor;
#[derive(Serialize, Deserialize, Debug)]
pub struct CredentialApplication {
pub name: String,
pub scopes: String,
pub redirect_uris: Vec<String>,
pub client_id: String,
pub client_secret: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerLink {
pub rel: String,
#[serde(rename = "type")]
pub ty: Option<String>,
pub href: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerHit {
pub subject: String,
pub aliases: Vec<String>,
pub links: Vec<WebfingerLink>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct StatusAttachment {
pub id: ObjectUuid,
#[serde(rename = "type")]
pub ty: String,
pub url: String,
pub description: String
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Status {
pub id: ObjectUuid,
pub created_at: String,
pub in_reply_to_id: Option<ObjectUri>,
pub in_reply_to_account_id: Option<ObjectUri>,
pub sensitive: bool,
pub spoiler_text: String,
pub visibility: String,
pub language: String,
pub uri: ObjectUri,
pub url: String,
pub replies_count: i64,
pub reblogs_count: i64,
pub favourites_count: i64,
pub favourited: bool,
pub reblogged: bool,
pub muted: bool,
pub bookmarked: bool,
pub content: String,
pub reblog: Option<Box<Status>>,
pub application: Option<()>,
pub account: Account,
pub media_attachments: Vec<StatusAttachment>,
pub mentions: Vec<Option<()>>,
pub tags: Vec<Option<()>>,
pub emojis: Vec<Option<()>>,
pub card: Option<()>,
pub poll: Option<()>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Account {
pub id: ObjectUuid,
pub username: String,
pub acct: String,
pub display_name: String,
pub locked: bool,
pub bot: bool,
pub created_at: String,
pub attribution_domains: Vec<String>,
pub note: String,
pub url: String,
pub avatar: String,
pub avatar_static: String,
pub header: String,
pub header_static: String,
pub followers_count: i64,
pub following_count: i64,
pub statuses_count: i64,
pub last_status_at: Option<String>,
pub emojis: Vec<Emoji>,
pub fields: Vec<CustomField>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Emoji {
pub shortcode: String,
pub url: String,
pub static_url: String,
pub visible_in_picker: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct CustomField {
pub name: String,
pub value: String,
pub verified_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Instance {
pub domain: String,
pub title: String,
pub version: String,
pub source_url: String,
pub description: String,
pub thumbnail: Thumbnail,
pub icon: Vec<Icon>,
pub languages: Vec<String>,
pub configuration: Configuration,
pub registrations: Registrations,
pub contact: Contact,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub urls: Urls,
pub accounts: Accounts,
pub statuses: Statuses,
pub media_attachments: MediaAttachments,
pub polls: Polls,
pub translation: Translation,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Accounts {
pub max_featured_tags: i64,
pub max_pinned_statuses: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MediaAttachments {
pub supported_mime_types: Vec<String>,
pub description_limit: i64,
pub image_size_limit: i64,
pub image_matrix_limit: i64,
pub video_size_limit: i64,
pub video_frame_rate_limit: i64,
pub video_matrix_limit: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Polls {
pub max_options: i64,
pub max_characters_per_option: i64,
pub min_expiration: i64,
pub max_expiration: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Statuses {
pub max_characters: i64,
pub max_media_attachments: i64,
pub characters_reserved_per_url: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Translation {
pub enabled: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Urls {
pub streaming: String,
pub about: String,
pub privacy_policy: String,
pub terms_of_service: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Contact {
pub email: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Field {
pub name: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Icon {
pub src: String,
pub size: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Registrations {
pub enabled: bool,
pub approval_required: bool,
pub reason_required: bool,
pub message: Option<String>,
pub min_age: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Thumbnail {
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct Preferences {
#[serde(rename = "posting:default:visibility")]
pub posting_default_visibility: String,
#[serde(rename = "posting:default:sensitive")]
pub posting_default_sensitive: bool,
#[serde(rename = "posting:default:language")]
pub posting_default_language: Option<String>,
#[serde(rename = "reading:expand:media")]
pub reading_expand_media: String,
#[serde(rename = "reading:expand:spoilers")]
pub reading_expand_spoilers: bool,
}

View file

@ -0,0 +1,59 @@
use super::*;
use chrono::{DateTime, Utc};
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Follow {
pub id: ObjectUri,
pub follower: ObjectUri,
pub followed: ObjectUri,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Actor {
pub id: ObjectUri,
pub inbox: String,
pub outbox: String,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct UserPosts {
// User may have no posts
pub last_post_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct User {
pub id: ObjectUuid,
pub actor: Actor,
pub username: String,
pub display_name: String,
pub acct: String,
pub remote: bool,
pub url: String,
pub created_at: DateTime<Utc>,
pub icon_url: String,
pub posts: UserPosts,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Attachment {
pub id: ObjectUuid,
pub post_id: ObjectUuid,
pub url: String,
pub media_type: Option<String>,
pub sensitive: bool,
pub alt: Option<String>
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Post {
pub id: ObjectUuid,
pub uri: ObjectUri,
pub user: User,
pub content: String,
pub created_at: DateTime<Utc>,
pub boosted_post: Option<Box<Post>>,
pub attachments: Vec<Attachment>
}

View file

@ -7,6 +7,10 @@ pub mod convert;
pub mod get; pub mod get;
pub mod make; pub mod make;
pub mod db;
pub mod ap;
pub mod api;
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error> fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where where
T: Default + Deserialize<'de>, T: Default + Deserialize<'de>,
@ -67,540 +71,3 @@ pub struct Object {
pub context: ObjectContext, pub context: ObjectContext,
pub id: ObjectUri, pub id: ObjectUri,
} }
pub mod db {
use super::*;
use chrono::{DateTime, Utc};
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Follow {
pub id: ObjectUri,
pub follower: ObjectUri,
pub followed: ObjectUri,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Actor {
pub id: ObjectUri,
pub inbox: String,
pub outbox: String,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct UserPosts {
// User may have no posts
pub last_post_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct User {
pub id: ObjectUuid,
pub actor: Actor,
pub username: String,
pub display_name: String,
pub acct: String,
pub remote: bool,
pub url: String,
pub created_at: DateTime<Utc>,
pub icon_url: String,
pub posts: UserPosts,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Attachment {
pub id: ObjectUuid,
pub post_id: ObjectUuid,
pub url: String,
pub media_type: Option<String>,
pub sensitive: bool,
pub alt: Option<String>
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Post {
pub id: ObjectUuid,
pub uri: ObjectUri,
pub user: User,
pub content: String,
pub created_at: DateTime<Utc>,
pub boosted_post: Option<Box<Post>>,
pub attachments: Vec<Attachment>
}
}
pub mod ap {
use super::*;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum ActivityType {
Reject,
Create,
Note,
Delete,
Undo,
Accept,
Announce,
Person,
Like,
Follow,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MinimalActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
}
pub type DeleteActivity = BasicActivity;
pub type LikeActivity = BasicActivity;
#[derive(Serialize, Deserialize, Debug)]
pub struct BasicActivity {
#[serde(flatten)]
pub obj: Object,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: Post,
pub actor: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "published")]
pub ts: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FollowActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AcceptActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BoostActivity {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub actor: String,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub object: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum PostAttachmentType {
Document
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PostAttachment {
#[serde(rename = "type")]
pub ty: PostAttachmentType,
pub media_type: String,
pub url: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub name: String,
pub summary: Option<String>,
#[serde(default)]
pub sensitive: bool
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Post {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
#[serde(rename = "published")]
pub ts: String,
pub content: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub attachment: Vec<PostAttachment>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Actor {
#[serde(flatten)]
pub obj: Object,
pub inbox: String,
pub outbox: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum IconType {
Image
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct PersonIcon {
#[serde(rename = "type")]
pub ty: IconType,
pub url: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub width: i64,
#[serde(default)]
pub height: i64
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(flatten)]
pub obj: Object,
#[serde(rename = "type")]
pub ty: ActivityType,
pub following: String,
pub followers: String,
pub summary: String,
pub inbox: String,
pub outbox: String,
pub preferred_username: String,
pub name: String,
pub public_key: Option<UserKey>,
pub icon: Option<PersonIcon>
}
pub struct RemoteInfo {
pub is_remote: bool,
pub web_url: String,
pub acct: String
}
impl Person {
pub fn remote_info(&self) -> RemoteInfo {
let url = Url::parse(&self.obj.id.0).unwrap();
let host = url.host_str().unwrap();
let (acct, remote) = if host != "ferri.amy.mov" {
(format!("{}@{}", self.preferred_username, host), true)
} else {
(self.preferred_username.clone(), false)
};
let url = format!("https://ferri.amy.mov/{}", acct);
RemoteInfo {
acct: acct.to_string(),
web_url: url,
is_remote: remote,
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct UserKey {
pub id: String,
pub owner: String,
#[serde(rename = "publicKeyPem")]
pub public_key: String,
}
}
pub mod api {
use super::*;
use serde::{Deserialize, Serialize};
// API will not really use actors so treat them as DB actors
// until we require specificity
pub type Actor = db::Actor;
#[derive(Serialize, Deserialize, Debug)]
pub struct CredentialApplication {
pub name: String,
pub scopes: String,
pub redirect_uris: Vec<String>,
pub client_id: String,
pub client_secret: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerLink {
pub rel: String,
#[serde(rename = "type")]
pub ty: Option<String>,
pub href: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerHit {
pub subject: String,
pub aliases: Vec<String>,
pub links: Vec<WebfingerLink>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct StatusAttachment {
pub id: ObjectUuid,
#[serde(rename = "type")]
pub ty: String,
pub url: String,
pub description: String
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Status {
pub id: ObjectUuid,
pub created_at: String,
pub in_reply_to_id: Option<ObjectUri>,
pub in_reply_to_account_id: Option<ObjectUri>,
pub sensitive: bool,
pub spoiler_text: String,
pub visibility: String,
pub language: String,
pub uri: ObjectUri,
pub url: String,
pub replies_count: i64,
pub reblogs_count: i64,
pub favourites_count: i64,
pub favourited: bool,
pub reblogged: bool,
pub muted: bool,
pub bookmarked: bool,
pub content: String,
pub reblog: Option<Box<Status>>,
pub application: Option<()>,
pub account: Account,
pub media_attachments: Vec<StatusAttachment>,
pub mentions: Vec<Option<()>>,
pub tags: Vec<Option<()>>,
pub emojis: Vec<Option<()>>,
pub card: Option<()>,
pub poll: Option<()>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Account {
pub id: ObjectUuid,
pub username: String,
pub acct: String,
pub display_name: String,
pub locked: bool,
pub bot: bool,
pub created_at: String,
pub attribution_domains: Vec<String>,
pub note: String,
pub url: String,
pub avatar: String,
pub avatar_static: String,
pub header: String,
pub header_static: String,
pub followers_count: i64,
pub following_count: i64,
pub statuses_count: i64,
pub last_status_at: Option<String>,
pub emojis: Vec<Emoji>,
pub fields: Vec<CustomField>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Emoji {
pub shortcode: String,
pub url: String,
pub static_url: String,
pub visible_in_picker: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct CustomField {
pub name: String,
pub value: String,
pub verified_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Instance {
pub domain: String,
pub title: String,
pub version: String,
pub source_url: String,
pub description: String,
pub thumbnail: Thumbnail,
pub icon: Vec<Icon>,
pub languages: Vec<String>,
pub configuration: Configuration,
pub registrations: Registrations,
pub contact: Contact,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub urls: Urls,
pub accounts: Accounts,
pub statuses: Statuses,
pub media_attachments: MediaAttachments,
pub polls: Polls,
pub translation: Translation,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Accounts {
pub max_featured_tags: i64,
pub max_pinned_statuses: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MediaAttachments {
pub supported_mime_types: Vec<String>,
pub description_limit: i64,
pub image_size_limit: i64,
pub image_matrix_limit: i64,
pub video_size_limit: i64,
pub video_frame_rate_limit: i64,
pub video_matrix_limit: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Polls {
pub max_options: i64,
pub max_characters_per_option: i64,
pub min_expiration: i64,
pub max_expiration: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Statuses {
pub max_characters: i64,
pub max_media_attachments: i64,
pub characters_reserved_per_url: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Translation {
pub enabled: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Urls {
pub streaming: String,
pub about: String,
pub privacy_policy: String,
pub terms_of_service: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Contact {
pub email: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Field {
pub name: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Icon {
pub src: String,
pub size: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Registrations {
pub enabled: bool,
pub approval_required: bool,
pub reason_required: bool,
pub message: Option<String>,
pub min_age: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Thumbnail {
pub url: String,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ap_actor_to_db() {
let domain = "https://example.com";
let ap = ap::Actor {
obj: Object {
context: as_context(),
id: ObjectUri(format!("{}/users/sample", domain)),
},
inbox: format!("{}/users/sample/inbox", domain),
outbox: format!("{}/users/sample/outbox", domain),
};
let db: db::Actor = ap.into();
assert_eq!(
db,
db::Actor {
id: ObjectUri("https://example.com/users/sample".to_string()),
inbox: "https://example.com/users/sample/inbox".to_string(),
outbox: "https://example.com/users/sample/outbox".to_string(),
}
);
}
}

View file

@ -1,20 +1,14 @@
use rocket::{get, post, response::content::RawHtml}; use rocket::{get, response::content::RawHtml};
use askama::Template; use askama::Template;
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate { struct IndexTemplate {
val: String
}
#[post("/clicked")]
pub async fn button_clicked() -> RawHtml<String> {
let tmpl = IndexTemplate { val: "clicked".to_string() };
RawHtml(tmpl.render().unwrap())
} }
#[get("/")] #[get("/")]
pub async fn index() -> RawHtml<String> { pub async fn index() -> RawHtml<String> {
let tmpl = IndexTemplate { val: "test".to_string() }; let tmpl = IndexTemplate { };
RawHtml(tmpl.render().unwrap()) RawHtml(tmpl.render().unwrap())
} }

View file

@ -1,26 +1,12 @@
use main::types::api;
use rocket::{ use rocket::{
get, get,
serde::{Deserialize, Serialize, json::Json}, serde::json::Json,
}; };
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct Preferences {
#[serde(rename = "posting:default:visibility")]
pub posting_default_visibility: String,
#[serde(rename = "posting:default:sensitive")]
pub posting_default_sensitive: bool,
#[serde(rename = "posting:default:language")]
pub posting_default_language: Option<String>,
#[serde(rename = "reading:expand:media")]
pub reading_expand_media: String,
#[serde(rename = "reading:expand:spoilers")]
pub reading_expand_spoilers: bool,
}
#[get("/preferences")] #[get("/preferences")]
pub async fn preferences() -> Json<Preferences> { pub async fn preferences() -> Json<api::Preferences> {
Json(Preferences { Json(api::Preferences {
posting_default_visibility: "public".to_string(), posting_default_visibility: "public".to_string(),
posting_default_sensitive: false, posting_default_sensitive: false,
posting_default_language: None, posting_default_language: None,

View file

@ -1,48 +1,11 @@
use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount}; use crate::{AuthenticatedUser, Db};
use main::types::{api, get, ObjectUuid}; use main::types::{api, get};
use rocket::{ use rocket::{
get, get,
serde::{Deserialize, Serialize, json::Json}, serde::json::Json,
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
pub type TimelineAccount = CredentialAcount;
#[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TimelineStatusAttachment {
id: ObjectUuid,
#[serde(rename = "type")]
ty: String,
url: String,
description: String
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TimelineStatus {
pub id: String,
pub created_at: String,
pub in_reply_to_id: Option<String>,
pub in_reply_to_account_id: Option<String>,
pub content: String,
pub visibility: String,
pub spoiler_text: String,
pub sensitive: bool,
pub uri: String,
pub url: String,
pub replies_count: i64,
pub reblogs_count: i64,
pub favourites_count: i64,
pub favourited: bool,
pub reblogged: bool,
pub muted: bool,
pub bookmarked: bool,
pub reblog: Option<Box<TimelineStatus>>,
pub media_attachments: Vec<TimelineStatusAttachment>,
pub account: TimelineAccount,
}
#[get("/timelines/home")] #[get("/timelines/home")]
pub async fn home( pub async fn home(
mut db: Connection<Db>, mut db: Connection<Db>,
@ -50,7 +13,10 @@ pub async fn home(
) -> Json<Vec<api::Status>> { ) -> Json<Vec<api::Status>> {
let posts = get::home_timeline(user.actor_id, &mut **db) let posts = get::home_timeline(user.actor_id, &mut **db)
.await .await
.unwrap(); .unwrap()
.into_iter()
.map(|p| p.into())
.collect();
Json(posts.into_iter().map(Into::into).collect()) Json(posts)
} }

View file

@ -1,4 +1,5 @@
use main::ap; use main::ap;
use main::types::{api, get, ObjectUuid};
use rocket::response::status::NotFound; use rocket::response::status::NotFound;
use rocket::{ use rocket::{
State, get, post, State, get, post,
@ -6,8 +7,8 @@ use rocket::{
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use uuid::Uuid; use uuid::Uuid;
use tracing::info;
use crate::timeline::{TimelineAccount, TimelineStatus};
use crate::{AuthenticatedUser, Db}; use crate::{AuthenticatedUser, Db};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -34,7 +35,8 @@ pub struct CredentialAcount {
} }
#[get("/accounts/verify_credentials")] #[get("/accounts/verify_credentials")]
pub async fn verify_credentials() -> Json<CredentialAcount> { pub async fn verify_credentials(user: AuthenticatedUser) -> Json<CredentialAcount> {
info!("verifying creds for {:#?}", user);
Json(CredentialAcount { Json(CredentialAcount {
id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(), id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(),
username: "amy".to_string(), username: "amy".to_string(),
@ -98,31 +100,12 @@ pub async fn account(
mut db: Connection<Db>, mut db: Connection<Db>,
uuid: &str, uuid: &str,
_user: AuthenticatedUser, _user: AuthenticatedUser,
) -> Result<Json<TimelineAccount>, NotFound<String>> { ) -> Result<Json<api::Account>, NotFound<String>> {
let user = ap::User::from_id(uuid, &mut **db) let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db)
.await .await
.map_err(|e| NotFound(e.to_string()))?; .map_err(|e| NotFound(e.to_string()))?;
let user_uri = format!("https://ferri.amy.mov/users/{}", user.username());
Ok(Json(CredentialAcount { Ok(Json(user.into()))
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>")] #[get("/accounts/<uuid>/statuses?<_limit>")]
@ -131,69 +114,17 @@ pub async fn statuses(
uuid: &str, uuid: &str,
_limit: Option<i64>, _limit: Option<i64>,
_user: AuthenticatedUser, _user: AuthenticatedUser,
) -> Result<Json<Vec<TimelineStatus>>, NotFound<String>> { ) -> Result<Json<Vec<api::Status>>, NotFound<String>> {
let user = ap::User::from_id(uuid, &mut **db) let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db)
.await .await
.map_err(|e| NotFound(e.to_string()))?; .map_err(|e| NotFound(e.to_string()))?;
let uid = user.id(); let posts = get::posts_for_user_id(user.id, &mut **db)
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
ORDER BY p.created_at DESC
"#, uid)
.fetch_all(&mut **db)
.await .await
.unwrap(); .unwrap()
.into_iter()
.map(|p| p.into())
.collect();
let mut out = Vec::<TimelineStatus>::new(); Ok(Json(posts))
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,
reblog: None,
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(),
},
});
}
Ok(Json(out))
} }

View file

@ -22,10 +22,6 @@ impl<'a> HttpWrapper<'a> {
Self { client, key_id } Self { client, key_id }
} }
pub fn client(&self) -> &'a HttpClient {
self.client
}
async fn get<T: serde::de::DeserializeOwned + Debug>( async fn get<T: serde::de::DeserializeOwned + Debug>(
&self, &self,
ty: &str, ty: &str,

View file

@ -134,7 +134,6 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
"/admin", "/admin",
routes![ routes![
admin::index, admin::index,
admin::button_clicked
] ]
) )
.mount( .mount(

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="https://ferri.amy.mov/admin/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}{% endblock %}</title>
{%~ block styles ~%} {% endblock ~%}
<link href="https://cdn.jsdelivr.net/npm/tailwindcss-preflight@1.0.1/preflight.min.css" rel="stylesheet" />
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
{%~ block content %}{% endblock ~%}
</body>
</html>

View file

@ -1,19 +1,45 @@
<!DOCTYPE html> {% extends "_layout.html" %}
<html lang="en">
<head>
<base href="https://ferri.amy.mov/admin/">
<meta charset="UTF-8"> {%- block title -%}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> Control panel
<meta http-equiv="X-UA-Compatible" content="ie=edge"> {%- endblock -%}
<title>Ferri Test</title>
<link rel="stylesheet" href="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/dbc8023a08964f513c20796e170cb91ce891df3f/packages/tailwindcss/preflight.css"> {%- block styles -%}
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <style>
</head> main {
<body> display: grid;
<button hx-post="clicked" hx-swap="outerHTML"> grid-template-columns: repeat(2, 1fr);
Click Me {{ val }} grid-gap: 1rem;
</button> padding: 1rem;
</body> margin: auto;
</html> max-width: 75%;
margin: auto;
}
.grid-section {
border: 1px black solid;
padding: 0.5rem;
border-radius: 0.5rem;
}
</style>
{%- endblock -%}
{%- block content -%}
<main>
<div class='grid-section'>
<h2>Test</h2>
</div>
<div class='grid-section'>
<h2>Test</h2>
</div>
<div class='grid-section'>
<h2>Test</h2>
</div>
<div class='grid-section'>
<h2>Test</h2>
</div>
</main>
{%- endblock -%}