This commit is contained in:
Nutomic 2025-03-10 15:33:00 +01:00 committed by GitHub
commit 903d09a2de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 91 additions and 269 deletions

View file

@ -1,5 +1,5 @@
variables:
- &rust_image "rust:1.78-bullseye"
- &rust_image "rust:1.85-bullseye"
steps:
cargo_fmt:

View file

@ -12,7 +12,6 @@ documentation = "https://docs.rs/activitypub_federation/"
default = ["actix-web", "axum"]
actix-web = ["dep:actix-web", "dep:http02"]
axum = ["dep:axum", "dep:tower"]
diesel = ["dep:diesel"]
[lints.rust]
warnings = "deny"
@ -32,70 +31,66 @@ redundant_closure_for_method_calls = "deny"
unwrap_used = "deny"
[dependencies]
chrono = { version = "0.4.38", features = ["clock"], default-features = false }
serde = { version = "1.0.204", features = ["derive"] }
async-trait = "0.1.81"
url = { version = "2.5.2", features = ["serde"] }
serde_json = { version = "1.0.120", features = ["preserve_order"] }
reqwest = { version = "0.12.5", default-features = false, features = [
chrono = { version = "0.4.39", features = ["clock"], default-features = false }
serde = { version = "1.0.217", features = ["derive"] }
async-trait = "0.1.85"
url = { version = "2.5.4", features = ["serde"] }
serde_json = { version = "1.0.137", features = ["preserve_order"] }
reqwest = { version = "0.12.12", default-features = false, features = [
"json",
"stream",
"rustls-tls",
] }
reqwest-middleware = "0.3.2"
tracing = "0.1.40"
reqwest-middleware = "0.4.0"
tracing = "0.1.41"
base64 = "0.22.1"
rand = "0.8.5"
rsa = "0.9.6"
once_cell = "1.19.0"
http = "1.1.0"
rsa = "0.9.7"
http = "1.2.0"
sha2 = { version = "0.10.8", features = ["oid"] }
thiserror = "1.0.62"
derive_builder = "0.20.0"
itertools = "0.13.0"
thiserror = "2.0.11"
derive_builder = "0.20.2"
itertools = "0.14.0"
dyn-clone = "1.0.17"
enum_delegate = "0.2.0"
httpdate = "1.0.3"
http-signature-normalization-reqwest = { version = "0.12.0", default-features = false, features = [
http-signature-normalization-reqwest = { version = "0.13.0", default-features = false, features = [
"sha-2",
"middleware",
"default-spawner",
] }
http-signature-normalization = "0.7.0"
bytes = "1.6.1"
futures-core = { version = "0.3.30", default-features = false }
pin-project-lite = "0.2.14"
bytes = "1.9.0"
futures-core = { version = "0.3.31", default-features = false }
pin-project-lite = "0.2.16"
activitystreams-kinds = "0.3.0"
regex = { version = "1.10.5", default-features = false, features = [
regex = { version = "1.11.1", default-features = false, features = [
"std",
"unicode",
] }
tokio = { version = "1.38.0", features = [
tokio = { version = "1.43.0", features = [
"sync",
"rt",
"rt-multi-thread",
"time",
] }
diesel = { version = "2.2.1", features = [
"postgres",
], default-features = false, optional = true }
futures = "0.3.30"
moka = { version = "0.12.8", features = ["future"] }
futures = "0.3.31"
moka = { version = "0.12.10", features = ["future"] }
# Actix-web
actix-web = { version = "4.8.0", default-features = false, optional = true }
actix-web = { version = "4.9.0", default-features = false, optional = true }
http02 = { package = "http", version = "0.2.12", optional = true }
# Axum
axum = { version = "0.7.5", features = ["json"], default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
axum = { version = "0.8.1", features = ["json"], default-features = false, optional = true }
tower = { version = "0.5.2", optional = true }
[dev-dependencies]
anyhow = "1.0.86"
axum = { version = "0.7.5", features = ["macros"] }
axum-extra = { version = "0.9.3", features = ["typed-header"] }
env_logger = "0.11.3"
tokio = { version = "1.38.0", features = ["full"] }
anyhow = "1.0.95"
axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10.0", features = ["typed-header"] }
env_logger = "0.11.6"
tokio = { version = "1.43.0", features = ["full"] }
[profile.dev]
strip = "symbols"

View file

@ -26,7 +26,7 @@ pub enum SearchableObjects {
Note(Note)
}
#[async_trait::async_trait]
impl Object for SearchableDbObjects {
type DataType = DbConnection;
type Kind = SearchableObjects;

View file

@ -63,7 +63,6 @@ pub struct Person {
public_key: PublicKey,
}
#[async_trait::async_trait]
impl Object for DbUser {
type DataType = DatabaseHandle;
type Kind = Person;

View file

@ -44,7 +44,6 @@ pub struct Mention {
pub kind: MentionType,
}
#[async_trait::async_trait]
impl Object for DbPost {
type DataType = DatabaseHandle;
type Kind = Note;

View file

@ -30,8 +30,8 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
info!("Listening with axum on {hostname}");
let config = config.clone();
let app = Router::new()
.route("/:user/inbox", post(http_post_user_inbox))
.route("/:user", get(http_get_user))
.route("/{user}/inbox", post(http_post_user_inbox))
.route("/{user}", get(http_get_user))
.route("/.well-known/webfinger", get(webfinger))
.layer(FederationMiddleware::new(config));

View file

@ -128,7 +128,6 @@ impl DbUser {
}
}
#[async_trait::async_trait]
impl Object for DbUser {
type DataType = DatabaseHandle;
type Kind = Person;

View file

@ -41,7 +41,6 @@ pub struct Note {
content: String,
}
#[async_trait::async_trait]
impl Object for DbPost {
type DataType = DatabaseHandle;
type Kind = Note;

View file

@ -122,14 +122,14 @@ mod test {
let (_, _, config) = setup_receive_test().await;
let actor = Url::parse("http://ds9.lemmy.ml/u/lemmy_alpha").unwrap();
let id = "http://localhost:123/1";
let activity_id = "http://localhost:123/1";
let activity = json!({
"actor": actor.as_str(),
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/post/1",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"type": "Delete",
"id": id
"id": activity_id
}
);
let body: Bytes = serde_json::to_vec(&activity).unwrap().into();
@ -144,8 +144,8 @@ mod test {
.await;
match res {
Err(Error::ParseReceivedActivity(_, url)) => {
assert_eq!(id, url.expect("has url").as_str());
Err(Error::ParseReceivedActivity { err: _, id }) => {
assert_eq!(activity_id, id.expect("has url").as_str());
}
_ => unreachable!(),
}

View file

@ -10,7 +10,6 @@ use crate::{
traits::{ActivityHandler, Actor, Object},
};
use axum::{
async_trait,
body::Body,
extract::FromRequest,
http::{Request, StatusCode},
@ -58,7 +57,6 @@ pub struct ActivityData {
body: Vec<u8>,
}
#[async_trait]
impl<S> FromRequest<S> for ActivityData
where
S: Send + Sync,

View file

@ -1,5 +1,5 @@
use crate::config::{Data, FederationConfig, FederationMiddleware};
use axum::{async_trait, body::Body, extract::FromRequestParts, http::Request, response::Response};
use axum::{body::Body, extract::FromRequestParts, http::Request, response::Response};
use http::{request::Parts, StatusCode};
use std::task::{Context, Poll};
use tower::{Layer, Service};
@ -43,7 +43,6 @@ where
}
}
#[async_trait]
impl<S, T: Clone + 'static> FromRequestParts<S> for Data<T>
where
S: Send + Sync,

View file

@ -44,11 +44,16 @@ pub enum Error {
#[error("Failed to parse object {1} with content {2}: {0}")]
ParseFetchedObject(serde_json::Error, Url, String),
/// Failed to parse an activity received from another instance
#[error("Failed to parse incoming activity {}: {0}", match .1 {
#[error("Failed to parse incoming activity {}: {0}", match .id {
Some(t) => format!("with id {t}"),
None => String::new(),
})]
ParseReceivedActivity(serde_json::Error, Option<Url>),
ParseReceivedActivity {
/// The parse error
err: serde_json::Error,
/// ID of the Activitypub object which caused this error
id: Option<Url>,
},
/// Reqwest Middleware Error
#[error(transparent)]
ReqwestMiddleware(#[from] reqwest_middleware::Error),

View file

@ -102,92 +102,3 @@ where
self.0.eq(&other.0) && self.1 == other.1
}
}
#[cfg(feature = "diesel")]
const _IMPL_DIESEL_NEW_TYPE_FOR_COLLECTION_ID: () = {
use diesel::{
backend::Backend,
deserialize::{FromSql, FromStaticSqlRow},
expression::AsExpression,
internal::derives::as_expression::Bound,
pg::Pg,
query_builder::QueryId,
serialize,
serialize::{Output, ToSql},
sql_types::{HasSqlType, SingleValue, Text},
Expression,
Queryable,
};
// TODO: this impl only works for Postgres db because of to_string() call which requires reborrow
impl<Kind, ST> ToSql<ST, Pg> for CollectionId<Kind>
where
Kind: Collection,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
String: ToSql<ST, Pg>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
let v = self.0.to_string();
<String as ToSql<Text, Pg>>::to_sql(&v, &mut out.reborrow())
}
}
impl<'expr, Kind, ST> AsExpression<ST> for &'expr CollectionId<Kind>
where
Kind: Collection,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
Bound<ST, String>: Expression<SqlType = ST>,
ST: SingleValue,
{
type Expression = Bound<ST, &'expr str>;
fn as_expression(self) -> Self::Expression {
Bound::new(self.0.as_str())
}
}
impl<Kind, ST> AsExpression<ST> for CollectionId<Kind>
where
Kind: Collection,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
Bound<ST, String>: Expression<SqlType = ST>,
ST: SingleValue,
{
type Expression = Bound<ST, String>;
fn as_expression(self) -> Self::Expression {
Bound::new(self.0.to_string())
}
}
impl<Kind, ST, DB> FromSql<ST, DB> for CollectionId<Kind>
where
Kind: Collection + Send + 'static,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
String: FromSql<ST, DB>,
DB: Backend,
DB: HasSqlType<ST>,
{
fn from_sql(
raw: DB::RawValue<'_>,
) -> Result<Self, Box<dyn ::std::error::Error + Send + Sync>> {
let string: String = FromSql::<ST, DB>::from_sql(raw)?;
Ok(CollectionId::parse(&string)?)
}
}
impl<Kind, ST, DB> Queryable<ST, DB> for CollectionId<Kind>
where
Kind: Collection + Send + 'static,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
String: FromStaticSqlRow<ST, DB>,
DB: Backend,
DB: HasSqlType<ST>,
{
type Row = String;
fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
Ok(CollectionId::parse(&row)?)
}
}
impl<Kind> QueryId for CollectionId<Kind>
where
Kind: Collection + 'static,
for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
{
type QueryId = Self;
}
};

View file

@ -109,6 +109,7 @@ async fn fetch_object_http_with_accept<T: Clone, Kind: DeserializeOwned>(
let mut counter = data.request_counter.fetch_add(1, Ordering::SeqCst);
// fetch_add returns old value so we need to increment manually here
counter += 1;
url.to_string();
if counter > config.http_fetch_limit {
return Err(Error::RequestLimit);
}

View file

@ -144,7 +144,7 @@ where
data: &Data<<Kind as Object>::DataType>,
) -> Result<Option<Kind>, <Kind as Object>::Error> {
let id = self.0.clone();
Object::read_from_id(*id, data).await
<Kind as Object>::read_from_id(*id, data).await
}
/// Fetch object from origin instance over HTTP, then verify and parse it.
@ -271,95 +271,6 @@ where
}
}
#[cfg(feature = "diesel")]
const _IMPL_DIESEL_NEW_TYPE_FOR_OBJECT_ID: () = {
use diesel::{
backend::Backend,
deserialize::{FromSql, FromStaticSqlRow},
expression::AsExpression,
internal::derives::as_expression::Bound,
pg::Pg,
query_builder::QueryId,
serialize,
serialize::{Output, ToSql},
sql_types::{HasSqlType, SingleValue, Text},
Expression,
Queryable,
};
// TODO: this impl only works for Postgres db because of to_string() call which requires reborrow
impl<Kind, ST> ToSql<ST, Pg> for ObjectId<Kind>
where
Kind: Object,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
String: ToSql<ST, Pg>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
let v = self.0.to_string();
<String as ToSql<Text, Pg>>::to_sql(&v, &mut out.reborrow())
}
}
impl<'expr, Kind, ST> AsExpression<ST> for &'expr ObjectId<Kind>
where
Kind: Object,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
Bound<ST, String>: Expression<SqlType = ST>,
ST: SingleValue,
{
type Expression = Bound<ST, &'expr str>;
fn as_expression(self) -> Self::Expression {
Bound::new(self.0.as_str())
}
}
impl<Kind, ST> AsExpression<ST> for ObjectId<Kind>
where
Kind: Object,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
Bound<ST, String>: Expression<SqlType = ST>,
ST: SingleValue,
{
type Expression = Bound<ST, String>;
fn as_expression(self) -> Self::Expression {
Bound::new(self.0.to_string())
}
}
impl<Kind, ST, DB> FromSql<ST, DB> for ObjectId<Kind>
where
Kind: Object + Send + 'static,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
String: FromSql<ST, DB>,
DB: Backend,
DB: HasSqlType<ST>,
{
fn from_sql(
raw: DB::RawValue<'_>,
) -> Result<Self, Box<dyn ::std::error::Error + Send + Sync>> {
let string: String = FromSql::<ST, DB>::from_sql(raw)?;
Ok(ObjectId::parse(&string)?)
}
}
impl<Kind, ST, DB> Queryable<ST, DB> for ObjectId<Kind>
where
Kind: Object + Send + 'static,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
String: FromStaticSqlRow<ST, DB>,
DB: Backend,
DB: HasSqlType<ST>,
{
type Row = String;
fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
Ok(ObjectId::parse(&row)?)
}
}
impl<Kind> QueryId for ObjectId<Kind>
where
Kind: Object + 'static,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
{
type QueryId = Self;
}
};
/// Internal only
#[cfg(test)]
#[allow(clippy::unwrap_used)]

View file

@ -7,10 +7,9 @@ use crate::{
};
use http::HeaderValue;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display};
use std::{collections::HashMap, fmt::Display, sync::LazyLock};
use tracing::debug;
use url::Url;
@ -130,8 +129,8 @@ pub fn extract_webfinger_name<'i, T>(query: &'i str, data: &Data<T>) -> Result<&
where
T: Clone,
{
static WEBFINGER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^acct:([\p{L}0-9_\.\-]+)@(.*)$").expect("compile regex"));
static WEBFINGER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^acct:([\p{L}0-9_\.\-]+)@(.*)$").expect("compile regex"));
// Regex to extract usernames from webfinger query. Supports different alphabets using `\p{L}`.
// TODO: This should use a URL parser
let captures = WEBFINGER_REGEX

View file

@ -19,7 +19,6 @@ use http_signature_normalization_reqwest::{
prelude::{Config, SignExt},
DefaultSpawner,
};
use once_cell::sync::Lazy;
use reqwest::Request;
use reqwest_middleware::RequestBuilder;
use rsa::{
@ -30,7 +29,7 @@ use rsa::{
};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, fmt::Debug, time::Duration};
use std::{collections::BTreeMap, fmt::Debug, sync::LazyLock, time::Duration};
use tracing::debug;
use url::Url;
@ -82,9 +81,9 @@ pub(crate) async fn sign_request(
private_key: RsaPrivateKey,
http_signature_compat: bool,
) -> Result<Request, Error> {
static CONFIG: Lazy<Config<DefaultSpawner>> =
Lazy::new(|| Config::new().set_expiration(EXPIRES_AFTER));
static CONFIG_COMPAT: Lazy<Config> = Lazy::new(|| {
static CONFIG: LazyLock<Config<DefaultSpawner>> =
LazyLock::new(|| Config::new().set_expiration(EXPIRES_AFTER));
static CONFIG_COMPAT: LazyLock<Config> = LazyLock::new(|| {
Config::new()
.mastodon_compat()
.set_expiration(EXPIRES_AFTER)
@ -185,7 +184,7 @@ fn verify_signature_inner(
uri: &Uri,
public_key: &str,
) -> Result<(), Error> {
static CONFIG: Lazy<http_signature_normalization::Config> = Lazy::new(|| {
static CONFIG: LazyLock<http_signature_normalization::Config> = LazyLock::new(|| {
http_signature_normalization::Config::new()
.set_expiration(EXPIRES_AFTER)
.require_digest()
@ -288,9 +287,10 @@ pub mod test {
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey};
use std::str::FromStr;
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
static INBOX_URL: Lazy<Url> =
Lazy::new(|| Url::parse("https://example.com/u/alice/inbox").unwrap());
static ACTOR_ID: LazyLock<Url> =
LazyLock::new(|| Url::parse("https://example.com/u/alice").unwrap());
static INBOX_URL: LazyLock<Url> =
LazyLock::new(|| Url::parse("https://example.com/u/alice/inbox").unwrap());
#[tokio::test]
async fn test_sign() {

View file

@ -52,10 +52,10 @@ where
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
let activity: Activity = serde_json::from_slice(body).map_err(|e| {
let activity: Activity = serde_json::from_slice(body).map_err(|err| {
// Attempt to include activity id in error message
let id = extract_id(body).ok();
Error::ParseReceivedActivity(e, id)
Error::ParseReceivedActivity { err, id }
})?;
data.config.verify_url_and_domain(&activity).await?;
let actor = ObjectId::<ActorT>::from(activity.actor().clone())

View file

@ -43,7 +43,7 @@ use url::Url;
/// content: String,
/// }
///
/// #[async_trait::async_trait]
///
/// impl Object for DbPost {
/// type DataType = DbConnection;
/// type Kind = Note;
@ -93,7 +93,6 @@ use url::Url;
/// }
///
/// }
#[async_trait]
pub trait Object: Sized + Debug {
/// App data type passed to handlers. Must be identical to
/// [crate::config::FederationConfigBuilder::app_data] type.
@ -119,23 +118,29 @@ pub trait Object: Sized + Debug {
/// Try to read the object with given `id` from local database.
///
/// Should return `Ok(None)` if not found.
async fn read_from_id(
fn read_from_id(
object_id: Url,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error>;
) -> impl std::future::Future<Output = Result<Option<Self>, Self::Error>> + Send;
/// Mark remote object as deleted in local database.
///
/// Called when a `Delete` activity is received, or if fetch returns a `Tombstone` object.
async fn delete(self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
fn delete(
self,
_data: &Data<Self::DataType>,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
/// Convert database type to Activitypub type.
///
/// Called when a local object gets fetched by another instance over HTTP, or when an object
/// gets sent in an activity.
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error>;
fn into_json(
self,
data: &Data<Self::DataType>,
) -> impl std::future::Future<Output = Result<Self::Kind, Self::Error>> + Send;
/// Verifies that the received object is valid.
///
@ -144,18 +149,21 @@ pub trait Object: Sized + Debug {
///
/// It is necessary to use a separate method for this, because it might be used for activities
/// like `Delete/Note`, which shouldn't perform any database write for the inner `Note`.
async fn verify(
fn verify(
json: &Self::Kind,
expected_domain: &Url,
data: &Data<Self::DataType>,
) -> Result<(), Self::Error>;
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
/// Convert object from ActivityPub type to database type.
///
/// Called when an object is received from HTTP fetch or as part of an activity. This method
/// should write the received object to database. Note that there is no distinction between
/// create and update, so an `upsert` operation should be used.
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error>;
fn from_json(
json: Self::Kind,
data: &Data<Self::DataType>,
) -> impl std::future::Future<Output = Result<Self, Self::Error>> + Send;
}
/// Handler for receiving incoming activities.
@ -292,7 +300,6 @@ where
}
/// Trait for federating collections
#[async_trait]
pub trait Collection: Sized {
/// Actor or object that this collection belongs to
type Owner;
@ -305,31 +312,31 @@ pub trait Collection: Sized {
type Error;
/// Reads local collection from database and returns it as Activitypub JSON.
async fn read_local(
fn read_local(
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error>;
) -> impl std::future::Future<Output = Result<Self::Kind, Self::Error>> + Send;
/// Verifies that the received object is valid.
///
/// You should check here that the domain of id matches `expected_domain`. Additionally you
/// should perform any application specific checks.
async fn verify(
fn verify(
json: &Self::Kind,
expected_domain: &Url,
data: &Data<Self::DataType>,
) -> Result<(), Self::Error>;
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
/// Convert object from ActivityPub type to database type.
///
/// Called when an object is received from HTTP fetch or as part of an activity. This method
/// should also write the received object to database. Note that there is no distinction
/// between create and update, so an `upsert` operation should be used.
async fn from_json(
fn from_json(
json: Self::Kind,
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error>;
) -> impl std::future::Future<Output = Result<Self, Self::Error>> + Send;
}
/// Some impls of these traits for use in tests. Dont use this from external crates.
@ -338,7 +345,7 @@ pub trait Collection: Sized {
#[doc(hidden)]
#[allow(clippy::unwrap_used)]
pub mod tests {
use super::{async_trait, ActivityHandler, Actor, Data, Debug, Object, PublicKey, Url};
use super::{ActivityHandler, Actor, Data, Debug, Object, PublicKey, Url};
use crate::{
error::Error,
fetch::object_id::ObjectId,
@ -346,8 +353,9 @@ pub mod tests {
protocol::verification::verify_domains_match,
};
use activitystreams_kinds::{activity::FollowType, actor::PersonType};
use once_cell::sync::Lazy;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
#[derive(Clone)]
pub struct DbConnection;
@ -389,9 +397,10 @@ pub mod tests {
pub local: bool,
}
pub static DB_USER_KEYPAIR: Lazy<Keypair> = Lazy::new(|| generate_actor_keypair().unwrap());
pub static DB_USER_KEYPAIR: LazyLock<Keypair> =
LazyLock::new(|| generate_actor_keypair().unwrap());
pub static DB_USER: Lazy<DbUser> = Lazy::new(|| DbUser {
pub static DB_USER: LazyLock<DbUser> = LazyLock::new(|| DbUser {
name: String::new(),
federation_id: "https://localhost/123".parse().unwrap(),
inbox: "https://localhost/123/inbox".parse().unwrap(),
@ -401,7 +410,6 @@ pub mod tests {
local: false,
});
#[async_trait]
impl Object for DbUser {
type DataType = DbConnection;
type Kind = Person;
@ -505,7 +513,6 @@ pub mod tests {
#[derive(Debug, Clone)]
pub struct DbPost {}
#[async_trait]
impl Object for DbPost {
type DataType = DbConnection;
type Kind = Note;