Compare commits

...

11 Commits

Author SHA1 Message Date
Aumetra Weisman 7e1e2a2237
Merge 9018f05404 into 9911cc0658 2024-05-09 23:06:02 +02:00
Aumetra Weisman 9018f05404 format codebase 2024-05-09 23:05:59 +02:00
vesdev 987f151441 new layout 2024-05-09 23:05:59 +02:00
Aumetra Weisman 4e9c8293fe add register functionality 2024-05-09 23:05:59 +02:00
Aumetra Weisman 5f0ef5b676 frontpage overhaul 2024-05-09 23:05:59 +02:00
Aumetra Weisman 5e5e746348 add unplugin-icons 2024-05-09 23:05:59 +02:00
Aumetra Weisman 208c90c611 add graphql client 2024-05-09 23:05:59 +02:00
Aumetra Weisman 748c4f7f01 rewrite frontpage 2024-05-09 23:05:59 +02:00
Aumetra Weisman 41b777c9ef begin port 2024-05-09 23:05:59 +02:00
Aumetra Weisman 9911cc0658
Replace `std::sync::Arc` with `triomphe::Arc` (#530)
* Replace `std::sync::Arc` with `triomphe::Arc`

* Fix typo
2024-05-09 22:17:25 +02:00
Aumetra Weisman 03e8ca3cf9
Add `kitsune-derive` and use it throughout the project (#528)
* implement kitsune-derive

* Use on every service

* remove startup figlet
2024-05-09 17:24:26 +02:00
206 changed files with 7102 additions and 1198 deletions

564
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,8 @@ members = [
"crates/kitsune-config",
"crates/kitsune-core",
"crates/kitsune-db",
"crates/kitsune-derive",
"crates/kitsune-derive/impl",
"crates/kitsune-email",
"crates/kitsune-embed",
"crates/kitsune-error",
@ -122,3 +124,4 @@ install-updater = true
[patch.crates-io]
diesel-async = { git = "https://github.com/weiznich/diesel_async.git", rev = "d02798c67065d763154d7272dd0c09b39757d0f2" }
scraper = { git = "https://github.com/causal-agent/scraper.git", rev = "d67111f5cc0b7da6e6ff10e4549d87cf09ba3e5b" }

View File

@ -155,15 +155,10 @@ path = "./mrf-storage"
# OpenTelemetry configuration
#
# Kitsune supports exporting traces and metrics via the OpenTelemetry Protocol (OTLP, for short)
# Kitsune supports exporting traces via the OpenTelemetry Protocol (OTLP, for short)
# It's by now the de-facto standard wire protocol for exporting telemetry data
#
# As of now, you can only export all data to a single endpoint.
# This configuration might become more granular in the future.
#
#[opentelemetry]
#metrics-transport = "http" # "grpc" or "http"
#metrics-endpoint = "http://localhost:4317"
#tracing-transport = "http" # "grpc" or "http"
#tracing-endpoint = "http://localhost:4317"

View File

@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
async-trait = "0.1.80"
autometrics = { version = "1.0.1", default-features = false }
base64-simd = "0.8.0"
diesel = "2.1.6"
diesel-async = "0.4.1"
@ -37,6 +36,7 @@ sha2 = "0.10.8"
simd-json = "0.13.10"
speedy-uuid = { path = "../../lib/speedy-uuid" }
tracing = "0.1.40"
triomphe = "0.1.11"
typed-builder = "0.18.2"
url = "2.5.0"

View File

@ -1,4 +1,3 @@
use autometrics::autometrics;
use futures_util::{stream::FuturesUnordered, Stream, StreamExt};
use http::{Method, Request};
use kitsune_core::consts::USER_AGENT;
@ -26,7 +25,6 @@ pub struct Deliverer {
impl Deliverer {
/// Deliver the activity to an inbox
#[autometrics(track_concurrency)]
#[instrument(skip_all, fields(%inbox_url, activity_url = %activity.id))]
pub async fn deliver(
&self,

View File

@ -21,7 +21,7 @@ use kitsune_service::attachment::AttachmentService;
use kitsune_type::ap::{ap_context, Activity, ActivityType, ObjectField};
use kitsune_url::UrlService;
use kitsune_util::try_join;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
pub mod core;

View File

@ -1,6 +1,5 @@
use super::Fetcher;
use crate::process_attachments;
use autometrics::autometrics;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use kitsune_cache::CacheBackend;
@ -23,7 +22,6 @@ impl Fetcher {
///
/// - Panics if the URL doesn't contain a host section
#[instrument(skip(self))]
#[autometrics(track_concurrency)]
pub(crate) async fn fetch_actor(
&self,
opts: AccountFetchOptions<'_>,

View File

@ -6,6 +6,7 @@ use kitsune_config::language_detection::Configuration as LanguageDetectionConfig
use kitsune_core::{
consts::USER_AGENT,
traits::{
coerce::CoerceResolver,
fetcher::{AccountFetchOptions, PostFetchOptions},
Fetcher as FetcherTrait, Resolver,
},
@ -21,7 +22,7 @@ use kitsune_http_client::Client;
use kitsune_type::jsonld::RdfNode;
use mime::Mime;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
use url::Url;
@ -120,7 +121,7 @@ impl Fetcher {
#[async_trait]
impl FetcherTrait for Fetcher {
fn resolver(&self) -> Arc<dyn Resolver> {
Arc::new(self.resolver.clone())
Arc::new(self.resolver.clone()).coerce()
}
async fn fetch_account(&self, opts: AccountFetchOptions<'_>) -> Result<Option<Account>> {

View File

@ -1,6 +1,5 @@
use super::Fetcher;
use crate::{process_new_object, ProcessNewObject};
use autometrics::autometrics;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use kitsune_cache::CacheBackend;
@ -13,7 +12,6 @@ pub const MAX_FETCH_DEPTH: u32 = 15;
impl Fetcher {
#[instrument(skip(self))]
#[autometrics(track_concurrency)]
pub(crate) async fn fetch_object(&self, url: &str, call_depth: u32) -> Result<Option<Post>> {
if call_depth > MAX_FETCH_DEPTH {
return Ok(None);

View File

@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl;
use kitsune_activitypub::Fetcher;
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::Fetcher as _;
use kitsune_core::traits::{coerce::CoerceResolver, Fetcher as _};
use kitsune_db::{
model::{account::Account, media_attachment::MediaAttachment},
schema::{accounts, media_attachments},
@ -16,8 +16,8 @@ use kitsune_search::NoopSearchService;
use kitsune_test::{database_test, language_detection_config};
use kitsune_webfinger::Webfinger;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn fetch_actor() {
@ -36,10 +36,7 @@ async fn fetch_actor() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();
@ -78,7 +75,7 @@ async fn fetch_emoji() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();
@ -129,10 +126,7 @@ async fn fetch_note() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();

View File

@ -4,14 +4,15 @@ use hyper::{body::Bytes, Request, Response};
use kitsune_activitypub::Fetcher;
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::Fetcher as _;
use kitsune_core::traits::{coerce::CoerceResolver, Fetcher as _};
use kitsune_federation_filter::FederationFilter;
use kitsune_http_client::Client;
use kitsune_search::NoopSearchService;
use kitsune_test::{assert_display_eq, database_test, language_detection_config};
use kitsune_webfinger::Webfinger;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;
use tower::service_fn;
use triomphe::Arc;
macro_rules! assert_blocked {
($error:expr) => {
@ -46,10 +47,7 @@ async fn federation_allow() {
.clone()
.client(client.clone())
.language_detection_config(language_detection_config())
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.build();
assert_blocked!(fetcher
@ -67,10 +65,7 @@ async fn federation_allow() {
.clone()
.client(client.clone())
.language_detection_config(language_detection_config())
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.build();
assert!(matches!(
@ -106,10 +101,7 @@ async fn federation_deny() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();

View File

@ -4,7 +4,7 @@ use iso8601_timestamp::Timestamp;
use kitsune_activitypub::{fetcher::MAX_FETCH_DEPTH, Fetcher};
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::Fetcher as _;
use kitsune_core::traits::{coerce::CoerceResolver, Fetcher as _};
use kitsune_federation_filter::FederationFilter;
use kitsune_http_client::Client;
use kitsune_search::NoopSearchService;
@ -16,12 +16,10 @@ use kitsune_type::ap::{
use kitsune_webfinger::Webfinger;
use std::{
convert::Infallible,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
},
sync::atomic::{AtomicU32, Ordering},
};
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn fetch_infinitely_long_reply_chain() {
@ -104,7 +102,7 @@ async fn fetch_infinitely_long_reply_chain() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();

View File

@ -4,14 +4,15 @@ use hyper::Request;
use kitsune_activitypub::Fetcher;
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::Fetcher as _;
use kitsune_core::traits::{coerce::CoerceResolver, Fetcher as _};
use kitsune_federation_filter::FederationFilter;
use kitsune_http_client::Client;
use kitsune_search::NoopSearchService;
use kitsune_test::{assert_display_eq, database_test, language_detection_config};
use kitsune_webfinger::Webfinger;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn check_ap_id_authority() {
@ -38,10 +39,7 @@ async fn check_ap_id_authority() {
.clone()
.client(client.clone())
.language_detection_config(language_detection_config())
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.build();
// The mock HTTP client ensures that the fetcher doesn't access the correct server
@ -64,10 +62,7 @@ async fn check_ap_id_authority() {
.clone()
.client(client.clone())
.language_detection_config(language_detection_config())
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.build();
let _ = fetcher
@ -100,10 +95,7 @@ async fn check_ap_content_type() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();

View File

@ -4,7 +4,7 @@ use hyper::{Request, Response};
use kitsune_activitypub::Fetcher;
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::Fetcher as _;
use kitsune_core::traits::{coerce::CoerceResolver, Fetcher as _};
use kitsune_federation_filter::FederationFilter;
use kitsune_http_client::Client;
use kitsune_search::NoopSearchService;
@ -12,8 +12,9 @@ use kitsune_test::{database_test, language_detection_config};
use kitsune_type::webfinger::{Link, Resource};
use kitsune_webfinger::Webfinger;
use pretty_assertions::assert_eq;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn fetch_actor_with_custom_acct() {
@ -57,10 +58,7 @@ async fn fetch_actor_with_custom_acct() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();
@ -138,10 +136,7 @@ async fn ignore_fake_webfinger_acct() {
)
.language_detection_config(language_detection_config())
.search_backend(NoopSearchService)
.resolver(Arc::new(Webfinger::with_client(
client,
Arc::new(NoopCache.into()),
)))
.resolver(Arc::new(Webfinger::with_client(client, Arc::new(NoopCache.into()))).coerce())
.account_cache(Arc::new(NoopCache.into()))
.post_cache(Arc::new(NoopCache.into()))
.build();

View File

@ -17,6 +17,7 @@ redis = { version = "0.25.3", default-features = false, features = [
serde = "1.0.201"
simd-json = "0.13.10"
tracing = "0.1.40"
triomphe = "0.1.11"
typed-builder = "0.18.2"
[dev-dependencies]

View File

@ -4,7 +4,8 @@ extern crate tracing;
use enum_dispatch::enum_dispatch;
use kitsune_error::Result;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Display, sync::Arc};
use std::fmt::Display;
use triomphe::Arc;
pub use self::in_memory::InMemory as InMemoryCache;
pub use self::redis::Redis as RedisCache;

View File

@ -11,8 +11,6 @@ pub enum Transport {
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration {
pub metrics_transport: Transport,
pub metrics_endpoint: SmolStr,
pub tracing_transport: Transport,
pub tracing_endpoint: SmolStr,
}

View File

@ -11,8 +11,11 @@ async-trait = "0.1.80"
const_format = "0.2.32"
kitsune-db = { path = "../kitsune-db" }
kitsune-error = { path = "../kitsune-error" }
paste = "1.0.15"
serde = { version = "1.0.201", features = ["derive"] }
triomphe = { version = "0.1.11", features = ["unsize"] }
typed-builder = "0.18.2"
unsize = "1.1.0"
[build-dependencies]
vergen = { version = "8.3.1", features = ["build", "git", "gitcl"] }

View File

@ -0,0 +1,24 @@
use super::{Deliverer, Fetcher, Resolver};
use paste::paste;
use triomphe::Arc;
use unsize::{CoerceUnsize, Coercion};
macro_rules! create_coerce {
($trait:ident) => {
paste! {
pub trait [<Coerce $trait>] {
fn coerce(self) -> Arc<dyn $trait>;
}
impl<T> [<Coerce $trait>] for Arc<T> where T: $trait {
fn coerce(self) -> Arc<dyn $trait> {
self.unsize(Coercion!(to dyn $trait))
}
}
}
};
}
create_coerce!(Deliverer);
create_coerce!(Fetcher);
create_coerce!(Resolver);

View File

@ -2,7 +2,7 @@ use async_trait::async_trait;
use kitsune_db::model::{account::Account, favourite::Favourite, follower::Follow, post::Post};
use kitsune_error::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use triomphe::Arc;
#[derive(Clone, Deserialize, Serialize)]
pub enum Action {

View File

@ -1,8 +1,8 @@
use super::Resolver;
use super::{coerce::CoerceResolver, Resolver};
use async_trait::async_trait;
use kitsune_db::model::{account::Account, custom_emoji::CustomEmoji, post::Post};
use kitsune_error::Result;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
#[derive(Clone, Copy, Debug, TypedBuilder)]
@ -82,7 +82,7 @@ where
T: Fetcher,
{
fn resolver(&self) -> Arc<dyn Resolver> {
Arc::new(self.iter().map(Fetcher::resolver).collect::<Vec<_>>())
Arc::new(self.iter().map(Fetcher::resolver).collect::<Vec<_>>()).coerce()
}
async fn fetch_account(&self, opts: AccountFetchOptions<'_>) -> Result<Option<Account>> {

View File

@ -1,3 +1,4 @@
pub mod coerce;
pub mod deliverer;
pub mod fetcher;
pub mod resolver;

View File

@ -1,7 +1,7 @@
use async_trait::async_trait;
use kitsune_error::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use triomphe::Arc;
/// Description of a resolved account
#[derive(Clone, Debug, Deserialize, Serialize)]

View File

@ -0,0 +1,14 @@
[package]
name = "kitsune-derive"
authors.workspace = true
edition.workspace = true
version.workspace = true
license.workspace = true
[dependencies]
kitsune-derive-impl = { path = "impl" }
triomphe = "0.1.11"
typed-builder = "0.18.2"
[lints]
workspace = true

View File

@ -0,0 +1 @@
../../LICENSE-AGPL-3.0

View File

@ -0,0 +1,17 @@
[package]
name = "kitsune-derive-impl"
authors.workspace = true
edition.workspace = true
version.workspace = true
license.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.82"
quote = "1.0.36"
syn = { version = "2.0.61", features = ["full"] }
[lints]
workspace = true

View File

@ -0,0 +1,116 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::iter;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Token, Visibility,
};
struct Attributes {
expand_builder: bool,
}
impl Default for Attributes {
fn default() -> Self {
Self {
expand_builder: true,
}
}
}
impl Parse for Attributes {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut attributes = Self::default();
let punctuated = Punctuated::<syn::Ident, Token![,]>::parse_terminated(input)?;
for ident in punctuated {
if ident == "omit_builder" {
attributes.expand_builder = false;
} else {
return Err(syn::Error::new(
ident.span(),
format!("unknown attribute: {ident}"),
));
}
}
Ok(attributes)
}
}
fn expand_builder(
parsed_struct: &syn::ItemStruct,
inner_struct_name: &syn::Ident,
) -> (TokenStream, TokenStream) {
let struct_name = &parsed_struct.ident;
let inner_builder_name = format_ident!("{inner_struct_name}Builder");
let num_lifetimes = parsed_struct.generics.lifetimes().count();
let lifetimes = iter::repeat(quote!('_)).take(num_lifetimes);
let attrs = quote! {
#[derive(::kitsune_derive::typed_builder::TypedBuilder)]
#[builder(build_method(into = #struct_name))]
#[builder(crate_module_path = ::kitsune_derive::typed_builder)]
};
let impls = quote! {
impl #struct_name {
pub fn builder() -> #inner_builder_name<#(#lifetimes),*> {
#inner_struct_name::builder()
}
}
};
(attrs, impls)
}
pub fn expand(attrs: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attributes = syn::parse2::<Attributes>(attrs)?;
let mut parsed_struct = syn::parse2::<syn::ItemStruct>(input)?;
let struct_name = parsed_struct.ident.clone();
let inner_struct_name = format_ident!("__{struct_name}__Inner");
let (builder_attrs, builder_impls) = if attributes.expand_builder {
expand_builder(&parsed_struct, &inner_struct_name)
} else {
(TokenStream::new(), TokenStream::new())
};
parsed_struct.ident = inner_struct_name.clone();
parsed_struct.vis = Visibility::Public(Token![pub](parsed_struct.span()));
let output = quote! {
#builder_attrs
#[allow(non_camel_case_types)]
#[doc(hidden)]
#parsed_struct
#[derive(Clone)]
pub struct #struct_name {
inner: ::kitsune_derive::triomphe::Arc<#inner_struct_name>,
}
#builder_impls
impl ::core::ops::Deref for #struct_name {
type Target = #inner_struct_name;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl From<#inner_struct_name> for #struct_name {
fn from(inner: #inner_struct_name) -> Self {
Self {
inner: ::kitsune_derive::triomphe::Arc::new(inner),
}
}
}
};
Ok(output)
}

View File

@ -0,0 +1,12 @@
mod expand;
#[proc_macro_attribute]
pub fn kitsune_service(
attrs: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
match self::expand::expand(attrs.into(), input.into()) {
Ok(out) => out.into(),
Err(error) => error.to_compile_error().into(),
}
}

View File

@ -0,0 +1,3 @@
pub use ::kitsune_derive_impl::kitsune_service;
pub use ::triomphe;
pub use ::typed_builder;

View File

@ -14,6 +14,7 @@ askama_axum = "0.4.0" # Damn it, cargo. Because "kitsune" uses "askama" with the
diesel = "2.1.6"
diesel-async = "0.4.1"
kitsune-db = { path = "../kitsune-db" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-url = { path = "../kitsune-url" }
lettre = { version = "0.11.7", default-features = false, features = [
@ -31,6 +32,7 @@ mrml = { version = "3.1.5", default-features = false, features = [
"render",
] }
speedy-uuid = { path = "../../lib/speedy-uuid" }
triomphe = "0.1.11"
typed-builder = "0.18.2"
[lints]

View File

@ -4,7 +4,8 @@ use lettre::{
message::{Mailbox, MultiPart},
AsyncTransport, Message,
};
use std::{iter, sync::Arc};
use std::iter;
use triomphe::Arc;
use typed_builder::TypedBuilder;
pub use self::service::Mailing as MailingService;

View File

@ -2,13 +2,13 @@ use crate::{mails::confirm_account::ConfirmAccount, MailSender};
use diesel::{ExpressionMethods, NullableExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use kitsune_db::{function::now, model::user::User, schema::users, with_connection, PgPool};
use kitsune_derive::kitsune_service;
use kitsune_error::Result;
use kitsune_url::UrlService;
use lettre::{AsyncSmtpTransport, Tokio1Executor};
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct Mailing {
db_pool: PgPool,
sender: Option<MailSender<AsyncSmtpTransport<Tokio1Executor>>>,

View File

@ -12,12 +12,12 @@ embed-sdk = { git = "https://github.com/Lantern-chat/embed-service.git", rev = "
http = "1.1.0"
iso8601-timestamp = "0.2.17"
kitsune-db = { path = "../kitsune-db" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-http-client = { path = "../kitsune-http-client" }
once_cell = "1.19.0"
scraper = { version = "0.19.0", default-features = false }
smol_str = "0.2.1"
typed-builder = "0.18.2"
[lints]
workspace = true

View File

@ -9,12 +9,12 @@ use kitsune_db::{
schema::link_previews,
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::Result;
use kitsune_http_client::Client as HttpClient;
use once_cell::sync::Lazy;
use scraper::{Html, Selector};
use smol_str::SmolStr;
use typed_builder::TypedBuilder;
pub use embed_sdk;
pub use embed_sdk::Embed;
@ -32,7 +32,7 @@ fn first_link_from_fragment(fragment: &str) -> Option<String> {
.and_then(|element| element.value().attr("href").map(ToString::to_string))
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct Client {
db_pool: PgPool,
#[builder(setter(into))]

View File

@ -8,6 +8,7 @@ license.workspace = true
[dependencies]
globset = "0.4.14"
kitsune-config = { path = "../kitsune-config" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-type = { path = "../kitsune-type" }
url = "2.5.0"

View File

@ -1,8 +1,8 @@
use globset::{Glob, GlobSet, GlobSetBuilder};
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_derive::kitsune_service;
use kitsune_error::{kitsune_error, Result};
use kitsune_type::ap::{actor::Actor, Activity, Object};
use std::sync::Arc;
use url::Url;
pub trait Entity {
@ -33,9 +33,9 @@ enum FilterMode {
Deny,
}
#[derive(Clone)]
#[kitsune_service(omit_builder)]
pub struct FederationFilter {
domains: Arc<GlobSet>,
domains: GlobSet,
filter: FilterMode,
}
@ -50,9 +50,12 @@ impl FederationFilter {
for glob in globs {
globset.add(Glob::new(glob)?);
}
let domains = Arc::new(globset.build()?);
Ok(Self { domains, filter })
Ok(__FederationFilter__Inner {
domains: globset.build()?,
filter,
}
.into())
}
pub fn is_url_allowed(&self, url: &Url) -> Result<bool> {

View File

@ -18,6 +18,7 @@ kitsune-service = { path = "../kitsune-service" }
kitsune-url = { path = "../kitsune-url" }
kitsune-wasm-mrf = { path = "../kitsune-wasm-mrf" }
kitsune-webfinger = { path = "../kitsune-webfinger" }
triomphe = "0.1.11"
typed-builder = "0.18.2"
[lints]

View File

@ -4,7 +4,11 @@ use kitsune_activitypub::{
};
use kitsune_cache::ArcCache;
use kitsune_config::language_detection::Configuration as LanguageDetectionConfig;
use kitsune_core::traits::{resolver::AccountResource, Deliverer, Fetcher};
use kitsune_core::traits::{
coerce::{CoerceDeliverer, CoerceFetcher, CoerceResolver},
resolver::AccountResource,
Deliverer, Fetcher,
};
use kitsune_db::{
model::{account::Account, post::Post},
PgPool,
@ -15,7 +19,7 @@ use kitsune_service::attachment::AttachmentService;
use kitsune_url::UrlService;
use kitsune_wasm_mrf::MrfService;
use kitsune_webfinger::Webfinger;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
#[derive(TypedBuilder)]
@ -58,6 +62,7 @@ pub(crate) fn prepare_deliverer(prepare: PrepareDeliverer) -> Arc<dyn Deliverer>
.inbox_resolver(inbox_resolver)
.service(service)
.build()
.coerce()
}
#[inline]
@ -71,7 +76,8 @@ pub(crate) fn prepare_fetcher(prepare: PrepareFetcher) -> Arc<dyn Fetcher> {
.federation_filter(prepare.federation_filter.clone())
.language_detection_config(prepare.language_detection_config)
.post_cache(prepare.post_cache)
.resolver(Arc::new(webfinger))
.resolver(Arc::new(webfinger).coerce())
.search_backend(prepare.search_backend)
.build()
.coerce()
}

View File

@ -1,5 +1,8 @@
use kitsune_core::traits::{Deliverer, Fetcher};
use std::sync::Arc;
use kitsune_core::traits::{
coerce::{CoerceDeliverer, CoerceFetcher},
Deliverer, Fetcher,
};
use triomphe::Arc;
use typed_builder::TypedBuilder;
pub mod activitypub;
@ -29,14 +32,14 @@ pub struct Prepare {
#[must_use]
pub fn prepare_deliverer(prepare: PrepareDeliverer) -> Arc<dyn Deliverer> {
let deliverer = self::activitypub::prepare_deliverer(prepare.activitypub);
Arc::new(vec![deliverer])
Arc::new(vec![deliverer]).coerce()
}
#[inline]
#[must_use]
pub fn prepare_fetcher(prepare: PrepareFetcher) -> Arc<dyn Fetcher> {
let fetcher = self::activitypub::prepare_fetcher(prepare.activitypub);
Arc::new(vec![fetcher])
Arc::new(vec![fetcher]).coerce()
}
#[inline]

View File

@ -13,10 +13,6 @@ http-compat = { path = "../../lib/http-compat" }
hyper = { version = "1.3.1", default-features = false }
kitsune-config = { path = "../kitsune-config" }
kitsune-http-client = { path = "../kitsune-http-client" }
metrics = "0.22.3"
metrics-opentelemetry = { git = "https://github.com/aumetra/metrics-opentelemetry.git", rev = "95537b16370e595981e195be52f98ea5983a7a8e" }
metrics-tracing-context = "0.15.0"
metrics-util = "0.16.3"
opentelemetry = { version = "0.22.0", default-features = false, features = [
"trace",
] }
@ -24,7 +20,6 @@ opentelemetry-http = "0.11.1"
opentelemetry-otlp = { version = "0.15.0", default-features = false, features = [
"grpc-tonic",
"http-proto",
"metrics",
"tls",
"tls-roots",
"trace",

View File

@ -3,15 +3,9 @@ use eyre::WrapErr;
use http_body_util::BodyExt;
use http_compat::Compat;
use kitsune_config::{open_telemetry::Transport, Configuration};
use metrics_opentelemetry::OpenTelemetryRecorder;
use metrics_tracing_context::{MetricsLayer, TracingContextLayer};
use metrics_util::layers::Layer as _;
use opentelemetry::{
metrics::{noop::NoopMeterProvider, Meter, MeterProvider},
trace::{noop::NoopTracer, Tracer},
};
use opentelemetry::trace::{noop::NoopTracer, Tracer};
use opentelemetry_http::{Bytes, HttpClient, HttpError, Request, Response};
use opentelemetry_otlp::{MetricsExporterBuilder, SpanExporterBuilder, WithExportConfig};
use opentelemetry_otlp::{SpanExporterBuilder, WithExportConfig};
use opentelemetry_sdk::runtime::Tokio;
use std::{env, fmt};
use tracing_error::ErrorLayer;
@ -81,23 +75,13 @@ where
.with(ErrorLayer::default())
.with(OpenTelemetryLayer::new(tracer));
let subscriber = subscriber.with(MetricsLayer::new());
tracing::subscriber::set_global_default(subscriber)
.wrap_err("Couldn't install the global tracing subscriber")?;
Ok(())
}
fn initialise_metrics(meter: Meter) -> eyre::Result<()> {
let recorder = TracingContextLayer::all().layer(OpenTelemetryRecorder::new(meter));
metrics::set_global_recorder(recorder)
.wrap_err("Couldn't install the global metrics recorder")?;
Ok(())
}
pub fn initialise(app_name: &'static str, config: &Configuration) -> eyre::Result<()> {
pub fn initialise(config: &Configuration) -> eyre::Result<()> {
if let Some(ref opentelemetry_config) = config.opentelemetry {
let http_client = HttpClientAdapter {
inner: kitsune_http_client::Client::default(),
@ -116,23 +100,8 @@ pub fn initialise(app_name: &'static str, config: &Configuration) -> eyre::Resul
.install_batch(Tokio)?;
initialise_logging(tracer)?;
let metrics_exporter = build_exporter!(
MetricsExporterBuilder:
opentelemetry_config.metrics_transport,
&http_client,
opentelemetry_config.tracing_endpoint.as_str(),
);
let meter_provider = opentelemetry_otlp::new_pipeline()
.metrics(Tokio)
.with_exporter(metrics_exporter)
.build()?;
initialise_metrics(meter_provider.meter(app_name))?;
} else {
initialise_logging(NoopTracer::new())?;
initialise_metrics(NoopMeterProvider::new().meter(app_name))?;
}
Ok(())

View File

@ -10,6 +10,7 @@ enum_dispatch = "0.3.13"
http = "1.1.0"
http-body-util = "0.1.1"
kitsune-config = { path = "../kitsune-config" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-http-client = { path = "../kitsune-http-client" }
moka = { version = "0.12.7", features = ["future"] }

View File

@ -3,6 +3,7 @@ use crate::state::{
LoginState, OAuth2LoginState, Store,
};
use kitsune_config::oidc::{Configuration, StoreConfiguration};
use kitsune_derive::kitsune_service;
use kitsune_error::{bail, kitsune_error, Result};
use multiplex_pool::RoundRobinStrategy;
use openidconnect::{
@ -66,7 +67,7 @@ pub struct UserInfo {
pub oauth2: OAuth2Info,
}
#[derive(Clone)]
#[kitsune_service(omit_builder)]
pub struct OidcService {
client: OidcClient,
login_state_store: self::state::AnyStore,
@ -103,10 +104,11 @@ impl OidcService {
}
};
Ok(Self {
Ok(__OidcService__Inner {
client,
login_state_store,
})
}
.into())
}
pub async fn authorisation_url(

View File

@ -17,6 +17,7 @@ futures-util = "0.3.30"
http = "1.1.0"
kitsune-config = { path = "../kitsune-config" }
kitsune-db = { path = "../kitsune-db" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-http-client = { path = "../kitsune-http-client" }
kitsune-language = { path = "../kitsune-language" }
@ -27,7 +28,6 @@ serde_urlencoded = "0.7.1"
speedy-uuid = { path = "../../lib/speedy-uuid" }
strum = { version = "0.26.2", features = ["derive"] }
tracing = "0.1.40"
typed-builder = "0.18.2"
[lints]
workspace = true

View File

@ -1,5 +1,6 @@
use self::http_client::HttpClient;
use super::{Result, SearchBackend, SearchIndex, SearchItem, SearchResultReference};
use kitsune_derive::kitsune_service;
use meilisearch_sdk::{client::Client, indexes::Index, settings::Settings};
use serde::Deserialize;
use speedy_uuid::Uuid;
@ -12,7 +13,7 @@ struct MeilisearchResult {
id: Uuid,
}
#[derive(Clone)]
#[kitsune_service(omit_builder)]
pub struct MeiliSearchService {
client: Client<HttpClient>,
}
@ -29,9 +30,9 @@ impl MeiliSearchService {
.content_length_limit(None)
.build(),
};
let service = Self {
let service = Self::from(__MeiliSearchService__Inner {
client: Client::new_with_client(host, Some(api_key), http_client),
};
});
let settings = Settings::new()
.with_filterable_attributes(["created_at"])

View File

@ -11,10 +11,10 @@ use kitsune_db::{
schema::{accounts, posts},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct SearchService {
db_pool: PgPool,
language_detection_config: LanguageDetectionConfig,

View File

@ -32,6 +32,7 @@ kitsune-captcha = { path = "../kitsune-captcha" }
kitsune-config = { path = "../kitsune-config" }
kitsune-core = { path = "../kitsune-core" }
kitsune-db = { path = "../kitsune-db" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-email = { path = "../kitsune-email" }
kitsune-embed = { path = "../kitsune-embed" }
kitsune-error = { path = "../kitsune-error" }
@ -59,6 +60,7 @@ smol_str = "0.2.1"
speedy-uuid = { path = "../../lib/speedy-uuid" }
tokio = { version = "1.37.0", features = ["macros", "sync"] }
tracing = "0.1.40"
triomphe = "0.1.11"
typed-builder = "0.18.2"
url = "2.5.0"
zxcvbn = { version = "2.2.2", default-features = false }

View File

@ -27,6 +27,7 @@ use kitsune_db::{
schema::{accounts, accounts_follows, accounts_preferences, notifications, posts},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{Error, Result};
use kitsune_jobs::deliver::{
accept::DeliverAccept,
@ -38,7 +39,7 @@ use kitsune_jobs::deliver::{
use kitsune_url::UrlService;
use kitsune_util::{sanitize::CleanHtmlExt, try_join};
use speedy_uuid::Uuid;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
@ -178,7 +179,7 @@ impl<A, H> Update<A, H> {
}
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct AccountService {
attachment_service: AttachmentService,
db_pool: PgPool,

View File

@ -11,6 +11,7 @@ use kitsune_db::{
schema::media_attachments,
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{kitsune_error, Error, ErrorType, Result};
use kitsune_http_client::Client;
use kitsune_storage::{AnyStorageBackend, StorageBackend};
@ -73,7 +74,7 @@ impl<S> Upload<S> {
}
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct AttachmentService {
#[builder(default =
Client::builder()

View File

@ -1,8 +1,8 @@
use kitsune_captcha::{AnyCaptcha, CaptchaBackend, ChallengeStatus};
use kitsune_derive::kitsune_service;
use kitsune_error::Result;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct CaptchaService {
#[builder(setter(into))]
pub backend: Option<AnyCaptcha>,

View File

@ -14,6 +14,7 @@ use kitsune_db::{
schema::{custom_emojis, media_attachments, posts, posts_custom_emojis},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{Error, Result};
use kitsune_url::UrlService;
use speedy_uuid::Uuid;
@ -60,7 +61,7 @@ pub struct EmojiUpload<S> {
stream: S,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct CustomEmojiService {
attachment_service: AttachmentService,
db_pool: PgPool,

View File

@ -4,11 +4,11 @@ use kitsune_db::{
schema::{accounts, posts, users},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{Error, Result};
use smol_str::SmolStr;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct InstanceService {
db_pool: PgPool,
#[builder(setter(into))]

View File

@ -1,8 +1,9 @@
use athena::{JobDetails, JobQueue};
use iso8601_timestamp::Timestamp;
use kitsune_derive::kitsune_service;
use kitsune_error::Result;
use kitsune_jobs::{Job, KitsuneContextRepo};
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
#[derive(TypedBuilder)]
@ -12,7 +13,7 @@ pub struct Enqueue<T> {
run_at: Option<Timestamp>,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct JobService {
job_queue: Arc<dyn JobQueue<ContextRepository = KitsuneContextRepo>>,
}

View File

@ -11,11 +11,12 @@ use kitsune_db::{
schema::{accounts, accounts_follows, accounts_preferences, notifications, posts},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{Error, Result};
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct NotificationService {
db_pool: PgPool,
}

View File

@ -33,6 +33,7 @@ use kitsune_db::{
},
with_connection, with_transaction, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_embed::Client as EmbedClient;
use kitsune_error::{bail, Error, ErrorType, Result};
use kitsune_jobs::deliver::{
@ -294,7 +295,7 @@ pub struct GetAccountsInteractingWithPost {
max_id: Option<Uuid>,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct PostService {
db_pool: PgPool,
embed_client: Option<EmbedClient>,

View File

@ -111,7 +111,7 @@ mod test {
account::AccountService, attachment::AttachmentService, custom_emoji::CustomEmojiService,
job::JobService,
};
use athena::RedisJobQueue;
use athena::{Coerce, RedisJobQueue};
use core::convert::Infallible;
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
@ -121,6 +121,7 @@ mod test {
use kitsune_activitypub::Fetcher;
use kitsune_cache::NoopCache;
use kitsune_config::instance::FederationFilterConfiguration;
use kitsune_core::traits::coerce::{CoerceFetcher, CoerceResolver};
use kitsune_db::{
model::{
account::Account, custom_emoji::CustomEmoji, media_attachment::NewMediaAttachment,
@ -139,8 +140,8 @@ mod test {
use kitsune_webfinger::Webfinger;
use pretty_assertions::assert_eq;
use speedy_uuid::Uuid;
use std::sync::Arc;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
#[allow(clippy::too_many_lines)]
@ -164,7 +165,7 @@ mod test {
});
let client = Client::builder().service(client);
let webfinger = Arc::new(Webfinger::with_client(client.clone(), Arc::new(NoopCache.into())));
let webfinger = Arc::new(Webfinger::with_client(client.clone(), Arc::new(NoopCache.into()))).coerce();
let fetcher = Fetcher::builder()
.client(client)
@ -190,7 +191,7 @@ mod test {
.redis_pool(redis_pool)
.build();
let job_service = JobService::builder().job_queue(Arc::new(job_queue)).build();
let job_service = JobService::builder().job_queue(Arc::new(job_queue).coerce()).build();
let url_service = UrlService::builder()
.domain("example.com")
@ -207,7 +208,7 @@ mod test {
let account_service = AccountService::builder()
.attachment_service(attachment_service.clone())
.db_pool(db_pool.clone())
.fetcher(fetcher)
.fetcher(fetcher.coerce())
.job_service(job_service)
.resolver(webfinger)
.url_service(url_service.clone())

View File

@ -13,8 +13,9 @@ use kitsune_storage::{fs::Storage as FsStorage, s3::Storage as S3Storage, AnySto
use multiplex_pool::RoundRobinStrategy;
use redis::aio::ConnectionManager;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Display, str::FromStr, sync::Arc, time::Duration};
use std::{fmt::Display, str::FromStr, time::Duration};
use tokio::sync::OnceCell;
use triomphe::Arc;
pub async fn cache<K, V>(
config: &cache::Configuration,

View File

@ -9,10 +9,11 @@ use kitsune_db::{
schema::{accounts, posts},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::Result;
use kitsune_search::{SearchBackend, SearchIndex};
use speedy_uuid::Uuid;
use std::sync::Arc;
use triomphe::Arc;
use typed_builder::TypedBuilder;
use url::Url;
@ -37,7 +38,7 @@ pub struct Search<'a> {
max_id: Option<Uuid>,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct SearchService {
db_pool: PgPool,
fetcher: Arc<dyn Fetcher>,

View File

@ -9,6 +9,7 @@ use kitsune_db::{
schema::{accounts_follows, posts, posts_mentions},
with_connection, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{Error, Result};
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
@ -62,7 +63,7 @@ pub struct GetPublic {
only_remote: bool,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct TimelineService {
db_pool: PgPool,
}

View File

@ -1 +0,0 @@

View File

@ -16,6 +16,7 @@ use kitsune_db::{
schema::{accounts, accounts_preferences, users},
with_transaction, PgPool,
};
use kitsune_derive::kitsune_service;
use kitsune_error::{bail, kitsune_error, Error, ErrorType, Result};
use kitsune_jobs::mailing::confirmation::SendConfirmationMail;
use kitsune_url::UrlService;
@ -112,7 +113,7 @@ pub struct Register {
force_registration: bool,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct UserService {
allow_non_ascii_usernames: bool,
captcha_service: CaptchaService,

View File

@ -14,6 +14,7 @@ kitsune-s3 = { path = "../kitsune-s3" }
rusty-s3 = { version = "0.5.0", default-features = false }
tokio = { version = "1.37.0", features = ["fs", "io-util"] }
tokio-util = { version = "0.7.11", features = ["io"] }
triomphe = "0.1.11"
[dev-dependencies]
tempfile = "3.10.1"

View File

@ -7,7 +7,7 @@ use bytes::Bytes;
use futures_util::{Stream, StreamExt};
use kitsune_error::Result;
use rusty_s3::{Bucket, Credentials};
use std::sync::Arc;
use triomphe::Arc;
#[derive(Clone)]
/// S3-backed storage

View File

@ -23,13 +23,8 @@ redis = { version = "0.25.3", default-features = false, features = [
"tokio-rustls-comp",
] }
rusty-s3 = { version = "0.5.0", default-features = false }
testcontainers = "0.16.7"
testcontainers-modules = { version = "0.4.2", features = [
"minio",
"postgres",
"redis",
] }
tokio = { version = "1.37.0", features = ["time"] }
triomphe = "0.1.11"
url = "2.5.0"
uuid = { version = "1.8.0", features = ["fast-rng", "v4"] }

View File

@ -1,56 +0,0 @@
use testcontainers::{core::ContainerAsync, runners::AsyncRunner, RunnableImage};
use testcontainers_modules::{minio::MinIO, postgres::Postgres, redis::Redis};
pub trait Service {
const PORT: u16;
async fn url(&self) -> String;
}
impl Service for ContainerAsync<MinIO> {
const PORT: u16 = 9000;
async fn url(&self) -> String {
let port = self.get_host_port_ipv4(Self::PORT).await;
format!("http://127.0.0.1:{port}")
}
}
impl Service for ContainerAsync<Postgres> {
const PORT: u16 = 5432;
async fn url(&self) -> String {
let port = self.get_host_port_ipv4(Self::PORT).await;
format!("postgres://postgres:postgres@127.0.0.1:{port}/test_db")
}
}
impl Service for ContainerAsync<Redis> {
const PORT: u16 = 6379;
async fn url(&self) -> String {
let port = self.get_host_port_ipv4(Self::PORT).await;
format!("redis://127.0.0.1:{port}")
}
}
pub async fn minio() -> impl Service {
MinIO::default().start().await
}
pub async fn postgres() -> impl Service {
let base = Postgres::default()
.with_user("postgres")
.with_password("postgres")
.with_db_name("test_db");
RunnableImage::from(base)
.with_tag("15-alpine")
.start()
.await
}
pub async fn redis() -> impl Service {
#[allow(clippy::default_constructed_unit_structs)]
Redis::default().start().await
}

View File

@ -12,12 +12,12 @@ use kitsune_config::{
use kitsune_db::PgPool;
use multiplex_pool::RoundRobinStrategy;
use resource::provide_resource;
use std::sync::Arc;
use std::env;
use triomphe::Arc;
use url::Url;
use uuid::Uuid;
mod catch_panic;
mod container;
mod macros;
mod redis;
mod resource;
@ -37,8 +37,8 @@ where
F: FnOnce(PgPool) -> Fut,
Fut: Future,
{
let resource_handle = get_resource!("DATABASE_URL", self::container::postgres);
let mut url = Url::parse(&resource_handle.url().await).unwrap();
let db_url = env::var("DATABASE_URL").unwrap();
let mut url = Url::parse(&db_url).unwrap();
// Create a new separate database for this test
let id = Uuid::new_v4().as_simple().to_string();
@ -84,8 +84,8 @@ where
F: FnOnce(Arc<kitsune_s3::Client>) -> Fut,
Fut: Future,
{
let resource_handle = get_resource!("MINIO_URL", self::container::minio);
let endpoint = resource_handle.url().await.parse().unwrap();
let endpoint = env::var("MINIO_URL").unwrap();
let endpoint = endpoint.parse().unwrap();
// Create a new bucket with a random ID
let bucket_id = Uuid::new_v4().as_simple().to_string();
@ -116,8 +116,8 @@ where
F: FnOnce(multiplex_pool::Pool<ConnectionManager>) -> Fut,
Fut: Future,
{
let resource_handle = get_resource!("REDIS_URL", self::container::redis);
let client = ::redis::Client::open(resource_handle.url().await.as_ref()).unwrap();
let redis_url = env::var("REDIS_URL").unwrap();
let client = ::redis::Client::open(redis_url.as_ref()).unwrap();
// Connect to a random Redis database
let db_id = self::redis::find_unused_database(&client).await;

View File

@ -1,37 +1,5 @@
use crate::{catch_panic::CatchPanic, container::Service};
use std::{borrow::Cow, future::Future, panic};
#[macro_export]
macro_rules! get_resource {
($env_name:literal, $container_fn:path) => {
if let Ok(url) = ::std::env::var($env_name) {
$crate::resource::ResourceHandle::Url(url)
} else {
let container = $container_fn().await;
$crate::resource::ResourceHandle::Container(container)
}
};
}
pub enum ResourceHandle<S>
where
S: Service,
{
Container(S),
Url(String),
}
impl<S> ResourceHandle<S>
where
S: Service,
{
pub async fn url(&self) -> Cow<'_, str> {
match self {
Self::Container(container) => Cow::Owned(container.url().await),
Self::Url(ref url) => Cow::Borrowed(url),
}
}
}
use crate::catch_panic::CatchPanic;
use std::{future::Future, panic};
/// Provide a resource to the `run` closure, catch any panics that may occur while polling the future returned by `run`,
/// then run the `cleanup` closure, and resume any panic unwinds that were caught

View File

@ -6,9 +6,9 @@ version.workspace = true
license.workspace = true
[dependencies]
kitsune-derive = { path = "../kitsune-derive" }
smol_str = "0.2.1"
speedy-uuid = { path = "../../lib/speedy-uuid" }
typed-builder = "0.18.2"
[lints]
workspace = true

View File

@ -1,12 +1,12 @@
use kitsune_derive::kitsune_service;
use smol_str::SmolStr;
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
/// Small "service" to centralise the creation of URLs
///
/// For some light deduplication purposes and to centralise the whole formatting story.
/// Allows for easier adjustments of URLs.
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct UrlService {
#[builder(setter(into))]
scheme: SmolStr,

View File

@ -16,6 +16,7 @@ futures-util = { version = "0.3.30", default-features = false, features = [
"alloc",
] }
kitsune-config = { path = "../kitsune-config" }
kitsune-derive = { path = "../kitsune-derive" }
kitsune-error = { path = "../kitsune-error" }
kitsune-type = { path = "../kitsune-type" }
mrf-manifest = { path = "../../lib/mrf-manifest", features = ["decode"] }
@ -30,7 +31,7 @@ sled = "0.34.7"
smol_str = "0.2.1"
tokio = { version = "1.37.0", features = ["fs"] }
tracing = "0.1.40"
typed-builder = "0.18.2"
triomphe = "0.1.11"
walkdir = "2.5.0"
wasmtime = { version = "20.0.2", default-features = false, features = [
"addr2line",

View File

@ -1,6 +1,6 @@
use crate::kv_storage;
use slab::Slab;
use std::sync::Arc;
use triomphe::Arc;
use wasmtime::{component::ResourceTable, Engine, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};

View File

@ -11,6 +11,7 @@ use futures_util::{stream::FuturesUnordered, Stream, TryFutureExt, TryStreamExt}
use kitsune_config::mrf::{
Configuration as MrfConfiguration, FsKvStorage, KvStorage, RedisKvStorage,
};
use kitsune_derive::kitsune_service;
use kitsune_error::Error;
use kitsune_type::ap::Activity;
use mrf_manifest::{Manifest, ManifestV1};
@ -20,10 +21,9 @@ use std::{
fmt::Debug,
io,
path::{Path, PathBuf},
sync::Arc,
};
use tokio::fs;
use typed_builder::TypedBuilder;
use triomphe::Arc;
use walkdir::WalkDir;
use wasmtime::{
component::{Component, Linker},
@ -111,7 +111,7 @@ pub struct MrfModule {
pub manifest: ManifestV1<'static>,
}
#[derive(Clone, TypedBuilder)]
#[kitsune_service]
pub struct MrfService {
engine: Engine,
linker: Arc<Linker<Context>>,
@ -131,12 +131,13 @@ impl MrfService {
mrf_wit::v1::Mrf::add_to_linker(&mut linker, |ctx| ctx).map_err(eyre::Report::msg)?;
wasmtime_wasi::add_to_linker_async(&mut linker).map_err(eyre::Report::msg)?;
Ok(Self {
Ok(__MrfService__Inner {
engine,
linker: Arc::new(linker),
modules: modules.into(),
storage: Arc::new(storage),
})
}
.into())
}
#[instrument(skip_all, fields(module_dir = %config.module_dir))]

View File

@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
async-trait = "0.1.80"
autometrics = { version = "1.0.1", default-features = false }
futures-util = "0.3.30"
http = "1.1.0"
kitsune-cache = { path = "../kitsune-cache" }
@ -22,6 +21,7 @@ redis = { version = "0.25.3", default-features = false, features = [
"tokio-comp",
] }
tracing = "0.1.40"
triomphe = "0.1.11"
urlencoding = "2.1.3"
[dev-dependencies]

View File

@ -2,7 +2,6 @@
extern crate tracing;
use async_trait::async_trait;
use autometrics::autometrics;
use futures_util::future::{FutureExt, OptionFuture};
use http::{HeaderValue, StatusCode};
use kitsune_cache::{ArcCache, CacheBackend, RedisCache};
@ -15,7 +14,8 @@ use kitsune_http_client::Client;
use kitsune_type::webfinger::Resource;
use kitsune_util::try_join;
use redis::aio::ConnectionManager;
use std::{ptr, sync::Arc, time::Duration};
use std::{ptr, time::Duration};
use triomphe::Arc;
const CACHE_DURATION: Duration = Duration::from_secs(10 * 60); // 10 minutes
@ -68,7 +68,6 @@ impl Resolver for Webfinger {
/// `acct:{preferredUsername}@{domain}` URI points back to the resolved `acct:` resource,
/// which the caller should check by themselves before trusting the result.
#[instrument(skip(self))]
#[autometrics(track_concurrency)]
async fn resolve_account(
&self,
username: &str,

View File

@ -5,8 +5,9 @@ use kitsune_core::traits::Resolver;
use kitsune_http_client::Client;
use kitsune_webfinger::Webfinger;
use pretty_assertions::assert_eq;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn basic() {

View File

@ -6,8 +6,9 @@ use kitsune_http_client::Client;
use kitsune_type::webfinger::Resource;
use kitsune_webfinger::{Webfinger, MAX_JRD_REDIRECTS};
use pretty_assertions::assert_eq;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;
use tower::service_fn;
use triomphe::Arc;
#[tokio::test]
async fn follow_jrd_redirect() {

View File

@ -23,8 +23,6 @@ postgres://database-user:password-here@localhost:5432/db-name-here
The `max-connections` setting defines how many connections the globally shared connection pool will open to the database server _at maximum_.
What you should set this value to depends on many factors.
> We currently do not report any pool metrics via the Prometheus endpoint. This might be added in the future.
## TLS support
If you want to connect to a database using TLS, set the parameter `use-tls` to `true`.

View File

@ -1,14 +1,11 @@
# OpenTelemetry
Kitsune can export its traces and metrics via the OpenTelemetry Protocol (or OTLP, for short).
Kitsune can export its traces via the OpenTelemetry Protocol (or OTLP, for short).
To push the data to an endpoint, add the following to your configuration:
```toml
[opentelemetry]
# Where Kitsune pushes metrics (eg. Prometheus)
metrics-transport = "http" # "http" or "grpc"
metrics-endpoint = "https://localhost:5050/metrics-endpoint"
# Where Kitsune pushes traces (eg. Jaeger)
tracing-transport = "http" # "http" or "grpc"
tracing-endpoint = "https://localhost:5050/tracing-endpoint"

352
flake.nix
View File

@ -26,169 +26,209 @@
# like so `nix build --override-input debugBuild github:boolean-option/true`
debugBuild.url = "github:boolean-option/false/d06b4794a134686c70a1325df88a6e6768c6b212";
};
outputs = { self, devenv, flake-utils, nixpkgs, rust-overlay, crane, ... } @ inputs:
(flake-utils.lib.eachDefaultSystem
(system:
let
features = "--all-features";
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit overlays system;
};
stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv;
rustPlatform = pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
inherit stdenv;
};
craneLib = (crane.mkLib pkgs).overrideToolchain pkgs.rust-bin.stable.latest.minimal;
buildInputs = with pkgs; [
openssl
sqlite
zlib
];
nativeBuildInputs = with pkgs; [
protobuf
pkg-config
rustPlatform.bindgenHook
];
src = pkgs.lib.cleanSourceWith {
src = pkgs.lib.cleanSource ./.;
filter = name: type:
let baseName = baseNameOf (toString name);
in !(baseName == "flake.lock" || pkgs.lib.hasSuffix ".nix" baseName);
};
commonArgs = {
inherit src stdenv buildInputs nativeBuildInputs;
strictDeps = true;
meta = {
description = "ActivityPub-federated microblogging";
homepage = "https://joinkitsune.org";
outputs =
{ self
, devenv
, flake-utils
, nixpkgs
, rust-overlay
, crane
, ...
}@inputs:
(
flake-utils.lib.eachDefaultSystem
(
system:
let
features = "--all-features";
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit overlays system; };
stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv;
rustPlatform = pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
inherit stdenv;
};
OPENSSL_NO_VENDOR = 1;
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
cargoExtraArgs = "--locked ${features}";
} // (pkgs.lib.optionalAttrs inputs.debugBuild.value {
# do a debug build, as `dev` is the default debug profile
CARGO_PROFILE = "dev";
});
craneLib = (crane.mkLib pkgs).overrideToolchain pkgs.rust-bin.stable.latest.minimal;
buildInputs = with pkgs; [
openssl
sqlite
zlib
];
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
version = cargoToml.workspace.package.version;
nativeBuildInputs = with pkgs; [
protobuf
pkg-config
rustPlatform.bindgenHook
];
cargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
pname = "kitsune-workspace";
src = craneLib.cleanCargoSource src;
});
in
{
formatter = pkgs.nixpkgs-fmt;
packages = rec {
default = main;
cli = craneLib.buildPackage (commonArgs // {
pname = "kitsune-cli";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin kitsune-cli";
inherit cargoArtifacts;
doCheck = false;
});
mrf-tool = craneLib.buildPackage (commonArgs // {
pname = "mrf-tool";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin mrf-tool";
inherit cargoArtifacts;
doCheck = false;
});
main = craneLib.buildPackage (commonArgs // rec {
pname = "kitsune";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin kitsune --bin kitsune-job-runner";
inherit cargoArtifacts;
doCheck = false;
});
frontend = pkgs.mkYarnPackage {
inherit version;
packageJSON = "${src}/kitsune-fe/package.json";
yarnLock = "${src}/kitsune-fe/yarn.lock";
src = "${src}/kitsune-fe";
buildPhase = ''
export HOME=$(mktemp -d)
yarn --offline build
'';
installPhase = ''
mkdir -p $out
cp -R deps/kitsune-fe/dist $out
'';
distPhase = "true";
};
};
devShells = rec {
default = backend;
backend = devenv.lib.mkShell {
inherit pkgs inputs;
modules = [
({ pkgs, ... }: {
packages = with pkgs; [
cargo-insta
diesel-cli
rust-bin.stable.latest.default
]
++
buildInputs ++ nativeBuildInputs;
enterShell = ''
export PG_HOST=127.0.0.1
export PG_PORT=5432
[ -z "$DATABASE_URL" ] && export DATABASE_URL=postgres://$USER@$PG_HOST:$PG_PORT/$USER
export REDIS_PORT=6379
[ -z "$REDIS_URL" ] && export REDIS_URL="redis://127.0.0.1:$REDIS_PORT"
'';
services = {
postgres = {
enable = true;
listen_addresses = "127.0.0.1";
};
redis.enable = true;
};
})
];
src = pkgs.lib.cleanSourceWith {
src = pkgs.lib.cleanSource ./.;
filter =
name: type:
let
baseName = baseNameOf (toString name);
in
!(baseName == "flake.lock" || pkgs.lib.hasSuffix ".nix" baseName);
};
frontend = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs
yarn
];
};
};
}
) // {
overlays = rec {
default = kitsune;
kitsune = (import ./overlay.nix self);
};
commonArgs =
{
inherit
src
stdenv
buildInputs
nativeBuildInputs
;
nixosModules = rec {
default = kitsune;
kitsune = (import ./module.nix);
};
}) // {
strictDeps = true;
meta = {
description = "ActivityPub-federated microblogging";
homepage = "https://joinkitsune.org";
};
OPENSSL_NO_VENDOR = 1;
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
cargoExtraArgs = "--locked ${features}";
}
// (pkgs.lib.optionalAttrs inputs.debugBuild.value {
# do a debug build, as `dev` is the default debug profile
CARGO_PROFILE = "dev";
});
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
version = cargoToml.workspace.package.version;
cargoArtifacts = craneLib.buildDepsOnly (
commonArgs
// {
pname = "kitsune-workspace";
src = craneLib.cleanCargoSource src;
}
);
in
{
formatter = pkgs.nixpkgs-fmt;
packages = rec {
default = main;
cli = craneLib.buildPackage (
commonArgs
// {
pname = "kitsune-cli";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin kitsune-cli";
inherit cargoArtifacts;
doCheck = false;
}
);
mrf-tool = craneLib.buildPackage (
commonArgs
// {
pname = "mrf-tool";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin mrf-tool";
inherit cargoArtifacts;
doCheck = false;
}
);
main = craneLib.buildPackage (
commonArgs
// {
pname = "kitsune";
cargoExtraArgs = commonArgs.cargoExtraArgs + " --bin kitsune --bin kitsune-job-runner";
inherit cargoArtifacts;
doCheck = false;
}
);
frontend = pkgs.mkYarnPackage {
inherit version;
packageJSON = "${src}/kitsune-fe/package.json";
yarnLock = "${src}/kitsune-fe/yarn.lock";
src = "${src}/kitsune-fe";
buildPhase = ''
export HOME=$(mktemp -d)
yarn --offline build
'';
installPhase = ''
mkdir -p $out
cp -R deps/kitsune-fe/dist $out
'';
distPhase = "true";
};
};
devShells = rec {
default = backend;
backend = devenv.lib.mkShell {
inherit pkgs inputs;
modules = [
(
{ pkgs, ... }:
{
packages =
with pkgs;
[
cargo-insta
diesel-cli
rust-bin.stable.latest.default
]
++ buildInputs
++ nativeBuildInputs;
enterShell = ''
export PG_HOST=127.0.0.1
export PG_PORT=5432
[ -z "$DATABASE_URL" ] && export DATABASE_URL=postgres://$USER@$PG_HOST:$PG_PORT/$USER
export REDIS_PORT=6379
[ -z "$REDIS_URL" ] && export REDIS_URL="redis://127.0.0.1:$REDIS_PORT"
'';
services = {
postgres = {
enable = true;
listen_addresses = "127.0.0.1";
};
redis.enable = true;
};
}
)
];
};
frontend = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs
yarn
nodePackages.svelte-language-server
nodePackages.typescript-language-server
];
};
};
}
)
// {
overlays = rec {
default = kitsune;
kitsune = (import ./overlay.nix self);
};
nixosModules = rec {
default = kitsune;
kitsune = (import ./module.nix);
};
}
)
// {
nixci.default = {
debug = {
dir = ".";

24
kitsune-fe-old/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,13 @@
{
"endOfLine": "lf",
"importOrder": ["^\\w", "^[./|~/]"],
"importOrderSeparation": true,
"importOrderParserPlugins": ["typescript"],
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-css-order"
],
"semi": true,
"singleQuote": true,
"vueIndentScriptAndStyle": true
}

23
kitsune-fe-old/README.md Normal file
View File

@ -0,0 +1,23 @@
# Kitsune FE
Frontend for Kitsune using its GraphQL API
## Build
```
yarn build
```
## Development server
```
yarn dev
```
Set the backend the frontend uses inside `.env.development`
## Autogenerate GraphQL typings in the background
```
yarn graphql-codegen --watch
```

View File

@ -0,0 +1,73 @@
{
"name": "kitsune-fe",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint src",
"format": "prettier . --write"
},
"dependencies": {
"@fluent/bundle": "^0.18.0",
"@formkit/core": "^1.6.2",
"@formkit/validation": "^1.6.2",
"@formkit/vue": "^1.6.2",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-3",
"@tiptap/pm": "^2.3.0",
"@tiptap/starter-kit": "^2.3.0",
"@tiptap/vue-3": "^2.3.0",
"@urql/exchange-graphcache": "^7.0.1",
"@urql/vue": "^1.1.3",
"@vueuse/core": "^10.9.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
"floating-vue": "^5.2.2",
"fluent-vue": "^3.5.2",
"graphql": "^16.8.1",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"rollup": "npm:@rollup/wasm-node",
"tiptap-markdown": "^0.8.10",
"unhead": "^1.9.7",
"vue": "^3.4.23",
"vue-powerglitch": "^1.0.0",
"vue-router": "^4.3.2",
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/client-preset": "^4.2.5",
"@parcel/watcher": "^2.4.1",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash": "^4.17.0",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"eslint": "^9.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5",
"prettier-plugin-css-order": "^2.1.2",
"sass": "^1.75.0",
"typescript": "^5.4.5",
"unplugin-fluent-vue": "^1.3.0",
"vite": "^5.2.10",
"vue-tsc": "^2.0.13"
},
"resolutions": {
"rollup": "npm:@rollup/wasm-node"
}
}

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Some files were not shown because too many files have changed in this diff Show More