mirror of
https://github.com/nullishamy/ferri.git
synced 2025-04-30 04:39:20 +00:00
chore: tidy up ID usage; fmt
This commit is contained in:
parent
244cb8b7e6
commit
2b62948447
27 changed files with 603 additions and 483 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1264,6 +1264,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -28,9 +28,9 @@ async fn main() {
|
||||||
INSERT INTO actor (id, inbox, outbox)
|
INSERT INTO actor (id, inbox, outbox)
|
||||||
VALUES (?1, ?2, ?3)
|
VALUES (?1, ?2, ?3)
|
||||||
"#,
|
"#,
|
||||||
"https://ferri.amy.mov/users/amy",
|
"https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff",
|
||||||
"https://ferri.amy.mov/users/amy/inbox",
|
"https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff/inbox",
|
||||||
"https://ferri.amy.mov/users/amy/outbox"
|
"https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff/outbox"
|
||||||
)
|
)
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
.await
|
.await
|
||||||
|
@ -43,7 +43,7 @@ async fn main() {
|
||||||
"#,
|
"#,
|
||||||
"9b9d497b-2731-435f-a929-e609ca69dac9",
|
"9b9d497b-2731-435f-a929-e609ca69dac9",
|
||||||
"amy",
|
"amy",
|
||||||
"https://ferri.amy.mov/users/amy",
|
"https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff",
|
||||||
"amy"
|
"amy"
|
||||||
)
|
)
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
|
|
|
@ -9,6 +9,7 @@ serde_json = "1.0.140"
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rsa = { version = "0.9.8", features = ["sha2"] }
|
rsa = { version = "0.9.8", features = ["sha2"] }
|
||||||
|
|
136
ferri-main/src/ap/activity.rs
Normal file
136
ferri-main/src/ap/activity.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
use crate::ap::{Actor, User, http};
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::Sqlite;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ActivityType {
|
||||||
|
Follow,
|
||||||
|
Accept,
|
||||||
|
Create,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivityType {
|
||||||
|
fn to_raw(self) -> String {
|
||||||
|
match self {
|
||||||
|
ActivityType::Follow => "Follow".to_string(),
|
||||||
|
ActivityType::Accept => "Accept".to_string(),
|
||||||
|
ActivityType::Create => "Create".to_string(),
|
||||||
|
ActivityType::Unknown => "FIXME".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Activity<T: Serialize + Debug> {
|
||||||
|
pub id: String,
|
||||||
|
pub ty: ActivityType,
|
||||||
|
pub object: T,
|
||||||
|
pub published: DateTime<Local>,
|
||||||
|
pub to: Vec<String>,
|
||||||
|
pub cc: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize + Debug + Default> Default for Activity<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Default::default(),
|
||||||
|
ty: ActivityType::Unknown,
|
||||||
|
object: Default::default(),
|
||||||
|
published: Local::now(),
|
||||||
|
to: Default::default(),
|
||||||
|
cc: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type KeyId = String;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OutgoingActivity<T: Serialize + Debug> {
|
||||||
|
pub signed_by: KeyId,
|
||||||
|
pub req: Activity<T>,
|
||||||
|
pub to: Actor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize + Debug> OutgoingActivity<T> {
|
||||||
|
pub async fn save(&self, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
||||||
|
let ty = self.req.ty.clone().to_raw();
|
||||||
|
let actor_id = self.to.id();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO activity (id, ty, actor_id)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
"#,
|
||||||
|
self.req.id,
|
||||||
|
ty,
|
||||||
|
actor_id
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct RawActivity<T: Serialize + Debug> {
|
||||||
|
#[serde(rename = "@context")]
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
context: String,
|
||||||
|
|
||||||
|
id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
ty: String,
|
||||||
|
|
||||||
|
actor: String,
|
||||||
|
object: T,
|
||||||
|
published: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboxTransport = http::HttpClient;
|
||||||
|
pub struct Outbox<'a> {
|
||||||
|
user: User,
|
||||||
|
transport: &'a OutboxTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Outbox<'a> {
|
||||||
|
pub fn user(&self) -> &User {
|
||||||
|
&self.user
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post<T: Serialize + Debug>(&self, activity: OutgoingActivity<T>) {
|
||||||
|
dbg!(&activity);
|
||||||
|
let raw = RawActivity {
|
||||||
|
context: "https://www.w3.org/ns/activitystreams".to_string(),
|
||||||
|
id: activity.req.id,
|
||||||
|
ty: activity.req.ty.to_raw(),
|
||||||
|
actor: self.user.actor().id().to_string(),
|
||||||
|
object: activity.req.object,
|
||||||
|
published: activity.req.published.to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
dbg!(&raw);
|
||||||
|
|
||||||
|
let follow_res = self
|
||||||
|
.transport
|
||||||
|
.post(activity.to.inbox())
|
||||||
|
.activity()
|
||||||
|
.json(&raw)
|
||||||
|
.sign(&activity.signed_by)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
dbg!(follow_res);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> {
|
||||||
|
Outbox { user, transport }
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,8 @@ impl RequestBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activity(mut self) -> RequestBuilder {
|
pub fn activity(mut self) -> RequestBuilder {
|
||||||
self.inner = self.inner
|
self.inner = self
|
||||||
|
.inner
|
||||||
.header("Content-Type", "application/activity+json")
|
.header("Content-Type", "application/activity+json")
|
||||||
.header("Accept", "application/activity+json");
|
.header("Accept", "application/activity+json");
|
||||||
self
|
self
|
||||||
|
@ -66,14 +67,16 @@ impl RequestBuilder {
|
||||||
match self.verb {
|
match self.verb {
|
||||||
RequestVerb::GET => {
|
RequestVerb::GET => {
|
||||||
let sig = self.sign_get_request(key_id);
|
let sig = self.sign_get_request(key_id);
|
||||||
self.inner = self.inner
|
self.inner = self
|
||||||
|
.inner
|
||||||
.header("Date", sig.date)
|
.header("Date", sig.date)
|
||||||
.header("Signature", sig.signature);
|
.header("Signature", sig.signature);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
RequestVerb::POST => {
|
RequestVerb::POST => {
|
||||||
let sig = self.sign_post_request(key_id);
|
let sig = self.sign_post_request(key_id);
|
||||||
self.inner = self.inner
|
self.inner = self
|
||||||
|
.inner
|
||||||
.header("Date", sig.date)
|
.header("Date", sig.date)
|
||||||
.header("Digest", sig.digest)
|
.header("Digest", sig.digest)
|
||||||
.header("Signature", sig.signature);
|
.header("Signature", sig.signature);
|
||||||
|
@ -87,7 +90,8 @@ impl RequestBuilder {
|
||||||
let host = url.host_str().unwrap();
|
let host = url.host_str().unwrap();
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
|
|
||||||
let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
|
let private_key =
|
||||||
|
RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
|
||||||
let signing_key = SigningKey::<Sha256>::new(private_key);
|
let signing_key = SigningKey::<Sha256>::new(private_key);
|
||||||
|
|
||||||
// UTC=GMT for our purposes, use it
|
// UTC=GMT for our purposes, use it
|
||||||
|
@ -122,7 +126,8 @@ impl RequestBuilder {
|
||||||
let host = url.host_str().unwrap();
|
let host = url.host_str().unwrap();
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
|
|
||||||
let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
|
let private_key =
|
||||||
|
RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
|
||||||
let signing_key = SigningKey::<Sha256>::new(private_key);
|
let signing_key = SigningKey::<Sha256>::new(private_key);
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
|
|
@ -1,262 +1,27 @@
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use uuid::Uuid;
|
||||||
use sqlx::Sqlite;
|
|
||||||
|
|
||||||
use std::fmt::Debug;
|
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
mod activity;
|
||||||
pub struct Actor {
|
pub use activity::*;
|
||||||
id: String,
|
|
||||||
inbox: String,
|
mod user;
|
||||||
outbox: String,
|
pub use user::*;
|
||||||
|
|
||||||
|
mod post;
|
||||||
|
pub use post::*;
|
||||||
|
|
||||||
|
pub const AS_CONTEXT: &'static str = "https://www.w3.org/ns/activitystreams";
|
||||||
|
|
||||||
|
pub fn new_id() -> String {
|
||||||
|
Uuid::new_v4().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor {
|
pub fn new_ts() -> String {
|
||||||
pub fn from_raw(id: String, inbox: String, outbox: String) -> Self {
|
now().to_rfc3339()
|
||||||
Self { id, inbox, outbox }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub fn now() -> DateTime<Utc> {
|
||||||
pub struct User {
|
Utc::now()
|
||||||
id: String,
|
|
||||||
username: String,
|
|
||||||
actor: Actor,
|
|
||||||
display_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
pub fn id(&self) -> &str {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
|
||||||
&self.username
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn actor_id(&self) -> &str {
|
|
||||||
&self.actor.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_name(&self) -> &str {
|
|
||||||
&self.display_name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn actor(&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(
|
|
||||||
username: &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 username = ?1
|
|
||||||
"#,
|
|
||||||
username
|
|
||||||
)
|
|
||||||
.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_actor_id(
|
|
||||||
actor_id: &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 actor_id = ?1
|
|
||||||
"#,
|
|
||||||
actor_id
|
|
||||||
)
|
|
||||||
.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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ActivityType {
|
|
||||||
Follow,
|
|
||||||
Accept,
|
|
||||||
Create,
|
|
||||||
Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActivityType {
|
|
||||||
fn to_raw(self) -> String {
|
|
||||||
match self {
|
|
||||||
ActivityType::Follow => "Follow".to_string(),
|
|
||||||
ActivityType::Accept => "Accept".to_string(),
|
|
||||||
ActivityType::Create => "Create".to_string(),
|
|
||||||
ActivityType::Unknown => "FIXME".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Activity<T : Serialize + Debug> {
|
|
||||||
pub id: String,
|
|
||||||
pub ty: ActivityType,
|
|
||||||
pub object: T,
|
|
||||||
pub published: DateTime<Local>,
|
|
||||||
pub to: Vec<String>,
|
|
||||||
pub cc: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <T : Serialize + Debug + Default> Default for Activity<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Default::default(),
|
|
||||||
ty: ActivityType::Unknown,
|
|
||||||
object: Default::default(),
|
|
||||||
published: Local::now(),
|
|
||||||
to: Default::default(),
|
|
||||||
cc: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type KeyId = String;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct OutgoingActivity<T : Serialize + Debug> {
|
|
||||||
pub signed_by: KeyId,
|
|
||||||
pub req: Activity<T>,
|
|
||||||
pub to: Actor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <T : Serialize + Debug> OutgoingActivity<T> {
|
|
||||||
pub async fn save(&self, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
|
||||||
let ty = self.req.ty.clone().to_raw();
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO activity (id, ty, actor_id)
|
|
||||||
VALUES (?1, ?2, ?3)
|
|
||||||
"#,
|
|
||||||
self.req.id,
|
|
||||||
ty,
|
|
||||||
self.to.id
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct RawActivity<T : Serialize + Debug> {
|
|
||||||
#[serde(rename = "@context")]
|
|
||||||
#[serde(skip_deserializing)]
|
|
||||||
context: String,
|
|
||||||
|
|
||||||
id: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
ty: String,
|
|
||||||
|
|
||||||
actor: String,
|
|
||||||
object: T,
|
|
||||||
published: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboxTransport = http::HttpClient;
|
|
||||||
pub struct Outbox<'a> {
|
|
||||||
user: User,
|
|
||||||
transport: &'a OutboxTransport,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Outbox<'a> {
|
|
||||||
pub fn user(&self) -> &User {
|
|
||||||
&self.user
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn post<T : Serialize + Debug>(&self, activity: OutgoingActivity<T>) {
|
|
||||||
dbg!(&activity);
|
|
||||||
let raw = RawActivity {
|
|
||||||
context: "https://www.w3.org/ns/activitystreams".to_string(),
|
|
||||||
id: activity.req.id,
|
|
||||||
ty: activity.req.ty.to_raw(),
|
|
||||||
actor: self.user.actor.id.clone(),
|
|
||||||
object: activity.req.object,
|
|
||||||
published: activity.req.published.to_rfc3339(),
|
|
||||||
};
|
|
||||||
|
|
||||||
dbg!(&raw);
|
|
||||||
|
|
||||||
let follow_res = self
|
|
||||||
.transport
|
|
||||||
.post(activity.to.inbox)
|
|
||||||
.activity()
|
|
||||||
.json(&raw)
|
|
||||||
.sign(&activity.signed_by)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
dbg!(follow_res);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> {
|
|
||||||
Outbox { user, transport }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
113
ferri-main/src/ap/post.rs
Normal file
113
ferri-main/src/ap/post.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use crate::ap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::Sqlite;
|
||||||
|
|
||||||
|
const POST_TYPE: &'static str = "Post";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Post {
|
||||||
|
id: String,
|
||||||
|
from: ap::User,
|
||||||
|
ts: DateTime<Utc>,
|
||||||
|
content: String,
|
||||||
|
|
||||||
|
to: Vec<String>,
|
||||||
|
cc: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Post {
|
||||||
|
pub fn from_parts(id: String, content: String, from: ap::User) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
from,
|
||||||
|
ts: ap::now(),
|
||||||
|
to: vec![],
|
||||||
|
cc: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> &str {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created_at(&self) -> String {
|
||||||
|
self.ts.to_rfc3339()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uri(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"https://ferri.amy.mov/users/{}/posts/{}",
|
||||||
|
self.from.id(),
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
||||||
|
let ts = self.ts.to_rfc3339();
|
||||||
|
let user_id = self.from.id();
|
||||||
|
let post_id = self.id();
|
||||||
|
let uri = self.uri();
|
||||||
|
let content = self.content.clone();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO post (id, uri, user_id, content, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
"#,
|
||||||
|
post_id,
|
||||||
|
uri,
|
||||||
|
user_id,
|
||||||
|
content,
|
||||||
|
ts
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to(mut self, recipient: String) -> Self {
|
||||||
|
self.to.push(recipient);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cc(mut self, recipient: String) -> Self {
|
||||||
|
self.cc.push(recipient);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_ap(self) -> APPost {
|
||||||
|
APPost {
|
||||||
|
context: ap::AS_CONTEXT.to_string(),
|
||||||
|
id: self.uri(),
|
||||||
|
ty: POST_TYPE.to_string(),
|
||||||
|
ts: self.ts.to_rfc3339(),
|
||||||
|
content: self.content,
|
||||||
|
to: self.to,
|
||||||
|
cc: self.cc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Default)]
|
||||||
|
pub struct APPost {
|
||||||
|
#[serde(rename = "@context")]
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
context: String,
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
ty: String,
|
||||||
|
|
||||||
|
#[serde(rename = "published")]
|
||||||
|
ts: String,
|
||||||
|
|
||||||
|
content: String,
|
||||||
|
to: Vec<String>,
|
||||||
|
cc: Vec<String>,
|
||||||
|
}
|
138
ferri-main/src/ap/user.rs
Normal file
138
ferri-main/src/ap/user.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
use sqlx::Sqlite;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Actor {
|
||||||
|
id: String,
|
||||||
|
inbox: String,
|
||||||
|
outbox: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor {
|
||||||
|
pub fn from_raw(id: String, inbox: String, outbox: String) -> Self {
|
||||||
|
Self { id, inbox, outbox }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inbox(&self) -> &str {
|
||||||
|
&self.inbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
actor: Actor,
|
||||||
|
display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
&self.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn actor_id(&self) -> &str {
|
||||||
|
&self.actor.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_name(&self) -> &str {
|
||||||
|
&self.display_name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn actor(&self) -> &Actor {
|
||||||
|
&self.actor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uri(&self) -> String {
|
||||||
|
format!("https://ferri.amy.mov/users/{}", self.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
username: &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 username = ?1
|
||||||
|
"#,
|
||||||
|
username
|
||||||
|
)
|
||||||
|
.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_actor_id(
|
||||||
|
actor_id: &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 actor_id = ?1
|
||||||
|
"#,
|
||||||
|
actor_id
|
||||||
|
)
|
||||||
|
.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
use rocket::{
|
use rocket::{
|
||||||
options, fairing::{Fairing, Info, Kind},
|
Request, Response,
|
||||||
Request, Response, http::{Method, Header}
|
fairing::{Fairing, Info, Kind},
|
||||||
|
http::{Header, Method},
|
||||||
|
options,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct CORS;
|
pub struct CORS;
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use rocket::{get, serde::json::Json};
|
use rocket::{get, serde::json::Json};
|
||||||
|
|
||||||
use crate::types::instance::{Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses, Thumbnail, Translation, Urls};
|
use crate::types::instance::{
|
||||||
|
Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses,
|
||||||
|
Thumbnail, Translation, Urls,
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/instance")]
|
#[get("/instance")]
|
||||||
pub async fn instance() -> Json<Instance> {
|
pub async fn instance() -> Json<Instance> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod user;
|
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod status;
|
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
|
pub mod status;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
pub mod user;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use rocket::{get, serde::{json::Json, Deserialize, Serialize}};
|
use rocket::{
|
||||||
|
get,
|
||||||
|
serde::{Deserialize, Serialize, json::Json},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::timeline::TimelineStatus;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use main::ap::{self, http::HttpClient};
|
use main::ap::{self, http::HttpClient};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
@ -8,10 +9,9 @@ use rocket::{
|
||||||
};
|
};
|
||||||
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::api::user::CredentialAcount;
|
use crate::api::user::CredentialAcount;
|
||||||
|
use crate::{AuthenticatedUser, Db, types::content};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm)]
|
#[derive(Serialize, Deserialize, Debug, FromForm)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
|
@ -19,130 +19,23 @@ pub struct Status {
|
||||||
status: String,
|
status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/statuses", data = "<status>")]
|
async fn create_status(
|
||||||
pub async fn new_status(
|
|
||||||
mut db: Connection<Db>,
|
|
||||||
http: &State<HttpClient>,
|
|
||||||
status: Form<Status>,
|
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
) {
|
|
||||||
let user = ap::User::from_actor_id(&user.actor_id, &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.username(),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/statuses", data = "<status>", rank = 2)]
|
|
||||||
pub async fn new_status_json(
|
|
||||||
mut db: Connection<Db>,
|
mut db: Connection<Db>,
|
||||||
http: &State<HttpClient>,
|
http: &HttpClient,
|
||||||
status: Json<Status>,
|
status: &Status,
|
||||||
user: AuthenticatedUser,
|
) -> TimelineStatus {
|
||||||
) -> Json<TimelineStatus> {
|
|
||||||
dbg!(&user);
|
|
||||||
let user = ap::User::from_id(&user.username, &mut **db).await;
|
let user = ap::User::from_id(&user.username, &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().to_string();
|
let post_id = ap::new_id();
|
||||||
|
let now = ap::new_ts();
|
||||||
|
|
||||||
let uri = format!(
|
let post = ap::Post::from_parts(post_id, status.status.clone(), user.clone())
|
||||||
"https://ferri.amy.mov/users/{}/posts/{}",
|
.to(format!("{}/followers", user.uri()))
|
||||||
user.id(),
|
.cc("https://www.w3.org/ns/activitystreams#Public".to_string());
|
||||||
post_id
|
|
||||||
);
|
|
||||||
let id = user.id();
|
|
||||||
let now = Local::now().to_rfc3339();
|
|
||||||
|
|
||||||
let post = sqlx::query!(
|
post.save(&mut **db).await;
|
||||||
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")
|
let actors = sqlx::query!("SELECT * FROM actor")
|
||||||
.fetch_all(&mut **db)
|
.fetch_all(&mut **db)
|
||||||
|
@ -151,8 +44,8 @@ pub async fn new_status_json(
|
||||||
|
|
||||||
for record in actors {
|
for record in actors {
|
||||||
// Don't send to ourselves
|
// Don't send to ourselves
|
||||||
if &record.id == user.actor_id() {
|
if record.id == user.actor_id() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4());
|
let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4());
|
||||||
|
@ -160,22 +53,8 @@ pub async fn new_status_json(
|
||||||
let activity = ap::Activity {
|
let activity = ap::Activity {
|
||||||
id: create_id,
|
id: create_id,
|
||||||
ty: ap::ActivityType::Create,
|
ty: ap::ActivityType::Create,
|
||||||
object: content::Post {
|
object: post.clone().to_ap(),
|
||||||
context: "https://www.w3.org/ns/activitystreams".to_string(),
|
to: vec![format!("{}/followers", user.uri())],
|
||||||
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()],
|
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -185,9 +64,10 @@ pub async fn new_status_json(
|
||||||
record.inbox.clone(),
|
record.inbox.clone(),
|
||||||
record.outbox.clone(),
|
record.outbox.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let req = ap::OutgoingActivity {
|
let req = ap::OutgoingActivity {
|
||||||
req: activity,
|
req: activity,
|
||||||
signed_by: format!("https://ferri.amy.mov/users/{}#main-key", user.username()),
|
signed_by: format!("{}#main-key", user.uri()),
|
||||||
to: actor,
|
to: actor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -195,21 +75,17 @@ pub async fn new_status_json(
|
||||||
outbox.post(req).await;
|
outbox.post(req).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_uri = format!(
|
TimelineStatus {
|
||||||
"https://ferri.amy.mov/users/{}",
|
id: post.id().to_string(),
|
||||||
user.id(),
|
created_at: post.created_at(),
|
||||||
);
|
|
||||||
Json(TimelineStatus {
|
|
||||||
id: post.id.clone(),
|
|
||||||
created_at: post.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: post.content.clone(),
|
content: post.content().to_string(),
|
||||||
visibility: "public".to_string(),
|
visibility: "public".to_string(),
|
||||||
spoiler_text: "".to_string(),
|
spoiler_text: "".to_string(),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
uri: post.uri.clone(),
|
uri: post.uri(),
|
||||||
url: post.uri.clone(),
|
url: post.uri(),
|
||||||
replies_count: 0,
|
replies_count: 0,
|
||||||
reblogs_count: 0,
|
reblogs_count: 0,
|
||||||
favourites_count: 0,
|
favourites_count: 0,
|
||||||
|
@ -228,7 +104,7 @@ pub async fn new_status_json(
|
||||||
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: user_uri,
|
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(),
|
||||||
|
@ -237,6 +113,26 @@ pub async fn new_status_json(
|
||||||
following_count: 1,
|
following_count: 1,
|
||||||
statuses_count: 1,
|
statuses_count: 1,
|
||||||
last_status_at: "2025-04-10T22:14:34Z".to_string(),
|
last_status_at: "2025-04-10T22:14:34Z".to_string(),
|
||||||
}
|
},
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/statuses", data = "<status>")]
|
||||||
|
pub async fn new_status(
|
||||||
|
db: Connection<Db>,
|
||||||
|
http: &State<HttpClient>,
|
||||||
|
status: Form<Status>,
|
||||||
|
user: AuthenticatedUser,
|
||||||
|
) -> Json<TimelineStatus> {
|
||||||
|
Json(create_status(user, db, http.inner(), &status).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/statuses", data = "<status>", rank = 2)]
|
||||||
|
pub async fn new_status_json(
|
||||||
|
db: Connection<Db>,
|
||||||
|
http: &State<HttpClient>,
|
||||||
|
status: Json<Status>,
|
||||||
|
user: AuthenticatedUser,
|
||||||
|
) -> Json<TimelineStatus> {
|
||||||
|
Json(create_status(user, db, http.inner(), &status).await)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,11 @@ pub struct TimelineStatus {
|
||||||
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, p.uri as "post_uri", u.username, u.display_name, u.actor_id, p.created_at
|
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",
|
||||||
FROM post p
|
u.username, u.display_name, u.actor_id, p.created_at
|
||||||
INNER JOIN user u on p.user_id = u.id
|
FROM post p
|
||||||
"#
|
INNER JOIN user u on p.user_id = u.id
|
||||||
|
"#
|
||||||
)
|
)
|
||||||
.fetch_all(&mut **db)
|
.fetch_all(&mut **db)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -6,8 +6,8 @@ use rocket::{
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::timeline::{TimelineAccount, TimelineStatus};
|
||||||
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")]
|
||||||
|
@ -35,7 +35,7 @@ pub struct CredentialAcount {
|
||||||
#[get("/accounts/verify_credentials")]
|
#[get("/accounts/verify_credentials")]
|
||||||
pub async fn verify_credentials() -> Json<CredentialAcount> {
|
pub async fn verify_credentials() -> Json<CredentialAcount> {
|
||||||
Json(CredentialAcount {
|
Json(CredentialAcount {
|
||||||
id: "https://ferri.amy.mov/users/amy".to_string(),
|
id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(),
|
||||||
username: "amy".to_string(),
|
username: "amy".to_string(),
|
||||||
acct: "amy@ferri.amy.mov".to_string(),
|
acct: "amy@ferri.amy.mov".to_string(),
|
||||||
display_name: "amy".to_string(),
|
display_name: "amy".to_string(),
|
||||||
|
@ -89,7 +89,11 @@ pub async fn new_follow(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/<uuid>")]
|
#[get("/accounts/<uuid>")]
|
||||||
pub async fn account(mut db: Connection<Db>, uuid: &str, user: AuthenticatedUser) -> Json<TimelineAccount> {
|
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 = ap::User::from_id(uuid, &mut **db).await;
|
||||||
let user_uri = format!("https://ferri.amy.mov/users/{}", user.username());
|
let user_uri = format!("https://ferri.amy.mov/users/{}", user.username());
|
||||||
Json(CredentialAcount {
|
Json(CredentialAcount {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use main::ap::http::HttpClient;
|
use main::ap::http::HttpClient;
|
||||||
use rocket::{get, response::status, State};
|
use rocket::{State, get, response::status};
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{types::{self, activity, content, webfinger}, Db};
|
use crate::{
|
||||||
|
Db,
|
||||||
|
types::{self, activity, content, webfinger},
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/finger/<account>")]
|
#[get("/finger/<account>")]
|
||||||
pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Accepted<String> {
|
pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Accepted<String> {
|
||||||
|
@ -109,9 +112,11 @@ pub async fn test(http: &State<HttpClient>) -> &'static str {
|
||||||
.sign(key_id)
|
.sign(key_id)
|
||||||
.activity()
|
.activity()
|
||||||
.send()
|
.send()
|
||||||
.await.unwrap()
|
.await
|
||||||
|
.unwrap()
|
||||||
.text()
|
.text()
|
||||||
.await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
dbg!(follow);
|
dbg!(follow);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
use chrono::Local;
|
||||||
use main::ap;
|
use main::ap;
|
||||||
|
use rocket::serde::json::serde_json;
|
||||||
use rocket::{State, post};
|
use rocket::{State, post};
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
use rocket::serde::json::serde_json;
|
|
||||||
use sqlx::Sqlite;
|
use sqlx::Sqlite;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use chrono::Local;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Db,
|
Db,
|
||||||
|
@ -17,7 +17,11 @@ fn handle_delete_activity(activity: activity::DeleteActivity) {
|
||||||
dbg!(activity);
|
dbg!(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_actor(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
async fn create_actor(
|
||||||
|
user: &Person,
|
||||||
|
actor: String,
|
||||||
|
conn: impl sqlx::Executor<'_, Database = Sqlite>,
|
||||||
|
) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO actor (id, inbox, outbox)
|
INSERT INTO actor (id, inbox, outbox)
|
||||||
|
@ -33,7 +37,11 @@ async fn create_actor(user: &Person, actor: String, conn: impl sqlx::Executor<'_
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
async fn create_user(
|
||||||
|
user: &Person,
|
||||||
|
actor: String,
|
||||||
|
conn: impl sqlx::Executor<'_, Database = Sqlite>,
|
||||||
|
) {
|
||||||
// HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user
|
// HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user
|
||||||
let url = Url::parse(&actor).unwrap();
|
let url = Url::parse(&actor).unwrap();
|
||||||
let host = url.host_str().unwrap();
|
let host = url.host_str().unwrap();
|
||||||
|
@ -56,7 +64,10 @@ async fn create_user(user: &Person, actor: String, conn: impl sqlx::Executor<'_,
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_follow(activity: &activity::FollowActivity, conn: impl sqlx::Executor<'_, Database = Sqlite>) {
|
async fn create_follow(
|
||||||
|
activity: &activity::FollowActivity,
|
||||||
|
conn: impl sqlx::Executor<'_, Database = Sqlite>,
|
||||||
|
) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO follow (id, follower_id, followed_id)
|
INSERT INTO follow (id, follower_id, followed_id)
|
||||||
|
@ -72,7 +83,12 @@ async fn create_follow(activity: &activity::FollowActivity, conn: impl sqlx::Exe
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_follow_activity(followed_account: String, activity: activity::FollowActivity, http: &HttpClient, mut db: Connection<Db>) {
|
async fn handle_follow_activity(
|
||||||
|
followed_account: String,
|
||||||
|
activity: activity::FollowActivity,
|
||||||
|
http: &HttpClient,
|
||||||
|
mut db: Connection<Db>,
|
||||||
|
) {
|
||||||
let user = http
|
let user = http
|
||||||
.get(&activity.actor)
|
.get(&activity.actor)
|
||||||
.activity()
|
.activity()
|
||||||
|
@ -114,11 +130,16 @@ async fn handle_follow_activity(followed_account: String, activity: activity::Fo
|
||||||
async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection<Db>) {
|
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)
|
let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object)
|
||||||
.fetch_one(&mut **db)
|
.fetch_one(&mut **db)
|
||||||
.await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
dbg!(&target_post);
|
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
|
||||||
.get(&activity.actor)
|
.get(&activity.actor)
|
||||||
|
@ -141,13 +162,20 @@ async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpCl
|
||||||
let post_id = Uuid::new_v4().to_string();
|
let post_id = Uuid::new_v4().to_string();
|
||||||
let uri = activity.id;
|
let uri = activity.id;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
sqlx::query!(r#"
|
r#"
|
||||||
INSERT INTO post (id, uri, user_id, content, created_at)
|
INSERT INTO post (id, uri, user_id, content, created_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
"#, post_id, uri, user_id, content, now)
|
"#,
|
||||||
.execute(&mut **db)
|
post_id,
|
||||||
.await.unwrap();
|
uri,
|
||||||
|
user_id,
|
||||||
|
content,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<user>/inbox", data = "<body>")]
|
#[post("/users/<user>/inbox", data = "<body>")]
|
||||||
|
@ -161,15 +189,15 @@ pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: String, b
|
||||||
"Follow" => {
|
"Follow" => {
|
||||||
let activity = serde_json::from_str::<activity::FollowActivity>(&body).unwrap();
|
let activity = serde_json::from_str::<activity::FollowActivity>(&body).unwrap();
|
||||||
handle_follow_activity(user, activity, http.inner(), db).await;
|
handle_follow_activity(user, activity, http.inner(), db).await;
|
||||||
},
|
}
|
||||||
"Create" => {
|
"Create" => {
|
||||||
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" => {
|
"Like" => {
|
||||||
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap();
|
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap();
|
||||||
handle_like_activity(activity, db).await;
|
handle_like_activity(activity, db).await;
|
||||||
},
|
}
|
||||||
unknown => {
|
unknown => {
|
||||||
eprintln!("WARN: Unknown activity '{}' - {}", unknown, body);
|
eprintln!("WARN: Unknown activity '{}' - {}", unknown, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use rocket::http::{ContentType, MediaType};
|
use rocket::http::{ContentType, MediaType};
|
||||||
|
|
||||||
pub mod user;
|
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod well_known;
|
|
||||||
pub mod custom;
|
pub mod custom;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
|
pub mod well_known;
|
||||||
|
|
||||||
fn activity_type() -> ContentType {
|
fn activity_type() -> ContentType {
|
||||||
ContentType(MediaType::new("application", "activity+json"))
|
ContentType(MediaType::new("application", "activity+json"))
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use rocket::{get, post, response::Redirect, serde::{json::Json, Deserialize, Serialize}};
|
use rocket::{
|
||||||
|
get, post,
|
||||||
|
response::Redirect,
|
||||||
|
serde::{Deserialize, Serialize, json::Json},
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
||||||
pub async fn authorize(
|
pub async fn authorize(
|
||||||
|
|
|
@ -7,14 +7,12 @@ use crate::{
|
||||||
types::{OrderedCollection, Person, UserKey, content},
|
types::{OrderedCollection, Person, UserKey, content},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
use super::activity_type;
|
use super::activity_type;
|
||||||
|
|
||||||
#[get("/users/<user>/inbox")]
|
#[get("/users/<user>/inbox")]
|
||||||
pub async fn inbox(user: String) -> Json<OrderedCollection> {
|
pub async fn inbox(user: String) -> Json<OrderedCollection> {
|
||||||
Json(OrderedCollection {
|
Json(OrderedCollection {
|
||||||
ty: "OrderedCollection".to_string(),
|
ty: "OrderedCollection".to_string(),
|
||||||
summary: format!("Inbox for {}", user),
|
|
||||||
total_items: 0,
|
total_items: 0,
|
||||||
ordered_items: vec![],
|
ordered_items: vec![],
|
||||||
})
|
})
|
||||||
|
@ -25,7 +23,6 @@ pub async fn outbox(user: String) -> Json<OrderedCollection> {
|
||||||
dbg!(&user);
|
dbg!(&user);
|
||||||
Json(OrderedCollection {
|
Json(OrderedCollection {
|
||||||
ty: "OrderedCollection".to_string(),
|
ty: "OrderedCollection".to_string(),
|
||||||
summary: format!("Outbox for {}", user),
|
|
||||||
total_items: 0,
|
total_items: 0,
|
||||||
ordered_items: vec![],
|
ordered_items: vec![],
|
||||||
})
|
})
|
||||||
|
@ -49,7 +46,6 @@ pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollec
|
||||||
|
|
||||||
Json(OrderedCollection {
|
Json(OrderedCollection {
|
||||||
ty: "OrderedCollection".to_string(),
|
ty: "OrderedCollection".to_string(),
|
||||||
summary: format!("Followers for {}", uuid),
|
|
||||||
total_items: 1,
|
total_items: 1,
|
||||||
ordered_items: followers
|
ordered_items: followers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -76,7 +72,6 @@ pub async fn following(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollec
|
||||||
|
|
||||||
Json(OrderedCollection {
|
Json(OrderedCollection {
|
||||||
ty: "OrderedCollection".to_string(),
|
ty: "OrderedCollection".to_string(),
|
||||||
summary: format!("Following for {}", uuid),
|
|
||||||
total_items: 1,
|
total_items: 1,
|
||||||
ordered_items: following
|
ordered_items: following
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -86,12 +81,20 @@ pub async fn following(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollec
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>/posts/<post>")]
|
#[get("/users/<uuid>/posts/<post>")]
|
||||||
pub async fn post(mut db: Connection<Db>, uuid: &str, post: String) -> (ContentType, Json<content::Post>) {
|
pub async fn post(
|
||||||
let post = sqlx::query!(r#"
|
mut db: Connection<Db>,
|
||||||
|
uuid: &str,
|
||||||
|
post: String,
|
||||||
|
) -> (ContentType, Json<content::Post>) {
|
||||||
|
let post = sqlx::query!(
|
||||||
|
r#"
|
||||||
SELECT * FROM post WHERE id = ?1
|
SELECT * FROM post WHERE id = ?1
|
||||||
"#, post)
|
"#,
|
||||||
.fetch_one(&mut **db)
|
post
|
||||||
.await.unwrap();
|
)
|
||||||
|
.fetch_one(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
(
|
(
|
||||||
activity_type(),
|
activity_type(),
|
||||||
|
|
|
@ -2,7 +2,10 @@ use main::ap;
|
||||||
use rocket::{get, serde::json::Json};
|
use rocket::{get, serde::json::Json};
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
|
|
||||||
use crate::{types::webfinger::{Link, WebfingerResponse}, Db};
|
use crate::{
|
||||||
|
Db,
|
||||||
|
types::webfinger::{Link, WebfingerResponse},
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/.well-known/host-meta")]
|
#[get("/.well-known/host-meta")]
|
||||||
pub async fn host_meta() -> &'static str {
|
pub async fn host_meta() -> &'static str {
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
use endpoints::{
|
||||||
|
api::{self, timeline},
|
||||||
|
custom, inbox, oauth, user, well_known,
|
||||||
|
};
|
||||||
use main::ap::http;
|
use main::ap::http;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
build, get, http::ContentType, request::{FromRequest, Outcome}, routes, Build, Request, Rocket
|
Build, Request, Rocket, build, get,
|
||||||
|
http::ContentType,
|
||||||
|
request::{FromRequest, Outcome},
|
||||||
|
routes,
|
||||||
};
|
};
|
||||||
use endpoints::{api::{self, timeline}, oauth, well_known, custom, user, inbox};
|
use rocket_db_pools::{Database, sqlx};
|
||||||
use rocket_db_pools::{sqlx, Database};
|
|
||||||
|
|
||||||
mod cors;
|
mod cors;
|
||||||
mod types;
|
|
||||||
mod endpoints;
|
mod endpoints;
|
||||||
|
mod types;
|
||||||
|
|
||||||
#[derive(Database)]
|
#[derive(Database)]
|
||||||
#[database("sqlite_ferri")]
|
#[database("sqlite_ferri")]
|
||||||
|
@ -26,7 +32,7 @@ async fn activity_endpoint(activity: String) {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct AuthenticatedUser {
|
struct AuthenticatedUser {
|
||||||
username: String,
|
username: String,
|
||||||
actor_id: String
|
actor_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -41,10 +47,13 @@ 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);
|
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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ pub mod webfinger;
|
||||||
|
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
|
@ -31,11 +30,11 @@ pub struct Person {
|
||||||
pub ty: String,
|
pub ty: String,
|
||||||
pub following: String,
|
pub following: String,
|
||||||
pub followers: String,
|
pub followers: String,
|
||||||
|
pub summary: String,
|
||||||
pub inbox: String,
|
pub inbox: String,
|
||||||
pub outbox: String,
|
pub outbox: String,
|
||||||
pub preferred_username: String,
|
pub preferred_username: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub summary: String,
|
|
||||||
pub public_key: Option<UserKey>,
|
pub public_key: Option<UserKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +52,6 @@ pub struct Object {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct OrderedCollection {
|
pub struct OrderedCollection {
|
||||||
pub summary: String,
|
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub ty: String,
|
pub ty: String,
|
||||||
pub total_items: u64,
|
pub total_items: u64,
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use rocket::{serde::{Deserialize, Serialize}, FromForm};
|
use rocket::{
|
||||||
|
FromForm,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm)]
|
#[derive(Serialize, Deserialize, Debug, FromForm)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
|
|
Loading…
Add table
Reference in a new issue