mirror of
https://github.com/nullishamy/ferri.git
synced 2025-08-17 18:19:14 +00:00
Compare commits
4 commits
77fba1a082
...
a924415a74
Author | SHA1 | Date | |
---|---|---|---|
a924415a74 | |||
41c0091e98 | |||
f7f57e92e3 | |||
4167a2f8bb |
22 changed files with 625 additions and 61 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -110,6 +110,48 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
|
@ -206,6 +248,15 @@ version = "1.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
|
@ -2156,6 +2207,7 @@ dependencies = [
|
|||
name = "server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"chrono",
|
||||
"main",
|
||||
"rand 0.8.5",
|
||||
|
|
|
@ -55,9 +55,9 @@ async fn main() {
|
|||
r#"
|
||||
INSERT INTO user (
|
||||
id, acct, url, remote, username,
|
||||
actor_id, display_name, created_at
|
||||
actor_id, display_name, created_at, icon_url
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
"#,
|
||||
"9b9d497b-2731-435f-a929-e609ca69dac9",
|
||||
"amy",
|
||||
|
@ -66,7 +66,8 @@ async fn main() {
|
|||
"amy",
|
||||
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9",
|
||||
"amy",
|
||||
ts
|
||||
ts,
|
||||
"https://ferri.amy.mov/assets/pfp.png"
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await
|
||||
|
|
|
@ -44,10 +44,10 @@ impl From<db::User> for api::Account {
|
|||
note: "".to_string(),
|
||||
url: val.url,
|
||||
|
||||
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(),
|
||||
avatar: val.icon_url.clone(),
|
||||
avatar_static: val.icon_url.clone(),
|
||||
header: val.icon_url.clone(),
|
||||
header_static: val.icon_url,
|
||||
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
|
@ -79,6 +79,42 @@ impl From<db::User> for ap::Person {
|
|||
owner: format!("https://ferri.amy.mov/users/{}", val.id.0),
|
||||
public_key: include_str!("../../../public.pem").to_string(),
|
||||
}),
|
||||
icon: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<db::Post> for api::Status {
|
||||
fn from(value: db::Post) -> api::Status {
|
||||
api::Status {
|
||||
id: value.id,
|
||||
created_at: value.created_at.to_rfc3339(),
|
||||
in_reply_to_id: None,
|
||||
in_reply_to_account_id: None,
|
||||
sensitive: false,
|
||||
spoiler_text: String::new(),
|
||||
visibility: "Public".to_string(),
|
||||
language: "en-GB".to_string(),
|
||||
uri: value.uri.clone(),
|
||||
url: value.uri.0.to_string(),
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
muted: false,
|
||||
bookmarked: false,
|
||||
content: value.content,
|
||||
reblog: None,
|
||||
application: None,
|
||||
account: value.user.into(),
|
||||
media_attachments: vec![],
|
||||
mentions: vec![],
|
||||
tags: vec![],
|
||||
emojis: vec![],
|
||||
card: None,
|
||||
poll: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
use crate::types::{DbError, ObjectUri, ObjectUuid, db};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use sqlx::SqliteConnection;
|
||||
use tracing::info;
|
||||
use tracing::{info, error};
|
||||
|
||||
const SQLITE_TIME_FMT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
|
||||
fn parse_ts(ts: String) -> Option<DateTime<Utc>> {
|
||||
NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT)
|
||||
.ok()
|
||||
.map(|nt| nt.and_utc())
|
||||
// Depending on how the TS is queried it may be naive (so get it back to utc)
|
||||
// or it may have a timezone associated with it
|
||||
let dt = NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT)
|
||||
.map(|ndt| {
|
||||
ndt.and_utc()
|
||||
})
|
||||
.or_else(|_| {
|
||||
DateTime::parse_from_rfc3339(&ts)
|
||||
.map(|dt| dt.to_utc())
|
||||
});
|
||||
|
||||
if let Err(err) = dt {
|
||||
error!("could not parse datetime {} ({}), db weirdness", ts, err);
|
||||
return None
|
||||
}
|
||||
|
||||
Some(dt.unwrap())
|
||||
}
|
||||
|
||||
pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
|
||||
|
@ -26,7 +40,8 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
|
|||
u.url,
|
||||
u.acct,
|
||||
u.remote,
|
||||
u.created_at
|
||||
u.created_at,
|
||||
u.icon_url
|
||||
FROM "user" u
|
||||
INNER JOIN "actor" a ON u.actor_id = a.id
|
||||
WHERE u.id = ?1
|
||||
|
@ -59,9 +74,10 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
|
|||
"#,
|
||||
record.user_id
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| DbError::FetchError(e.to_string()))?
|
||||
.flatten()
|
||||
.and_then(|ts| {
|
||||
info!("parsing timestamp {}", ts);
|
||||
parse_ts(ts)
|
||||
|
@ -86,5 +102,142 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
|
|||
created_at: user_created,
|
||||
url: record.url,
|
||||
posts: db::UserPosts { last_post_at },
|
||||
icon_url: record.icon_url
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn user_by_actor_uri(uri: ObjectUri, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
|
||||
info!("fetching user by actor_uri '{:?}' from the database", uri);
|
||||
|
||||
let record = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
u.id as "user_id",
|
||||
u.username,
|
||||
u.actor_id,
|
||||
u.display_name,
|
||||
a.inbox,
|
||||
a.outbox,
|
||||
u.url,
|
||||
u.acct,
|
||||
u.remote,
|
||||
u.created_at,
|
||||
u.icon_url
|
||||
FROM "user" u
|
||||
INNER JOIN "actor" a ON u.actor_id = a.id
|
||||
WHERE u.actor_id = ?1
|
||||
"#,
|
||||
uri.0
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| DbError::FetchError(e.to_string()))?;
|
||||
|
||||
let follower_count = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(follower_id)
|
||||
FROM "follow"
|
||||
WHERE followed_id = ?1
|
||||
"#,
|
||||
record.actor_id
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| DbError::FetchError(e.to_string()))?;
|
||||
|
||||
let last_post_at = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT datetime(p.created_at)
|
||||
FROM post p
|
||||
WHERE p.user_id = ?1
|
||||
ORDER BY datetime(p.created_at) DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
record.user_id
|
||||
)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| DbError::FetchError(e.to_string()))?
|
||||
.flatten()
|
||||
.and_then(|ts| {
|
||||
info!("parsing timestamp {}", ts);
|
||||
parse_ts(ts)
|
||||
});
|
||||
|
||||
let user_created = parse_ts(record.created_at).expect("no db corruption");
|
||||
|
||||
info!("user {:?} has {} followers", record.user_id, follower_count);
|
||||
info!("user {:?} last posted {:?}", record.user_id, last_post_at);
|
||||
|
||||
Ok(db::User {
|
||||
id: ObjectUuid(record.user_id),
|
||||
actor: db::Actor {
|
||||
id: ObjectUri(record.actor_id),
|
||||
inbox: record.inbox,
|
||||
outbox: record.outbox,
|
||||
},
|
||||
acct: record.acct,
|
||||
remote: record.remote,
|
||||
username: record.username,
|
||||
display_name: record.display_name,
|
||||
created_at: user_created,
|
||||
url: record.url,
|
||||
posts: db::UserPosts { last_post_at },
|
||||
icon_url: record.icon_url
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn posts_for_user_id(
|
||||
id: ObjectUuid,
|
||||
conn: &mut SqliteConnection
|
||||
) -> Result<Vec<db::Post>, DbError> {
|
||||
let mut out = vec![];
|
||||
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 as "post_created",
|
||||
p.boosted_post_id, a.inbox, a.outbox, u.created_at as "user_created",
|
||||
u.acct, u.remote, u.url as "user_url", u.icon_url
|
||||
FROM post p
|
||||
INNER JOIN user u on p.user_id = u.id
|
||||
INNER JOIN actor a ON u.actor_id = a.id
|
||||
WHERE p.user_id = ?
|
||||
"#, id.0)
|
||||
.fetch_all(&mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for record in posts {
|
||||
let user_created = parse_ts(record.user_created)
|
||||
.expect("no db corruption");
|
||||
|
||||
out.push(db::Post {
|
||||
id: ObjectUuid(record.post_id),
|
||||
uri: ObjectUri(record.post_uri),
|
||||
user: db::User {
|
||||
id: ObjectUuid(record.user_id),
|
||||
actor: db::Actor {
|
||||
id: ObjectUri(record.actor_id),
|
||||
inbox: record.inbox,
|
||||
outbox: record.outbox,
|
||||
},
|
||||
acct: record.acct,
|
||||
remote: record.remote,
|
||||
username: record.username,
|
||||
display_name: record.display_name,
|
||||
created_at: user_created,
|
||||
url: record.user_url,
|
||||
icon_url: record.icon_url,
|
||||
posts: db::UserPosts {
|
||||
last_post_at: None
|
||||
}
|
||||
},
|
||||
content: record.content,
|
||||
created_at: parse_ts(record.post_created).unwrap(),
|
||||
boosted_post: None
|
||||
})
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@ pub async fn new_user(user: db::User, conn: &mut SqliteConnection) -> Result<db:
|
|||
let ts = user.created_at.to_rfc3339();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user (id, acct, url, created_at, remote, username, actor_id, display_name)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
INSERT INTO user (id, acct, url, created_at, remote,
|
||||
username, actor_id, display_name, icon_url)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
"#,
|
||||
user.id.0,
|
||||
user.acct,
|
||||
|
@ -15,7 +16,8 @@ pub async fn new_user(user: db::User, conn: &mut SqliteConnection) -> Result<db:
|
|||
user.remote,
|
||||
user.username,
|
||||
user.actor.id.0,
|
||||
user.display_name
|
||||
user.display_name,
|
||||
user.icon_url
|
||||
)
|
||||
.execute(conn)
|
||||
.await
|
||||
|
|
|
@ -27,6 +27,12 @@ pub enum ObjectContext {
|
|||
Vec(Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
impl Default for ObjectContext {
|
||||
fn default() -> Self {
|
||||
ObjectContext::Str(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
|
||||
pub struct ObjectUri(pub String);
|
||||
|
||||
|
@ -48,6 +54,7 @@ impl ObjectUuid {
|
|||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct Object {
|
||||
#[serde(rename = "@context")]
|
||||
#[serde(default)]
|
||||
pub context: ObjectContext,
|
||||
pub id: ObjectUri,
|
||||
}
|
||||
|
@ -79,9 +86,20 @@ pub mod db {
|
|||
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 Post {
|
||||
pub id: ObjectUuid,
|
||||
pub uri: ObjectUri,
|
||||
pub user: User,
|
||||
pub content: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub boosted_post: Option<ObjectUuid>
|
||||
}
|
||||
}
|
||||
|
||||
pub mod ap {
|
||||
|
@ -103,6 +121,8 @@ pub mod ap {
|
|||
pub struct MinimalActivity {
|
||||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
}
|
||||
|
||||
|
@ -123,6 +143,7 @@ pub mod ap {
|
|||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
|
||||
pub object: Post,
|
||||
|
@ -139,6 +160,7 @@ pub mod ap {
|
|||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
|
||||
pub object: String,
|
||||
|
@ -149,7 +171,8 @@ pub mod ap {
|
|||
pub struct AcceptActivity {
|
||||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
|
||||
pub object: String,
|
||||
|
@ -161,6 +184,7 @@ pub mod ap {
|
|||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
|
||||
pub actor: String,
|
||||
|
@ -175,6 +199,7 @@ pub mod ap {
|
|||
#[serde(flatten)]
|
||||
pub obj: Object,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub ty: ActivityType,
|
||||
|
||||
#[serde(rename = "published")]
|
||||
|
@ -196,6 +221,25 @@ pub mod ap {
|
|||
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 {
|
||||
|
@ -213,6 +257,8 @@ pub mod ap {
|
|||
pub name: String,
|
||||
|
||||
pub public_key: Option<UserKey>,
|
||||
|
||||
pub icon: Option<PersonIcon>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
|
@ -257,6 +303,37 @@ pub mod api {
|
|||
pub links: Vec<WebfingerLink>,
|
||||
}
|
||||
|
||||
#[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<Option<()>>,
|
||||
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,
|
||||
|
|
|
@ -18,4 +18,5 @@ tracing = { workspace = true }
|
|||
tracing-subscriber = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
askama = "0.14.0"
|
||||
|
|
20
ferri-server/src/endpoints/admin/mod.rs
Normal file
20
ferri-server/src/endpoints/admin/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use rocket::{get, post, response::content::RawHtml};
|
||||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
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("/")]
|
||||
pub async fn index() -> RawHtml<String> {
|
||||
let tmpl = IndexTemplate { val: "test".to_string() };
|
||||
RawHtml(tmpl.render().unwrap())
|
||||
}
|
|
@ -27,8 +27,8 @@ pub async fn new_app(
|
|||
VALUES (?1, ?2, ?3)
|
||||
"#,
|
||||
app.client_name,
|
||||
app.scopes,
|
||||
secret
|
||||
secret,
|
||||
app.scopes
|
||||
)
|
||||
.execute(&mut **db)
|
||||
.await
|
||||
|
|
|
@ -4,3 +4,4 @@ pub mod preferences;
|
|||
pub mod status;
|
||||
pub mod timeline;
|
||||
pub mod user;
|
||||
pub mod search;
|
||||
|
|
88
ferri-server/src/endpoints/api/search.rs
Normal file
88
ferri-server/src/endpoints/api/search.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use rocket::{
|
||||
get, serde::json::Json, FromFormField, State,
|
||||
};
|
||||
use main::types::{api, get};
|
||||
use rocket_db_pools::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, error};
|
||||
|
||||
use crate::{http_wrapper::HttpWrapper, AuthenticatedUser, Db};
|
||||
|
||||
#[derive(Serialize, Deserialize, FromFormField, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SearchType {
|
||||
Accounts,
|
||||
Hashtags,
|
||||
Statuses,
|
||||
All
|
||||
}
|
||||
|
||||
impl Default for SearchType {
|
||||
fn default() -> Self {
|
||||
Self::All
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SearchResults {
|
||||
statuses: Vec<api::Status>,
|
||||
accounts: Vec<api::Account>,
|
||||
hashtags: Vec<()>
|
||||
}
|
||||
|
||||
#[get("/search?<q>&<type>")]
|
||||
pub async fn search(
|
||||
q: &str,
|
||||
r#type: SearchType,
|
||||
helpers: &State<crate::Helpers>,
|
||||
mut db: Connection<Db>,
|
||||
user: AuthenticatedUser
|
||||
) -> Json<SearchResults> {
|
||||
let ty = r#type;
|
||||
info!("search for {} (ty: {:?})", q, ty);
|
||||
|
||||
let key_id = "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key";
|
||||
let http = HttpWrapper::new(&helpers.http, key_id);
|
||||
|
||||
let mut accounts = vec![];
|
||||
let mut statuses = vec![];
|
||||
|
||||
match ty {
|
||||
SearchType::Accounts => {
|
||||
let person = {
|
||||
let res = http.get_person(q).await;
|
||||
if let Err(e) = res {
|
||||
error!("could not load user {}: {}", q, e.to_string());
|
||||
None
|
||||
} else {
|
||||
Some(res.unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
let user = get::user_by_actor_uri(person.unwrap().obj.id, &mut **db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
accounts.push(user.into())
|
||||
},
|
||||
SearchType::Statuses => {
|
||||
if q == "me" {
|
||||
let st = get::posts_for_user_id(user.id, &mut **db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for status in st.into_iter() {
|
||||
statuses.push(status.into());
|
||||
}
|
||||
}
|
||||
},
|
||||
SearchType::Hashtags => todo!(),
|
||||
SearchType::All => todo!(),
|
||||
}
|
||||
|
||||
Json(SearchResults {
|
||||
statuses,
|
||||
accounts,
|
||||
hashtags: vec![],
|
||||
})
|
||||
}
|
|
@ -43,7 +43,7 @@ async fn create_status(
|
|||
http: &HttpClient,
|
||||
status: &Status,
|
||||
) -> TimelineStatus {
|
||||
let user = ap::User::from_id(&user.id, &mut **db).await.unwrap();
|
||||
let user = ap::User::from_id(&user.id.0, &mut **db).await.unwrap();
|
||||
let outbox = ap::Outbox::for_user(user.clone(), http);
|
||||
|
||||
let post_id = ap::new_id();
|
||||
|
|
|
@ -35,11 +35,8 @@ pub struct TimelineStatus {
|
|||
#[get("/timelines/home")]
|
||||
pub async fn home(
|
||||
mut db: Connection<Db>,
|
||||
helpers: &State<crate::Helpers>,
|
||||
_user: AuthenticatedUser,
|
||||
) -> Json<Vec<TimelineStatus>> {
|
||||
let config = &helpers.config;
|
||||
|
||||
#[derive(sqlx::FromRow, Debug)]
|
||||
struct Post {
|
||||
is_boost_source: bool,
|
||||
|
@ -51,6 +48,8 @@ pub async fn home(
|
|||
boosted_post_id: Option<String>,
|
||||
display_name: String,
|
||||
username: String,
|
||||
icon_url: String,
|
||||
user_url: String
|
||||
}
|
||||
|
||||
// FIXME: query! can't cope with this. returns a type error
|
||||
|
@ -75,7 +74,7 @@ pub async fn home(
|
|||
)
|
||||
SELECT is_boost_source, 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, p.boosted_post_id
|
||||
u.actor_id, p.created_at, p.boosted_post_id, u.icon_url, u.url as "user_url"
|
||||
FROM get_home_timeline_with_boosts
|
||||
JOIN post p ON p.id = get_home_timeline_with_boosts.id
|
||||
JOIN user u ON u.id = p.user_id;
|
||||
|
@ -90,7 +89,6 @@ pub async fn home(
|
|||
for record in posts.iter() {
|
||||
let mut boost: Option<Box<TimelineStatus>> = None;
|
||||
if let Some(ref boosted_id) = record.boosted_post_id {
|
||||
let user_uri = config.user_url(&record.user_id);
|
||||
let record = posts.iter().find(|p| &p.post_id == boosted_id).unwrap();
|
||||
|
||||
boost = Some(Box::new(TimelineStatus {
|
||||
|
@ -123,11 +121,11 @@ pub async fn home(
|
|||
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(),
|
||||
url: record.user_url.clone(),
|
||||
avatar: record.icon_url.clone(),
|
||||
avatar_static: record.icon_url.clone(),
|
||||
header: record.icon_url.clone(),
|
||||
header_static: record.icon_url.clone(),
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
statuses_count: 1,
|
||||
|
@ -137,7 +135,6 @@ pub async fn home(
|
|||
}
|
||||
|
||||
if !record.is_boost_source {
|
||||
let user_uri = config.user_web_url(&record.username);
|
||||
out.push(TimelineStatus {
|
||||
id: record.post_id.clone(),
|
||||
created_at: record.created_at.clone(),
|
||||
|
@ -168,11 +165,11 @@ pub async fn home(
|
|||
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(),
|
||||
url: record.user_url.clone(),
|
||||
avatar: record.icon_url.clone(),
|
||||
avatar_static: record.icon_url.clone(),
|
||||
header: record.icon_url.clone(),
|
||||
header_static: record.icon_url.clone(),
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
statuses_count: 1,
|
||||
|
|
|
@ -66,7 +66,7 @@ pub async fn new_follow(
|
|||
) -> Result<(), NotFound<String>> {
|
||||
let http = &helpers.http;
|
||||
|
||||
let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
|
||||
let follower = ap::User::from_actor_id(&user.actor_id.0, &mut **db).await;
|
||||
|
||||
let followed = ap::User::from_id(uuid, &mut **db)
|
||||
.await
|
||||
|
|
|
@ -57,6 +57,9 @@ async fn create_user(
|
|||
};
|
||||
|
||||
let url = format!("https://ferri.amy.mov/{}", acct);
|
||||
let icon_url = user.icon.as_ref().map(|ic| ic.url.clone()).unwrap_or(
|
||||
"https://ferri.amy.mov/assets/pfp.png".to_string()
|
||||
);
|
||||
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
// FIXME: Pull from user
|
||||
|
@ -65,10 +68,10 @@ async fn create_user(
|
|||
r#"
|
||||
INSERT INTO user (
|
||||
id, acct, url, remote, username,
|
||||
actor_id, display_name, created_at
|
||||
actor_id, display_name, created_at, icon_url
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
ON CONFLICT(actor_id) DO NOTHING;
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
ON CONFLICT(actor_id) DO NOTHING;
|
||||
"#,
|
||||
uuid,
|
||||
acct,
|
||||
|
@ -77,7 +80,8 @@ async fn create_user(
|
|||
user.preferred_username,
|
||||
actor,
|
||||
user.name,
|
||||
ts
|
||||
ts,
|
||||
icon_url
|
||||
)
|
||||
.execute(conn)
|
||||
.await
|
||||
|
@ -170,6 +174,9 @@ async fn resolve_actor<'a>(
|
|||
remote: remote_info.is_remote,
|
||||
url: remote_info.web_url,
|
||||
created_at: main::ap::now(),
|
||||
icon_url: person.icon.map(|ic| ic.url).unwrap_or(
|
||||
"https://ferri.amy.mov/assets/pfp.png".to_string()
|
||||
),
|
||||
|
||||
posts: db::UserPosts { last_post_at: None },
|
||||
};
|
||||
|
@ -327,6 +334,8 @@ async fn handle_boost_activity<'a>(
|
|||
);
|
||||
let user_id = actor_user.id();
|
||||
|
||||
info!("inserting post with id {} uri {}", base_id, uri);
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id)
|
||||
|
|
|
@ -4,6 +4,8 @@ pub mod oauth;
|
|||
pub mod user;
|
||||
|
||||
pub mod api;
|
||||
pub mod admin;
|
||||
|
||||
pub mod custom;
|
||||
pub mod inbox;
|
||||
pub mod well_known;
|
||||
|
|
|
@ -1,23 +1,40 @@
|
|||
use crate::Db;
|
||||
use askama::Template;
|
||||
use tracing::error;
|
||||
|
||||
use rocket::{
|
||||
FromForm,
|
||||
form::Form,
|
||||
get, post,
|
||||
response::Redirect,
|
||||
response::status::BadRequest,
|
||||
response::content::RawHtml,
|
||||
serde::{Deserialize, Serialize, json::Json},
|
||||
};
|
||||
|
||||
use rocket_db_pools::Connection;
|
||||
|
||||
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
||||
pub async fn authorize(
|
||||
client_id: &str,
|
||||
scope: &str,
|
||||
redirect_uri: &str,
|
||||
response_type: &str,
|
||||
struct AuthorizeClient {
|
||||
id: String
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "authorize.html")]
|
||||
struct AuthorizeTemplate {
|
||||
client: AuthorizeClient,
|
||||
scopes: Vec<String>,
|
||||
scope_raw: String,
|
||||
redirect_uri: String,
|
||||
user_id: String
|
||||
}
|
||||
|
||||
#[post("/oauth/accept?<id>&<client_id>&<scope>")]
|
||||
pub async fn accept(
|
||||
mut db: Connection<Db>,
|
||||
) -> Redirect {
|
||||
// For now, we will always authorize the request and assign it to an admin user
|
||||
let user_id = "9b9d497b-2731-435f-a929-e609ca69dac9";
|
||||
id: &str,
|
||||
client_id: &str,
|
||||
scope: &str
|
||||
) -> RawHtml<String> {
|
||||
let user_id = id;
|
||||
let code = main::gen_token(15);
|
||||
|
||||
// This will act as a token for the user, but we will in future say that it expires very shortly
|
||||
|
@ -52,7 +69,37 @@ pub async fn authorize(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
Redirect::temporary(format!("{}?code={}", redirect_uri, code))
|
||||
// HACK: Until we are storing oauth stuff more properly we will hardcode phanpy
|
||||
RawHtml(format!(r#"
|
||||
<script>window.location.href="{}{}"</script>
|
||||
"#, "https://phanpy.social?code=", code))
|
||||
}
|
||||
|
||||
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
||||
pub async fn authorize(
|
||||
client_id: &str,
|
||||
scope: &str,
|
||||
redirect_uri: &str,
|
||||
response_type: &str
|
||||
) -> Result<RawHtml<String>, BadRequest<String>> {
|
||||
if response_type != "code" {
|
||||
error!("unknown response type {}", response_type);
|
||||
return Err(
|
||||
BadRequest(format!("unknown response type {}", response_type))
|
||||
)
|
||||
}
|
||||
|
||||
let tmpl = AuthorizeTemplate {
|
||||
client: AuthorizeClient {
|
||||
id: client_id.to_string()
|
||||
},
|
||||
scope_raw: scope.to_string(),
|
||||
scopes: scope.split(" ").map(|s| s.to_string()).collect(),
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
user_id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string()
|
||||
};
|
||||
|
||||
Ok(RawHtml(tmpl.render().unwrap()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::http::HttpClient;
|
|||
use main::types::ap;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
use tracing::{Level, error, event};
|
||||
use tracing::{Level, error, event, info};
|
||||
|
||||
pub struct HttpWrapper<'a> {
|
||||
client: &'a HttpClient,
|
||||
|
@ -54,6 +54,7 @@ impl<'a> HttpWrapper<'a> {
|
|||
}
|
||||
|
||||
let raw_body = raw_body.unwrap();
|
||||
info!("raw body {}", raw_body);
|
||||
let decoded = serde_json::from_str::<T>(&raw_body);
|
||||
|
||||
if let Err(e) = decoded {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use endpoints::{
|
||||
api::{self, timeline},
|
||||
custom, inbox, oauth, user, well_known,
|
||||
admin, custom, inbox, oauth, user, well_known,
|
||||
};
|
||||
|
||||
use tracing_subscriber::fmt;
|
||||
|
||||
use main::ap;
|
||||
use main::{ap, types::{ObjectUri, ObjectUuid}};
|
||||
|
||||
use main::ap::http;
|
||||
use main::config::Config;
|
||||
|
@ -36,9 +36,9 @@ async fn activity_endpoint(_activity: String) {}
|
|||
#[derive(Debug)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub username: String,
|
||||
pub id: String,
|
||||
pub id: ObjectUuid,
|
||||
pub token: String,
|
||||
pub actor_id: String,
|
||||
pub actor_id: ObjectUri,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -72,9 +72,9 @@ impl<'a> FromRequest<'a> for AuthenticatedUser {
|
|||
if let Ok(auth) = auth {
|
||||
return Outcome::Success(AuthenticatedUser {
|
||||
token: auth.token,
|
||||
id: auth.id,
|
||||
id: ObjectUuid(auth.id),
|
||||
username: auth.display_name,
|
||||
actor_id: auth.actor_id,
|
||||
actor_id: ObjectUri(auth.actor_id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -122,6 +122,13 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
|
|||
.attach(Db::init())
|
||||
.attach(cors::CORS)
|
||||
.mount("/assets", rocket::fs::FileServer::from("./assets"))
|
||||
.mount(
|
||||
"/admin",
|
||||
routes![
|
||||
admin::index,
|
||||
admin::button_clicked
|
||||
]
|
||||
)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
|
@ -133,6 +140,7 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
|
|||
user::following,
|
||||
user::post,
|
||||
oauth::authorize,
|
||||
oauth::accept,
|
||||
oauth::new_token,
|
||||
cors::options_req,
|
||||
activity_endpoint,
|
||||
|
@ -142,7 +150,10 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
|
|||
user_profile,
|
||||
],
|
||||
)
|
||||
.mount("/api/v2", routes![api::instance::instance])
|
||||
.mount("/api/v2", routes![
|
||||
api::instance::instance,
|
||||
api::search::search,
|
||||
])
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![
|
||||
|
|
46
ferri-server/templates/authorize.html
Normal file
46
ferri-server/templates/authorize.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!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>Ferri Test</title>
|
||||
|
||||
<link rel="stylesheet" href="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/dbc8023a08964f513c20796e170cb91ce891df3f/packages/tailwindcss/preflight.css">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Authorization request</h1>
|
||||
|
||||
<span>
|
||||
App '{{ client.id }}' would like to access the following scopes in your account
|
||||
</span>
|
||||
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
<li>{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
hx-post="/oauth/accept?id={{ user_id }}&client_id={{ client.id }}&scope={{ scope_raw }}"
|
||||
>
|
||||
Accept and return to {{ redirect_uri }}?
|
||||
</button>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
19
ferri-server/templates/index.html
Normal file
19
ferri-server/templates/index.html
Normal 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>Ferri Test</title>
|
||||
|
||||
<link rel="stylesheet" href="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/dbc8023a08964f513c20796e170cb91ce891df3f/packages/tailwindcss/preflight.css">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button hx-post="clicked" hx-swap="outerHTML">
|
||||
Click Me {{ val }}
|
||||
</button>
|
||||
</body>
|
||||
</html>
|
|
@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS user
|
|||
username TEXT NOT NULL,
|
||||
actor_id TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
icon_url TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY(actor_id) REFERENCES actor(id)
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue