mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
feat: basic AP <-> lysand conversion (#4)
* feat: Update API domain variable name * save changes, fake commit * feat: function to receive lysand note * [feat]: lysand to ap for posts and users * feat: Add .env file to gitignore and update dependencies The commit adds the `.env` file to the `.gitignore` and updates the dependencies in the `Cargo.toml` and `Cargo.lock` files. This change ensures that sensitive environment variables are not committed to the repository and keeps the dependencies up to date. * feat: Add db_post_from_url function The commit adds the `db_post_from_url` function to the `conversion.rs` file. This function retrieves a post from the database based on a given URL. If the post is not found in the database, it fetches the post from the URL, saves it to the database, and returns the post. This change enhances the functionality of the codebase by providing a convenient way to retrieve and store posts. * feat: Update dependencies and add async-recursion crate * fix: Refactor lysand note handling in conversion.rs The commit refactors the lysand note handling in the `conversion.rs` file. It updates the `receive_lysand_note` function to properly handle quoting and replying to notes. This change improves the functionality and readability of the codebase. * fix: use example as default user * me oopid * fix: Refactor lysand note handling in conversion.rs * fix: fix post printing * fix: Refactor lysand note handling in conversion.rs * fix: Refactor lysand note handling in conversion.rs * fix: make nix-bootstrap executable * fix: remove unused linux only import * fix: make person stop screaming at me :( * feat: remove test code * fix: update deps * fix: rm build.rs fully * feat: standard user to apservice * fix: format files
This commit is contained in:
parent
14322c961f
commit
6b6d36b30b
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
DATABASE_URL="sqlite:///home/aprl/Documents/lysand-ap-layer/db.sqlite?mode=rwc"
|
||||||
|
LYSAND_DOMAIN="social.lysand.org"
|
||||||
|
API_DOMAIN="ap.lysand.org"
|
||||||
|
RUST_LOG="debug"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
||||||
.direnv
|
.direnv
|
||||||
migration/target
|
migration/target
|
||||||
db.sqlite
|
db.sqlite
|
||||||
|
.env
|
||||||
1070
Cargo.lock
generated
1070
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -2,7 +2,6 @@
|
||||||
name = "lysand-ap-layer"
|
name = "lysand-ap-layer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
|
||||||
authors = ["April John <aprl@acab.dev>"]
|
authors = ["April John <aprl@acab.dev>"]
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
repository = "https://github.com/lysand-org/lysand-ap-layer"
|
repository = "https://github.com/lysand-org/lysand-ap-layer"
|
||||||
|
|
@ -32,6 +31,8 @@ async_once = "0.2.6"
|
||||||
reqwest = { version = "0.12.4", features = ["blocking", "json", "multipart"] }
|
reqwest = { version = "0.12.4", features = ["blocking", "json", "multipart"] }
|
||||||
time = { version = "0.3.36", features = ["serde"] }
|
time = { version = "0.3.36", features = ["serde"] }
|
||||||
serde_derive = "1.0.201"
|
serde_derive = "1.0.201"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
async-recursion = "1.1.1"
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
@ -44,6 +45,15 @@ features = [
|
||||||
"sqlx-sqlite","sqlx-mysql","with-chrono"
|
"sqlx-sqlite","sqlx-mysql","with-chrono"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1.8.0"
|
||||||
|
features = [
|
||||||
|
"v4",
|
||||||
|
"v7",
|
||||||
|
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vcpkg = "0.2.15"
|
vcpkg = "0.2.15"
|
||||||
|
|
||||||
|
|
|
||||||
11
build.rs
11
build.rs
|
|
@ -1,11 +0,0 @@
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn main() {
|
|
||||||
vcpkg::Config::new()
|
|
||||||
.emit_includes(true)
|
|
||||||
.copy_dlls(true)
|
|
||||||
.find_package("libpq")
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn main() {}
|
|
||||||
7
migration/Cargo.lock
generated
7
migration/Cargo.lock
generated
|
|
@ -583,6 +583,12 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
|
@ -1129,6 +1135,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dotenv",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ path = "src/lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
[dependencies.sea-orm-migration]
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
use dotenv::dotenv;
|
||||||
use sea_orm_migration::prelude::*;
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
dotenv().ok();
|
||||||
cli::run_cli(migration::Migrator).await;
|
cli::run_cli(migration::Migrator).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
nix-bootstrap.sh
Executable file
2
nix-bootstrap.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
nix run .#ls-ap-migration
|
||||||
|
nix run .#lysand-ap-layer
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
person::DbUser,
|
person::DbUser,
|
||||||
post::{DbPost, Note},
|
post::{DbPost, Note},
|
||||||
},
|
},
|
||||||
utils::generate_object_id,
|
utils::generate_random_object_id,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
activity_sending::SendActivityTask,
|
activity_sending::SendActivityTask,
|
||||||
|
|
@ -39,7 +39,7 @@ impl CreatePost {
|
||||||
to: note.to.clone(),
|
to: note.to.clone(),
|
||||||
object: note,
|
object: note,
|
||||||
kind: CreateType::Create,
|
kind: CreateType::Create,
|
||||||
id: generate_object_id(data.domain())?,
|
id: generate_random_object_id(data.domain())?,
|
||||||
};
|
};
|
||||||
let create_with_context = WithContext::new_default(create);
|
let create_with_context = WithContext::new_default(create);
|
||||||
let sends = SendActivityTask::prepare(
|
let sends = SendActivityTask::prepare(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
database::StateHandle,
|
database::StateHandle,
|
||||||
entities::{follow_relation, post, user},
|
entities::{follow_relation, post, user},
|
||||||
error,
|
error,
|
||||||
utils::{generate_follow_accept_id, generate_object_id},
|
utils::{generate_follow_accept_id, generate_random_object_id},
|
||||||
DB,
|
DB,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl Follow {
|
||||||
actor: local_user.clone(),
|
actor: local_user.clone(),
|
||||||
object: followee.clone(),
|
object: followee.clone(),
|
||||||
kind: FollowType::Follow,
|
kind: FollowType::Follow,
|
||||||
id: generate_object_id(data.domain())?,
|
id: generate_random_object_id(data.domain())?,
|
||||||
};
|
};
|
||||||
let create_with_context = WithContext::new_default(create);
|
let create_with_context = WithContext::new_default(create);
|
||||||
let sends = SendActivityTask::prepare(
|
let sends = SendActivityTask::prepare(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ pub struct Model {
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
pub created_at: chrono::DateTime<Utc>,
|
||||||
#[sea_orm(column_type = "Timestamp")]
|
#[sea_orm(column_type = "Timestamp")]
|
||||||
pub updated_at: Option<chrono::DateTime<Utc>>,
|
pub updated_at: Option<chrono::DateTime<Utc>>,
|
||||||
#[sea_orm(column_type = "Timestamp")]
|
pub reblog_id: Option<String>,
|
||||||
pub reblog_id: Option<chrono::DateTime<Utc>>,
|
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
pub visibility: String,
|
pub visibility: String,
|
||||||
pub reply_id: Option<String>,
|
pub reply_id: Option<String>,
|
||||||
|
|
|
||||||
10
src/http.rs
10
src/http.rs
|
|
@ -2,6 +2,7 @@ use crate::{
|
||||||
database::StateHandle,
|
database::StateHandle,
|
||||||
entities::user,
|
entities::user,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
lysand::{self, conversion::receive_lysand_note},
|
||||||
objects::person::{DbUser, PersonAcceptedActivities},
|
objects::person::{DbUser, PersonAcceptedActivities},
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
|
|
@ -36,6 +37,15 @@ pub fn listen(config: &FederationConfig<StateHandle>) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lysand_inbox(
|
||||||
|
note: web::Json<lysand::objects::Note>,
|
||||||
|
id: web::Path<String>,
|
||||||
|
data: Data<StateHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
tokio::spawn(receive_lysand_note(note.into_inner(), id.into_inner()));
|
||||||
|
Ok(HttpResponse::Created().finish())
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles requests to fetch system user json over HTTP
|
/// Handles requests to fetch system user json over HTTP
|
||||||
/*pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
|
/*pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
|
||||||
let json_user = data.system_user.clone().into_json(&data).await?;
|
let json_user = data.system_user.clone().into_json(&data).await?;
|
||||||
|
|
|
||||||
239
src/lysand/conversion.rs
Normal file
239
src/lysand/conversion.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
use activitypub_federation::{fetch::object_id::ObjectId, http_signatures::generate_actor_keypair};
|
||||||
|
use activitystreams_kinds::public;
|
||||||
|
use anyhow::{anyhow, Ok};
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::State,
|
||||||
|
entities::{self, post, prelude, user},
|
||||||
|
objects::post::Mention,
|
||||||
|
utils::{generate_object_id, generate_user_id},
|
||||||
|
API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
objects::{ContentFormat, Note},
|
||||||
|
superx::request_client,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::User> {
|
||||||
|
let req_client = request_client();
|
||||||
|
let request = req_client.get(url).send().await?;
|
||||||
|
Ok(request.json::<super::objects::User>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn option_content_format_text(opt: Option<ContentFormat>) -> Option<String> {
|
||||||
|
if let Some(format) = opt {
|
||||||
|
return Some(format.select_rich_text().await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[async_recursion]
|
||||||
|
pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model> {
|
||||||
|
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
||||||
|
return Err(anyhow!("not lysands domain"));
|
||||||
|
}
|
||||||
|
let str_url = url.to_string();
|
||||||
|
let post_res: Option<post::Model> = prelude::Post::find()
|
||||||
|
.filter(entities::post::Column::Url.eq(str_url.clone()))
|
||||||
|
.one(DB.get().unwrap())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(post) = post_res {
|
||||||
|
Ok(post)
|
||||||
|
} else {
|
||||||
|
let post = fetch_note_from_url(url.clone()).await?;
|
||||||
|
let res = receive_lysand_note(post, "https://ap.lysand.org/example".to_string()).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model> {
|
||||||
|
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
||||||
|
return Err(anyhow!("not lysands domain"));
|
||||||
|
}
|
||||||
|
let user_res: Option<user::Model> = prelude::User::find()
|
||||||
|
.filter(entities::user::Column::Url.eq(url.to_string()))
|
||||||
|
.one(DB.get().unwrap())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(user) = user_res {
|
||||||
|
Ok(user)
|
||||||
|
} else {
|
||||||
|
let ls_user = fetch_user_from_url(url).await?;
|
||||||
|
let keypair = generate_actor_keypair()?;
|
||||||
|
let user = entities::user::ActiveModel {
|
||||||
|
id: Set(ls_user.id.to_string()),
|
||||||
|
username: Set(ls_user.username.clone()),
|
||||||
|
name: Set(ls_user.display_name.unwrap_or(ls_user.username)),
|
||||||
|
inbox: Set(ls_user.inbox.to_string()),
|
||||||
|
public_key: Set(keypair.public_key.clone()),
|
||||||
|
private_key: Set(Some(keypair.private_key.clone())),
|
||||||
|
last_refreshed_at: Set(Utc::now()),
|
||||||
|
follower_count: Set(0),
|
||||||
|
following_count: Set(0),
|
||||||
|
url: Set(ls_user.uri.to_string()),
|
||||||
|
local: Set(true),
|
||||||
|
created_at: Set(
|
||||||
|
DateTime::from_timestamp(ls_user.created_at.unix_timestamp(), 0).unwrap(),
|
||||||
|
),
|
||||||
|
summary: Set(option_content_format_text(ls_user.bio).await),
|
||||||
|
updated_at: Set(Some(Utc::now())),
|
||||||
|
followers: Set(Some(ls_user.followers.to_string())),
|
||||||
|
following: Set(Some(ls_user.following.to_string())),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let db = DB.get().unwrap();
|
||||||
|
Ok(user.insert(db).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_note_from_url(url: Url) -> anyhow::Result<super::objects::Note> {
|
||||||
|
let req_client = request_client();
|
||||||
|
let request = req_client.get(url).send().await?;
|
||||||
|
Ok(request.json::<super::objects::Note>().await?)
|
||||||
|
}
|
||||||
|
#[async_recursion]
|
||||||
|
pub async fn receive_lysand_note(
|
||||||
|
note: Note,
|
||||||
|
db_id: String,
|
||||||
|
) -> anyhow::Result<entities::post::Model> {
|
||||||
|
let lysand_author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
|
||||||
|
let user_res = prelude::User::find_by_id(db_id)
|
||||||
|
.one(DB.get().unwrap())
|
||||||
|
.await;
|
||||||
|
if user_res.is_err() {
|
||||||
|
println!("{}", user_res.as_ref().unwrap_err());
|
||||||
|
return Err(user_res.err().unwrap().into());
|
||||||
|
}
|
||||||
|
if let Some(target) = user_res? {
|
||||||
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
|
let id: ObjectId<post::Model> =
|
||||||
|
generate_object_id(data.domain(), ¬e.id.to_string())?.into();
|
||||||
|
let user_id = generate_user_id(data.domain(), &target.id.to_string())?;
|
||||||
|
let user = fetch_user_from_url(note.author.clone()).await?;
|
||||||
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
|
let mut tag: Vec<Mention> = Vec::new();
|
||||||
|
for l_tag in note.mentions.clone().unwrap_or_default() {
|
||||||
|
tag.push(Mention {
|
||||||
|
href: l_tag, //TODO convert to ap url
|
||||||
|
kind: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let to = match note
|
||||||
|
.visibility
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(super::objects::VisibilityType::Public)
|
||||||
|
{
|
||||||
|
super::objects::VisibilityType::Public => {
|
||||||
|
vec![public(), Url::parse(&user.followers.to_string().as_str())?]
|
||||||
|
}
|
||||||
|
super::objects::VisibilityType::Followers => {
|
||||||
|
vec![Url::parse(&user.followers.to_string().as_str())?]
|
||||||
|
}
|
||||||
|
super::objects::VisibilityType::Direct => note.mentions.unwrap_or_default(),
|
||||||
|
super::objects::VisibilityType::Unlisted => {
|
||||||
|
vec![Url::parse(&user.followers.to_string().as_str())?]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cc = match note
|
||||||
|
.visibility
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(super::objects::VisibilityType::Public)
|
||||||
|
{
|
||||||
|
super::objects::VisibilityType::Unlisted => Some(vec![public()]),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let reply: Option<ObjectId<entities::post::Model>> =
|
||||||
|
if let Some(rep) = note.replies_to.clone() {
|
||||||
|
let note = fetch_note_from_url(rep).await?;
|
||||||
|
let fake_rep_url = Url::parse(&format!(
|
||||||
|
"https://{}/apbridge/object/{}",
|
||||||
|
API_DOMAIN.to_string(),
|
||||||
|
¬e.id.to_string()
|
||||||
|
))?;
|
||||||
|
Some(fake_rep_url.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let quote: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.quotes.clone()
|
||||||
|
{
|
||||||
|
let note = fetch_note_from_url(rep).await?;
|
||||||
|
let fake_rep_url = Url::parse(&format!(
|
||||||
|
"https://{}/apbridge/object/{}",
|
||||||
|
API_DOMAIN.to_string(),
|
||||||
|
¬e.id.to_string()
|
||||||
|
))?;
|
||||||
|
Some(fake_rep_url.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let reply_uuid: Option<String> = if let Some(rep) = note.replies_to.clone() {
|
||||||
|
Some(db_post_from_url(rep).await?.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let quote_uuid: Option<String> = if let Some(rep) = note.quotes.clone() {
|
||||||
|
Some(db_post_from_url(rep).await?.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let ap_note = crate::objects::post::Note {
|
||||||
|
kind: Default::default(),
|
||||||
|
id,
|
||||||
|
sensitive: note.is_sensitive.unwrap_or(false),
|
||||||
|
cc,
|
||||||
|
to,
|
||||||
|
tag,
|
||||||
|
attributed_to: Url::parse(user.uri.clone().as_str()).unwrap().into(),
|
||||||
|
content: option_content_format_text(note.content)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default(),
|
||||||
|
in_reply_to: reply.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let visibility = match note
|
||||||
|
.visibility
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(super::objects::VisibilityType::Public)
|
||||||
|
{
|
||||||
|
super::objects::VisibilityType::Public => "public",
|
||||||
|
super::objects::VisibilityType::Followers => "followers",
|
||||||
|
super::objects::VisibilityType::Direct => "direct",
|
||||||
|
super::objects::VisibilityType::Unlisted => "unlisted",
|
||||||
|
};
|
||||||
|
if let Some(obj) = note.replies_to {
|
||||||
|
println!("Quoting: {}", db_post_from_url(obj).await?.url);
|
||||||
|
}
|
||||||
|
if let Some(obj) = note.quotes {
|
||||||
|
println!("Replying to: {}", db_post_from_url(obj).await?.url);
|
||||||
|
}
|
||||||
|
let post = entities::post::ActiveModel {
|
||||||
|
id: Set(note.id.to_string()),
|
||||||
|
creator: Set(lysand_author.id.clone()),
|
||||||
|
content: Set(ap_note.content.clone()),
|
||||||
|
sensitive: Set(ap_note.sensitive),
|
||||||
|
created_at: Set(Utc
|
||||||
|
.timestamp_micros(note.created_at.unix_timestamp())
|
||||||
|
.unwrap()),
|
||||||
|
local: Set(true),
|
||||||
|
updated_at: Set(Some(Utc::now())),
|
||||||
|
content_type: Set("Note".to_string()),
|
||||||
|
visibility: Set(visibility.to_string()),
|
||||||
|
title: Set(note.subject.clone()),
|
||||||
|
url: Set(note.uri.clone().to_string()),
|
||||||
|
reply_id: Set(reply_uuid),
|
||||||
|
quoting_id: Set(quote_uuid),
|
||||||
|
spoiler_text: Set(note.subject),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let res = post.insert(DB.get().unwrap()).await?;
|
||||||
|
Ok(res)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("User not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/lysand/funcs.rs
Normal file
1
src/lysand/funcs.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
37
src/lysand/http.rs
Normal file
37
src/lysand/http.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use activitypub_federation::{traits::Object, FEDERATION_CONTENT_TYPE};
|
||||||
|
use actix_web::{get, web, HttpResponse};
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::State,
|
||||||
|
entities::{
|
||||||
|
post::{self, Entity},
|
||||||
|
prelude,
|
||||||
|
},
|
||||||
|
error, Response, DB, FEDERATION_CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/apbridge/object/{post}")]
|
||||||
|
async fn fetch_post(
|
||||||
|
path: web::Path<String>,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||||
|
let db = DB.get().unwrap();
|
||||||
|
|
||||||
|
let post = prelude::Post::find()
|
||||||
|
.filter(post::Column::Id.eq(path.as_str()))
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let post = match post {
|
||||||
|
Some(post) => post,
|
||||||
|
None => return Ok(HttpResponse::NotFound().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(FEDERATION_CONTENT_TYPE)
|
||||||
|
.json(
|
||||||
|
post.into_json(&FEDERATION_CONFIG.get().unwrap().to_request_data())
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
pub mod conversion;
|
||||||
|
pub mod funcs;
|
||||||
|
pub mod http;
|
||||||
pub mod objects;
|
pub mod objects;
|
||||||
pub mod superx;
|
pub mod superx;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use time::{
|
||||||
OffsetDateTime,
|
OffsetDateTime,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
const FORMAT: Iso8601<6651332276412969266533270467398074368> = Iso8601::<
|
const FORMAT: Iso8601<6651332276412969266533270467398074368> = Iso8601::<
|
||||||
{
|
{
|
||||||
|
|
@ -49,6 +50,26 @@ pub enum LysandType {
|
||||||
ServerMetadata,
|
ServerMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub enum CategoryType {
|
||||||
|
Microblog,
|
||||||
|
Forum,
|
||||||
|
Blog,
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
Messaging,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum VisibilityType {
|
||||||
|
Public,
|
||||||
|
Unlisted,
|
||||||
|
Followers,
|
||||||
|
Direct,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum LysandExtensions {
|
pub enum LysandExtensions {
|
||||||
#[serde(rename = "org.lysand:microblogging/Announce")]
|
#[serde(rename = "org.lysand:microblogging/Announce")]
|
||||||
|
|
@ -88,10 +109,55 @@ pub struct ContentHash {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ContentFormat {
|
pub struct ContentFormat {
|
||||||
x: HashMap<String, ContentEntry>,
|
x: HashMap<String, ContentEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ContentFormat {
|
||||||
|
pub async fn select_rich_text(&self) -> anyhow::Result<String> {
|
||||||
|
if let Some(entry) = self.x.get("text/x.misskeymarkdown") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("text/html") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("text/markdown") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("text/plain") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn select_rich_img(&self) -> anyhow::Result<String> {
|
||||||
|
if let Some(entry) = self.x.get("image/webp") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/png") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/avif") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/jxl") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/jpeg") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/gif") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
if let Some(entry) = self.x.get("image/bmp") {
|
||||||
|
return Ok(entry.content.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for ContentFormat {
|
impl Serialize for ContentFormat {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|
@ -135,25 +201,74 @@ pub struct ContentEntry {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
public_key: PublicKey,
|
pub public_key: PublicKey,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
rtype: LysandType,
|
pub rtype: LysandType,
|
||||||
id: String,
|
pub id: Uuid,
|
||||||
uri: Url,
|
pub uri: Url,
|
||||||
#[serde(with = "iso_lysand")]
|
#[serde(with = "iso_lysand")]
|
||||||
created_at: OffsetDateTime,
|
pub created_at: OffsetDateTime,
|
||||||
display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
// TODO bio: Option<String>,
|
// TODO bio: Option<String>,
|
||||||
inbox: Url,
|
pub inbox: Url,
|
||||||
outbox: Url,
|
pub outbox: Url,
|
||||||
featured: Url,
|
pub featured: Url,
|
||||||
followers: Url,
|
pub followers: Url,
|
||||||
following: Url,
|
pub following: Url,
|
||||||
likes: Url,
|
pub likes: Url,
|
||||||
dislikes: Url,
|
pub dislikes: Url,
|
||||||
username: String,
|
pub username: String,
|
||||||
bio: Option<ContentFormat>,
|
pub bio: Option<ContentFormat>,
|
||||||
avatar: Option<ContentFormat>,
|
pub avatar: Option<ContentFormat>,
|
||||||
header: Option<ContentFormat>,
|
pub header: Option<ContentFormat>,
|
||||||
fields: Option<Vec<FieldKV>>,
|
pub fields: Option<Vec<FieldKV>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct LinkPreview {
|
||||||
|
description: String,
|
||||||
|
title: String,
|
||||||
|
link: Url,
|
||||||
|
image: Option<Url>,
|
||||||
|
icon: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Note {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub rtype: LysandType,
|
||||||
|
pub id: Uuid,
|
||||||
|
pub uri: Url,
|
||||||
|
pub author: Url,
|
||||||
|
#[serde(with = "iso_lysand")]
|
||||||
|
pub created_at: OffsetDateTime,
|
||||||
|
pub category: Option<CategoryType>,
|
||||||
|
pub content: Option<ContentFormat>,
|
||||||
|
pub device: Option<DeviceInfo>,
|
||||||
|
pub previews: Option<Vec<LinkPreview>>,
|
||||||
|
pub group: Option<String>,
|
||||||
|
pub attachments: Option<Vec<ContentFormat>>,
|
||||||
|
pub replies_to: Option<Url>,
|
||||||
|
pub quotes: Option<Url>,
|
||||||
|
pub mentions: Option<Vec<Url>>,
|
||||||
|
pub subject: Option<String>,
|
||||||
|
pub is_sensitive: Option<bool>,
|
||||||
|
pub visibility: Option<VisibilityType>,
|
||||||
|
//TODO extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Outbox {
|
||||||
|
pub first: Url,
|
||||||
|
pub last: Url,
|
||||||
|
pub next: Option<Url>,
|
||||||
|
pub prev: Option<Url>,
|
||||||
|
pub items: Vec<Note>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,26 @@ pub async fn serialize_lysand_type(
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn deserialize_note(data: String) -> anyhow::Result<super::objects::Note> {
|
||||||
|
let post: super::objects::Note = serde_json::from_str(&data)?;
|
||||||
|
Ok(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serialize_note(post: super::objects::Note) -> anyhow::Result<String> {
|
||||||
|
let data = serde_json::to_string(&SortAlphabetically(&post))?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn deserialize_outbox(data: String) -> anyhow::Result<super::objects::Outbox> {
|
||||||
|
let outbox: super::objects::Outbox = serde_json::from_str(&data)?;
|
||||||
|
Ok(outbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serialize_outbox(outbox: super::objects::Outbox) -> anyhow::Result<String> {
|
||||||
|
let data = serde_json::to_string(&SortAlphabetically(&outbox))?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn request_client() -> reqwest::Client {
|
pub fn request_client() -> reqwest::Client {
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,24 @@ use crate::lysand::objects::SortAlphabetically;
|
||||||
|
|
||||||
use super::superx::request_client;
|
use super::superx::request_client;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_user_serial() {
|
||||||
|
let client = request_client();
|
||||||
|
let response = client
|
||||||
|
.get("https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let user = super::superx::deserialize_user(response.text().await.unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let response_outbox = client.get(user.outbox.as_str()).send().await.unwrap();
|
||||||
|
let outbox = super::superx::deserialize_outbox(response_outbox.text().await.unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(outbox.items.len() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn main() -> anyhow::Result<()> {
|
pub async fn main() -> anyhow::Result<()> {
|
||||||
let client = request_client();
|
let client = request_client();
|
||||||
|
|
||||||
|
|
@ -22,5 +40,23 @@ pub async fn main() -> anyhow::Result<()> {
|
||||||
let user_json = serde_json::to_string_pretty(&SortAlphabetically(&user))?;
|
let user_json = serde_json::to_string_pretty(&SortAlphabetically(&user))?;
|
||||||
println!("{}", user_json);
|
println!("{}", user_json);
|
||||||
|
|
||||||
|
let response_outbox = client.get(user.outbox.as_str()).send().await?;
|
||||||
|
|
||||||
|
let outbox_json = response_outbox.text().await?;
|
||||||
|
let outbox = super::superx::deserialize_outbox(outbox_json).await?;
|
||||||
|
|
||||||
|
println!("\n\n\nOutbox: ");
|
||||||
|
print!("{:#?}", outbox);
|
||||||
|
|
||||||
|
println!("\n\n\nas AP:");
|
||||||
|
for item in outbox.items {
|
||||||
|
let ap_item = super::conversion::receive_lysand_note(
|
||||||
|
item,
|
||||||
|
"https://ap.lysand.org/example".to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
println!("{:#?}", ap_item);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
src/main.rs
51
src/main.rs
|
|
@ -28,13 +28,14 @@ use tokio::signal;
|
||||||
use tracing::{info, instrument::WithSubscriber};
|
use tracing::{info, instrument::WithSubscriber};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::utils::generate_object_id;
|
use crate::utils::generate_random_object_id;
|
||||||
use crate::{
|
use crate::{
|
||||||
activities::create_post::CreatePost,
|
activities::create_post::CreatePost,
|
||||||
database::{Config, State},
|
database::{Config, State},
|
||||||
objects::post::{Mention, Note},
|
objects::post::{Mention, Note},
|
||||||
};
|
};
|
||||||
use crate::{activities::follow::Follow, entities::user};
|
use crate::{activities::follow::Follow, entities::user};
|
||||||
|
use dotenv::dotenv;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
mod activities;
|
mod activities;
|
||||||
|
|
@ -76,29 +77,31 @@ async fn post_manually(
|
||||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||||
let local_user = state.local_user().await?;
|
let local_user = state.local_user().await?;
|
||||||
let data = FEDERATION_CONFIG.get().unwrap();
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
let creator =
|
let target =
|
||||||
webfinger_resolve_actor::<State, user::Model>(path.0.as_str(), &data.to_request_data())
|
webfinger_resolve_actor::<State, user::Model>(path.0.as_str(), &data.to_request_data())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mention = Mention {
|
let mention = Mention {
|
||||||
href: Url::parse(&creator.id)?,
|
href: Url::parse(&target.id)?,
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
};
|
};
|
||||||
let id: ObjectId<post::Model> = generate_object_id(data.domain())?.into();
|
// TODO change
|
||||||
|
let id: ObjectId<post::Model> = generate_random_object_id(data.domain())?.into();
|
||||||
let note = Note {
|
let note = Note {
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
id,
|
id,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
attributed_to: Url::parse(&data.local_user().await?.id).unwrap().into(),
|
attributed_to: Url::parse(&local_user.id).unwrap().into(),
|
||||||
to: vec![public()],
|
to: vec![public()],
|
||||||
content: format!("{} {}", path.1, creator.name),
|
content: format!("{} {}", path.1, target.name),
|
||||||
tag: vec![mention],
|
tag: vec![mention],
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
cc: vec![].into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
CreatePost::send(
|
CreatePost::send(
|
||||||
note,
|
note,
|
||||||
creator.shared_inbox_or_inbox(),
|
target.shared_inbox_or_inbox(),
|
||||||
&data.to_request_data(),
|
&data.to_request_data(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -131,16 +134,18 @@ async fn follow_manually(
|
||||||
Ok(HttpResponse::Ok().json(Response { health: true }))
|
Ok(HttpResponse::Ok().json(Response { health: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOMAIN_DEF: &str = "example.com";
|
const DOMAIN_DEF: &str = "social.lysand.org";
|
||||||
const LOCAL_USER_NAME: &str = "example";
|
const LOCAL_USER_NAME: &str = "apservice";
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SERVER_URL: String = env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string());
|
static ref SERVER_URL: String = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_string());
|
||||||
static ref DATABASE_URL: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
static ref DATABASE_URL: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
static ref USERNAME: String =
|
static ref USERNAME: String =
|
||||||
env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string());
|
env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string());
|
||||||
static ref API_DOMAIN: String = env::var("API_DOMAIN").unwrap_or(DOMAIN_DEF.to_string());
|
static ref API_DOMAIN: String = env::var("API_DOMAIN").expect("not set API_DOMAIN");
|
||||||
static ref FEDERATED_DOMAIN: String = env::var("FEDERATED_DOMAIN").unwrap_or(DOMAIN_DEF.to_string());
|
static ref LYSAND_DOMAIN: String = env::var("LYSAND_DOMAIN").expect("not set LYSAND_DOMAIN");
|
||||||
|
static ref FEDERATED_DOMAIN: String =
|
||||||
|
env::var("FEDERATED_DOMAIN").unwrap_or(API_DOMAIN.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
|
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
|
||||||
|
|
@ -148,12 +153,9 @@ static FEDERATION_CONFIG: OnceLock<FederationConfig<State>> = OnceLock::new();
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
|
dotenv().ok();
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
//TODO remove this
|
|
||||||
//lysand::test::main().await?;
|
|
||||||
//return Ok(());
|
|
||||||
|
|
||||||
let ap_id = Url::parse(&format!(
|
let ap_id = Url::parse(&format!(
|
||||||
"https://{}/{}",
|
"https://{}/{}",
|
||||||
API_DOMAIN.to_string(),
|
API_DOMAIN.to_string(),
|
||||||
|
|
@ -204,7 +206,7 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = FederationConfig::builder()
|
let data = FederationConfig::builder()
|
||||||
.domain(env::var("FEDERATED_DOMAIN").expect("FEDERATED_DOMAIN must be set"))
|
.domain(FEDERATED_DOMAIN.to_string())
|
||||||
.app_data(state.clone())
|
.app_data(state.clone())
|
||||||
.http_signature_compat(true)
|
.http_signature_compat(true)
|
||||||
.signed_fetch_actor(&state.local_user().await.unwrap())
|
.signed_fetch_actor(&state.local_user().await.unwrap())
|
||||||
|
|
@ -214,18 +216,9 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
let _ = FEDERATION_CONFIG.set(data.clone());
|
let _ = FEDERATION_CONFIG.set(data.clone());
|
||||||
|
|
||||||
let mut labels = HashMap::new();
|
let mut labels = HashMap::new();
|
||||||
labels.insert(
|
labels.insert("domain".to_string(), FEDERATED_DOMAIN.to_string());
|
||||||
"domain".to_string(),
|
labels.insert("name".to_string(), USERNAME.to_string());
|
||||||
env::var("FEDERATED_DOMAIN")
|
labels.insert("api_domain".to_string(), API_DOMAIN.to_string());
|
||||||
.expect("FEDERATED_DOMAIN must be set")
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
labels.insert(
|
|
||||||
"name".to_string(),
|
|
||||||
env::var("LOCAL_USER_NAME")
|
|
||||||
.expect("LOCAL_USER_NAME must be set")
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let prometheus = PrometheusMetricsBuilder::new("api")
|
let prometheus = PrometheusMetricsBuilder::new("api")
|
||||||
.endpoint("/metrics")
|
.endpoint("/metrics")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
database::StateHandle,
|
database::StateHandle,
|
||||||
entities::{post, user},
|
entities::{post, user},
|
||||||
error::Error,
|
error::Error,
|
||||||
|
lysand::conversion::db_user_from_url,
|
||||||
objects::person::DbUser,
|
objects::person::DbUser,
|
||||||
utils::generate_object_id,
|
utils::generate_object_id,
|
||||||
};
|
};
|
||||||
|
|
@ -14,7 +15,7 @@ use activitypub_federation::{
|
||||||
traits::{Actor, Object},
|
traits::{Actor, Object},
|
||||||
};
|
};
|
||||||
use activitystreams_kinds::link::MentionType;
|
use activitystreams_kinds::link::MentionType;
|
||||||
use sea_orm::{ActiveModelTrait, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -40,6 +41,7 @@ pub struct Note {
|
||||||
pub(crate) in_reply_to: Option<ObjectId<post::Model>>,
|
pub(crate) in_reply_to: Option<ObjectId<post::Model>>,
|
||||||
pub(crate) tag: Vec<Mention>,
|
pub(crate) tag: Vec<Mention>,
|
||||||
pub(crate) sensitive: bool,
|
pub(crate) sensitive: bool,
|
||||||
|
pub(crate) cc: Option<Vec<Url>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
@ -56,14 +58,42 @@ impl Object for post::Model {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn read_from_id(
|
async fn read_from_id(
|
||||||
_object_id: Url,
|
object_id: Url,
|
||||||
_data: &Data<Self::DataType>,
|
data: &Data<Self::DataType>,
|
||||||
) -> Result<Option<Self>, Self::Error> {
|
) -> Result<Option<Self>, Self::Error> {
|
||||||
Ok(None)
|
let post = crate::entities::prelude::Post::find()
|
||||||
|
.filter(post::Column::Id.eq(object_id.to_string()))
|
||||||
|
.one(data.app_data().database_connection.clone().as_ref())
|
||||||
|
.await;
|
||||||
|
Ok(post.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||||
todo!()
|
let creator = db_user_from_url(Url::parse(self.creator.as_str()).unwrap()).await?;
|
||||||
|
let to = match self.visibility.as_str() {
|
||||||
|
"public" => vec![
|
||||||
|
public(),
|
||||||
|
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
|
||||||
|
],
|
||||||
|
"followers" => vec![Url::parse(creator.followers.unwrap().as_str()).unwrap()],
|
||||||
|
"direct" => vec![], //TODO: implement this
|
||||||
|
"unlisted" => vec![
|
||||||
|
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
|
||||||
|
public(),
|
||||||
|
],
|
||||||
|
_ => vec![public()],
|
||||||
|
};
|
||||||
|
Ok(Note {
|
||||||
|
kind: Default::default(),
|
||||||
|
id: Url::parse(self.url.as_str()).unwrap().into(),
|
||||||
|
attributed_to: Url::parse(self.creator.as_str()).unwrap().into(),
|
||||||
|
to: to.clone(),
|
||||||
|
content: self.content,
|
||||||
|
in_reply_to: None,
|
||||||
|
tag: vec![],
|
||||||
|
sensitive: self.sensitive,
|
||||||
|
cc: Some(to),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify(
|
async fn verify(
|
||||||
|
|
|
||||||
28
src/utils.rs
28
src/utils.rs
|
|
@ -1,18 +1,24 @@
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
/// Just generate random url as object id. In a real project, you probably want to use
|
pub fn generate_object_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
|
||||||
/// an url which contains the database id for easy retrieval (or store the random id in db).
|
let id: String = uuid::Uuid::new_v4().to_string();
|
||||||
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
|
Url::parse(&format!("https://{}/apbridge/object/{}", domain, id))
|
||||||
let id: String = thread_rng()
|
}
|
||||||
.sample_iter(&Alphanumeric)
|
|
||||||
.take(7)
|
pub fn generate_user_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
|
||||||
.map(char::from)
|
let id: String = uuid::Uuid::new_v4().to_string();
|
||||||
.collect();
|
Url::parse(&format!("https://{}/apbridge/user/{}", domain, id))
|
||||||
Url::parse(&format!("https://{}/objects/{}", domain, id))
|
}
|
||||||
|
|
||||||
|
pub fn generate_random_object_id(domain: &str) -> Result<Url, ParseError> {
|
||||||
|
let id: String = uuid::Uuid::new_v4().to_string();
|
||||||
|
Url::parse(&format!("https://{}/apbridge/object/{}", domain, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a follow accept id
|
/// Generate a follow accept id
|
||||||
pub fn generate_follow_accept_id(domain: &str, db_id: i32) -> Result<Url, ParseError> {
|
pub fn generate_follow_accept_id(domain: &str, db_id: i32) -> Result<Url, ParseError> {
|
||||||
Url::parse(&format!("https://{}/activities/follow/{}", domain, db_id))
|
Url::parse(&format!(
|
||||||
|
"https://{}/apbridge/activity/follow/{}",
|
||||||
|
domain, db_id
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue