Compare commits

..

No commits in common. "a29cea6cbc631b9f8af7e6f2811df79fe64c59de" and "8ab6dd9803d9b74c306d2fce629e59779391fac6" have entirely different histories.

23 changed files with 559 additions and 2604 deletions

1816
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,6 @@ members = [
"ext_calckey_model",
"fe_calckey",
"magnetar_common",
"magnetar_sdk",
"core"
]
@ -22,7 +21,6 @@ version = "0.2.0"
edition = "2021"
[workspace.dependencies]
async-trait = "0.1"
axum = "0.6"
cached = "0.44"
cfg-if = "1"
@ -31,19 +29,15 @@ dotenvy = "0.15"
futures-core = "0.3"
futures-util = "0.3"
headers = "0.3"
http = "0.2"
hyper = "0.14"
js-sys = "0.3"
log = "0.4"
miette = "5.9"
percent-encoding = "2.2"
redis = "0.23"
reqwest = "0.11"
sea-orm = "0.12"
sea-orm-migration = "0.12"
sea-orm = "0.11"
sea-orm-migration = "0.11"
serde = "1"
serde_json = "1"
serde-wasm-bindgen = "0.5"
strum = "0.25"
tera = { version = "1", default-features = false }
thiserror = "1"
@ -54,12 +48,8 @@ tower = "0.4"
tower-http = "0.4"
tracing = "0.1"
tracing-subscriber = "0.3"
ts-rs = "6"
url = "2.3"
walkdir = "2.3"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"
[dependencies]
magnetar_core = { path = "./core" }
@ -67,7 +57,6 @@ magnetar_common = { path = "./magnetar_common" }
magnetar_webfinger = { path = "./ext_webfinger" }
magnetar_nodeinfo = { path = "./ext_nodeinfo" }
magnetar_calckey_model = { path = "./ext_calckey_model" }
magnetar_sdk = { path = "./magnetar_sdk" }
cached = { workspace = true }
chrono = { workspace = true }
@ -94,6 +83,3 @@ percent-encoding = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
toml = { workspace = true }
[profile.release]
lto = true

View File

@ -48,6 +48,8 @@ pub struct Model {
pub room: Json,
#[sea_orm(column_type = "JsonBinary")]
pub integrations: Json,
#[sea_orm(column_name = "injectFeaturedNote")]
pub inject_featured_note: bool,
#[sea_orm(column_name = "enableWordMute")]
pub enable_word_mute: bool,
#[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")]

View File

@ -3,7 +3,6 @@ pub use sea_orm_migration::prelude::*;
mod m20220101_000001_bootstrap;
mod m20230729_201733_drop_messaging_integrations;
mod m20230729_212237_user_unique_idx;
mod m20230806_142918_drop_featured_note_option;
pub struct Migrator;
@ -14,7 +13,6 @@ impl MigratorTrait for Migrator {
Box::new(m20220101_000001_bootstrap::Migration),
Box::new(m20230729_201733_drop_messaging_integrations::Migration),
Box::new(m20230729_212237_user_unique_idx::Migration),
Box::new(m20230806_142918_drop_featured_note_option::Migration),
]
}
}

View File

@ -1,33 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote";
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
ALTER TABLE "user_profile" ADD COLUMN "injectFeaturedNote" BOOLEAN NOT NULL DEFAULT TRUE;
"#,
)
.await?;
Ok(())
}
}

View File

@ -695,6 +695,7 @@ export type Endpoints = {
preventAiLearning?: boolean;
isBot?: boolean;
isCat?: boolean;
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
mutedWords?: string[][];

View File

@ -96,6 +96,7 @@ export type MeDetailed = UserDetailed & {
hasUnreadNotification: boolean;
hasUnreadSpecifiedNotes: boolean;
hideOnlineStatus: boolean;
injectFeaturedNote: boolean;
isDeleted: boolean;
isExplorable: boolean;
mutedWords: string[][];

View File

@ -1,5 +1,13 @@
<template>
<div class="_formRoot">
<FormSwitch
v-model="$i.injectFeaturedNote"
class="_formBlock"
@update:modelValue="onChangeInjectFeaturedNote"
>
{{ i18n.ts.showFeaturedNotesInTimeline }}
</FormSwitch>
<!--
<FormSwitch v-model="reportError" class="_formBlock">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch>
-->
@ -21,10 +29,27 @@
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent } from "vue";
import FormSwitch from "@/components/form/switch.vue";
import FormLink from "@/components/form/link.vue";
import MkButton from "@/components/MkButton.vue";
import * as os from "@/os";
import { popup } from "@/os";
import { defaultStore } from "@/store";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
const reportError = computed(defaultStore.makeGetterSetter("reportError"));
function onChangeInjectFeaturedNote(v) {
os.api("i/update", {
injectFeaturedNote: v,
}).then((i) => {
$i!.injectFeaturedNote = i.injectFeaturedNote;
});
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);

View File

@ -1,46 +0,0 @@
[package]
name = "magnetar_sdk"
version.workspace = true
edition.workspace = true
[lib]
crate-type = ["rlib", "cdylib"]
[features]
reqwest = ["dep:reqwest"]
[target.'cfg(not(target_arch = "wasm32"))'.features]
default = ["reqwest"]
[target.'cfg(target_arch = "wasm32")'.features]
default = []
[dependencies]
magnetar_sdk_macros = { path = "./macros" }
chrono = { workspace = true, features = ["serde"] }
reqwest = { workspace = true, features = ["json"], optional = true }
http = { workspace = true }
async-trait = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
serde-wasm-bindgen = { workspace = true }
chrono = { workspace = true, features = ["serde", "wasm-bindgen", "js-sys"] }
js-sys = { workspace = true }
web-sys = { workspace = true, features = [
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
"UrlSearchParams"
] }

View File

@ -1,11 +0,0 @@
[package]
name = "magnetar_sdk_macros"
version.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full", "extra-traits"] }
quote = "1"

View File

@ -1,237 +0,0 @@
use proc_macro::TokenStream;
use std::collections::HashSet;
use syn::parse::Parse;
use syn::punctuated::Punctuated;
use syn::{Expr, ExprLit, ExprPath, Ident, Lit, Meta, MetaNameValue, Token};
struct Field {
name: Ident,
ty: Ident,
}
struct PackInput {
struct_name: syn::Ident,
fields: Vec<Field>,
}
impl Parse for PackInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let _ = input.parse::<Token![,]>()?;
let mut fields = Vec::new();
loop {
let ty = input.parse()?;
let _alias_keyword = input.parse::<Token![as]>()?;
let name = input.parse()?;
fields.push(Field { name, ty });
if input.is_empty() {
break;
}
let _ = input.parse::<Token![&]>()?;
}
Ok(PackInput {
struct_name,
fields,
})
}
}
#[proc_macro]
pub fn pack(item: TokenStream) -> TokenStream {
let parsed = syn::parse_macro_input!(item as PackInput);
let struct_name = &parsed.struct_name;
let fields = &parsed.fields;
let names = fields.iter().map(|f| &f.name);
let types = fields.iter().map(|f| &f.ty);
let export_path = format!("bindings/packed/{}.ts", struct_name);
let expanded = quote::quote! {
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export, export_to = #export_path)]
pub struct #struct_name {
#(
#[serde(flatten)]
pub #names: #types
),*
}
};
TokenStream::from(expanded)
}
#[proc_macro_derive(Endpoint, attributes(name, endpoint, method, request, response))]
pub fn derive_endpoint(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
let struct_name = input.ident;
assert!(input.generics.params.is_empty(), "generics not supported");
let where_clause = input.generics.where_clause;
assert!(where_clause.is_none(), "where clauses not supported");
let data = match input.data {
syn::Data::Struct(data) => data,
_ => panic!("only unit structs are supported"),
};
assert!(
matches!(data.fields, syn::Fields::Unit),
"only unit structs are supported"
);
let mut name = struct_name.to_string();
let mut endpoint = None;
let mut method = None;
let mut request = None;
let mut response = None;
let mut found = HashSet::new();
let struct_attrs = input.attrs;
for struct_attr in struct_attrs {
if !struct_attr.meta.path().is_ident("endpoint") {
continue;
}
if struct_attr.style != syn::AttrStyle::Outer {
panic!("expected outer attribute");
}
let Meta::List(list) = struct_attr.meta else {
panic!("expected a list of attributes");
};
let nested = list
.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)
.expect("expected a list of attributes");
for kvp in nested {
let key = kvp
.path
.get_ident()
.expect("expected a single identifier")
.to_owned();
let value = kvp.value;
if found.contains(&key) {
panic!("duplicate attribute: {}", key);
}
match key.to_string().as_ref() {
"name" => {
let Expr::Lit(ExprLit { lit: Lit::Str(new_name), .. }) = value else {
panic!("expected a string literal");
};
name = new_name.value();
}
"endpoint" => {
let Expr::Lit(ExprLit { lit: Lit::Str(endpoint_val), .. }) = value else {
panic!("expected a string literal");
};
endpoint = Some(endpoint_val.value());
}
"method" => {
let Expr::Path(ExprPath { path, .. }) = value else {
panic!("expected a an identifier");
};
method = Some(path);
}
"request" => {
let Expr::Path(ExprPath { path, .. }) = value else {
panic!("expected a an identifier");
};
request = Some(path);
}
"response" => {
let Expr::Path(ExprPath { path, .. }) = value else {
panic!("expected an identifier")
};
response = Some(path);
}
_ => panic!("unknown attribute: {}", key),
}
found.insert(key);
}
}
let endpoint = endpoint.expect("missing endpoint attribute");
let method = method.expect("missing method attribute");
let request = request.expect("missing request attribute");
let response = response.expect("missing response attribute");
let ts_path = format!("bindings/endpoints/{}.ts", name);
let expanded = quote::quote! {
impl Default for #struct_name {
fn default() -> Self {
Self
}
}
impl Endpoint for #struct_name {
const NAME: &'static str = #name;
const ENDPOINT: &'static str = #endpoint;
const METHOD: Method = #method;
type Request = #request;
type Response = #response;
}
#[cfg(test)]
#[test]
fn export_bindings_getuserbyid() {
#struct_name::export().expect("could not export type");
}
impl TS for #struct_name {
const EXPORT_TO: Option<&'static str> = Some(#ts_path);
fn decl() -> String {
format!(
"interface {} {{\n \
endpoint: \"{}\";\n \
method: \"{}\";\n \
request: {};\n \
response: {};\n\
}}
",
Self::name(),
Self::ENDPOINT,
Self::METHOD,
<#struct_name as Endpoint>::Request::name(),
<#struct_name as Endpoint>::Response::name()
)
}
fn name() -> String {
#struct_name::NAME.to_string()
}
fn dependencies() -> Vec<Dependency> {
vec![
Dependency::from_ty::<<#struct_name as Endpoint>::Request>().unwrap(),
Dependency::from_ty::<<#struct_name as Endpoint>::Response>().unwrap(),
]
}
fn transparent() -> bool {
false
}
}
};
TokenStream::from(expanded)
}

View File

@ -1,277 +0,0 @@
use crate::client::ApiClientImpl;
use crate::endpoints::list::match_endpoint;
use crate::endpoints::{Endpoint, ErrorKind, ResponseError};
use crate::Client;
use http::Method;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
pub struct FetchClient {
base_url: String,
application: String,
}
impl FetchClient {
pub fn new(base_url: &str, application: &str) -> Self {
FetchClient {
base_url: base_url.to_string(),
application: application.to_string(),
}
}
}
#[async_trait::async_trait(?Send)]
impl Client for FetchClient {
fn base_url(&self) -> &str {
&self.base_url
}
async fn call<'a, I, O, E>(
&self,
endpoint: &E,
data: &I,
path_params: &impl AsRef<[(&'a str, &'a str)]>,
) -> Result<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static,
{
let url = endpoint.template_path(self.base_url(), path_params);
let url = if E::METHOD == Method::GET {
let search = web_sys::UrlSearchParams::new_with_str_sequence_sequence(
&serde_wasm_bindgen::to_value(data).unwrap(),
)
.unwrap();
format!("{}?{}", url, search.to_string())
} else {
url
};
let mut opts = web_sys::RequestInit::new();
opts.method(E::METHOD.as_str());
if E::METHOD != Method::GET {
opts.body(Some(&serde_wasm_bindgen::to_value(data).unwrap()));
}
let headers = web_sys::Headers::new().unwrap();
headers.set("Content-Type", "application/json").unwrap();
headers.set("User-Agent", &self.application).unwrap();
opts.headers(&headers);
let req = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap();
let js_response = wasm_bindgen_futures::JsFuture::from(
web_sys::window()
.ok_or_else(|| ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:NoWindow".to_string(),
message: "No window object".to_string(),
status: None,
})?
.fetch_with_request(&req),
)
.await
.map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:FetchFail".to_string(),
message: format!("{:#?}", e),
status: None,
})?;
let response = web_sys::Response::from(js_response);
let status = response.status();
if (400..=599).contains(&status) {
let body = wasm_bindgen_futures::JsFuture::from(response.json().map_err(|e| {
ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:ResponseErrorPromiseFail".to_string(),
message: format!("{:#?}", e),
status: Some(status),
}
})?)
.await
.map_err(|e| ResponseError {
kind: ErrorKind::ApiError,
code: "FetchClient:ResponseErrorFail".to_string(),
message: format!("{:#?}", e),
status: Some(status),
})?;
let data = serde_wasm_bindgen::from_value::<ResponseError>(body).map_err(|e| {
ResponseError {
kind: ErrorKind::ApiError,
code: "FetchClient:ResponseErrorDeserializeFail".to_string(),
message: format!("{:#?}", e),
status: Some(status),
}
})?;
return Err(data);
} else if (200..=299).contains(&status) {
if status == 204 {
return if let Some(val) = E::default_response() {
Ok(val)
} else {
Err(ResponseError {
kind: ErrorKind::ApiError,
code: "FetchClient:ResponseError204".to_string(),
message: "Response is empty".to_string(),
status: Some(status),
})
};
}
let body = wasm_bindgen_futures::JsFuture::from(response.json().map_err(|e| {
ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:ResponseJsonPromiseFail".to_string(),
message: format!("{:#?}", e),
status: None,
}
})?)
.await
.map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:ResponseJsonFail".to_string(),
message: format!("{:#?}", e),
status: None,
})?;
let data = serde_wasm_bindgen::from_value::<O>(body).map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "FetchClient:ResponseErrorDeserializeFail".to_string(),
message: e.to_string(),
status: None,
})?;
return Ok(data);
}
Err(ResponseError {
kind: ErrorKind::ApiError,
code: "FetchClient:ApiUnknownStatusError".to_string(),
message: response.status().to_string(),
status: Some(response.status()),
})
}
}
#[wasm_bindgen(getter_with_clone)]
pub struct ApiClientOptions {
base_url: String,
application: String,
}
#[wasm_bindgen]
pub struct ApiClient {
client: ApiClientImpl,
}
#[wasm_bindgen]
impl ApiClient {
#[wasm_bindgen(constructor)]
pub fn new(implementation: &str, options: ApiClientOptions) -> Option<ApiClient> {
match implementation {
"fetch" => Some(Self {
client: ApiClientImpl::Fetch(FetchClient::new(
&options.base_url,
&options.application,
)),
}),
#[cfg(feature = "reqwest")]
"reqwest" => Some(Self {
client: ApiClientImpl::Reqwest(crate::client::reqwest::ReqwestClient::new(
&options.base_url,
&options.application,
)),
}),
_ => None,
}
}
async fn call<I: Serialize, E: Endpoint, O: DeserializeOwned>(
&self,
endpoint: &E,
data: &I,
path_params: &[(&str, &str)],
) -> Result<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static,
{
match &self.client {
ApiClientImpl::Fetch(client) => {
client.call::<I, O, E>(endpoint, data, &path_params).await
}
#[cfg(feature = "reqwest")]
ApiClientImpl::Reqwest(client) => {
client.call::<I, O, E>(endpoint, data, &path_params).await
}
}
}
}
#[wasm_bindgen(getter_with_clone)]
#[derive(Debug, Clone)]
pub struct EndpointLike {
pub path: String,
pub method: String,
}
#[wasm_bindgen]
pub async fn api_call(
client: &ApiClient,
endpoint_like: &EndpointLike,
data: JsValue,
path_params: JsValue,
) -> Result<JsValue, ResponseError> {
let method = Method::try_from(endpoint_like.method.as_str()).map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "Bindgen::ParseMethod".to_string(),
message: e.to_string(),
status: None,
})?;
let endpoint = match_endpoint(&endpoint_like.path, &method).ok_or_else(|| ResponseError {
kind: ErrorKind::Other,
code: "Bindgen::MatchEndpoint".to_string(),
message: format!(
"No endpoint `{}` with method `{}` found",
endpoint_like.path, method
),
status: None,
})?;
let input = serde_wasm_bindgen::from_value(data).map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "Bindgen::DeserializeInput".to_string(),
message: e.to_string(),
status: None,
})?;
let path_params = serde_wasm_bindgen::from_value::<HashMap<String, String>>(path_params)
.map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "Bindgen::DeserializePathParams".to_string(),
message: e.to_string(),
status: None,
})?;
let path_params_borrow = path_params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect::<Vec<(&str, &str)>>();
serde_wasm_bindgen::to_value(&client.call(&endpoint, &input, &path_params_borrow).await?)
.map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "Bindgen::SerializeOutput".to_string(),
message: e.to_string(),
status: None,
})
}

View File

@ -1,12 +0,0 @@
#[cfg(feature = "reqwest")]
pub mod reqwest;
#[cfg(target_arch = "wasm32")]
pub mod fetch;
pub enum ApiClientImpl {
#[cfg(feature = "reqwest")]
Reqwest(reqwest::ReqwestClient),
#[cfg(target_arch = "wasm32")]
Fetch(fetch::FetchClient),
}

View File

@ -1,112 +0,0 @@
use crate::endpoints::{Endpoint, ErrorKind, ResponseError};
use crate::Client;
use http::header::{CONTENT_TYPE, USER_AGENT};
use http::{HeaderMap, HeaderValue, Method, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
pub struct ReqwestClient {
client: reqwest::Client,
base_url: String,
}
impl ReqwestClient {
pub fn new(base_url: &str, application: &str) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
HeaderValue::from_str("application/json").unwrap(),
);
headers.insert(USER_AGENT, HeaderValue::from_str(application).unwrap());
let client_builder = reqwest::ClientBuilder::new().default_headers(headers);
#[cfg(not(target_arch = "wasm32"))]
let client_builder = { client_builder.https_only(true) };
let client = client_builder.build().unwrap();
ReqwestClient {
client,
base_url: base_url.to_string(),
}
}
}
#[async_trait::async_trait(?Send)]
impl Client for ReqwestClient {
fn base_url(&self) -> &str {
&self.base_url
}
async fn call<'a, I, O, E>(
&self,
endpoint: &E,
data: &I,
path_params: &impl AsRef<[(&'a str, &'a str)]>,
) -> Result<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static,
{
let url = endpoint.template_path(self.base_url(), path_params);
let req = self.client.request(E::METHOD, &url);
let req = if E::METHOD == Method::GET {
req.query(&data)
} else {
req.json(&data)
};
let response = req.send().await.map_err(|e| ResponseError {
kind: ErrorKind::Other,
code: "ReqwestClient:Fail".to_string(),
message: e.to_string(),
status: None,
})?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
match response.json::<ResponseError>().await {
Ok(res) => Err(res),
Err(e) => Err(ResponseError {
kind: ErrorKind::ApiError,
code: "ReqwestClient:ApiGenericError".to_string(),
message: e.to_string(),
status: Some(status.as_u16()),
}),
}
} else if status.is_success() {
if status == StatusCode::NO_CONTENT.as_u16() {
return if let Some(val) = E::default_response() {
Ok(val)
} else {
Err(ResponseError {
kind: ErrorKind::ApiError,
code: "ReqwestClient:ResponseError204".to_string(),
message: "Response is empty".to_string(),
status: Some(status.as_u16()),
})
};
}
let data = response.json::<O>().await.map_err(|e| ResponseError {
kind: ErrorKind::ApiError,
code: "ReqwestClient:JsonError".to_string(),
message: e.to_string(),
status: Some(status.as_u16()),
})?;
Ok(data)
} else {
Err(ResponseError {
kind: ErrorKind::ApiError,
code: "ReqwestClient:ApiUnknownStatusError".to_string(),
message: status.to_string(),
status: Some(status.as_u16()),
})
}
}
}

View File

@ -1,21 +0,0 @@
use crate::endpoints::Endpoint;
macro_rules! match_from {
(($endpoint:expr, $method:expr) in [$($e:ty,)*]) => {
match ($endpoint, $method) {
$(
(<$e as Endpoint>::ENDPOINT, &<$e as Endpoint>::METHOD) => Some(<$e as Default>::default()),
)*
_ => None,
}
};
}
pub(crate) fn match_endpoint(endpoint: &str, method: &http::Method) -> Option<impl Endpoint> {
match_from!(
(endpoint, method)
in [
crate::endpoints::user::GetUserById,
]
)
}

View File

@ -1,57 +0,0 @@
pub(crate) mod list;
pub mod user;
use http::Method;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))]
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
pub struct ResponseError {
pub kind: ErrorKind,
pub status: Option<u16>,
pub code: String,
pub message: String,
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, TS)]
pub enum ErrorKind {
#[default]
ApiError,
Other,
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
pub struct Empty;
impl From<Empty> for () {
fn from(_: Empty) -> Self {}
}
pub trait Endpoint {
const NAME: &'static str;
const ENDPOINT: &'static str;
const METHOD: Method;
type Request: Serialize + DeserializeOwned + Send + Sync + 'static;
type Response: Serialize + DeserializeOwned + Send + Sync + 'static;
fn default_response() -> Option<Self::Response> {
None
}
fn template_path<'a>(&self, base_path: &str, var: &impl AsRef<[(&'a str, &'a str)]>) -> String {
let mut path_suffix = Self::ENDPOINT.to_string();
for (key, value) in var.as_ref() {
path_suffix = path_suffix.replace(&format!(":{}", key), value);
}
format!("{}{}", base_path, path_suffix)
}
}

View File

@ -1,24 +0,0 @@
use crate::endpoints::Endpoint;
use http::Method;
use magnetar_sdk_macros::Endpoint;
use serde::{Deserialize, Serialize};
use ts_rs::{Dependency, TS};
use crate::types::user::PackUserProfileFull;
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct UserByIdReq {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
}
#[derive(Endpoint, Deserialize)]
#[endpoint(
endpoint = "/users/:user_id",
method = Method::GET,
request = UserByIdReq,
response = PackUserProfileFull
)]
pub struct GetUserById;

View File

@ -1,23 +0,0 @@
pub mod client;
pub mod endpoints;
pub mod types;
use crate::endpoints::Endpoint;
use endpoints::ResponseError;
use serde::{de::DeserializeOwned, Serialize};
#[async_trait::async_trait(?Send)]
pub trait Client {
fn base_url(&self) -> &str;
async fn call<'a, I, O, E>(
&self,
endpoint: &E,
data: &I,
path_params: &impl AsRef<[(&'a str, &'a str)]>,
) -> Result<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static;
}

View File

@ -1,89 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::types::user::PackUserBase;
use magnetar_sdk_macros::pack;
use crate::types::Id;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct ImageMeta {
pub width: Option<u64>,
pub height: Option<u64>,
pub orientation: Option<u64>,
pub color: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct DriveFolderBase {
pub name: String,
pub created_at: DateTime<Utc>,
pub comment: Option<String>,
pub file_count: u64,
pub folder_count: u64,
pub parent_id: Option<String>,
pub user_id: String,
}
pack!(PackDriveFolderBase, Id as id & DriveFolderBase as folder);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct DriveFolderParentExt {
pub folder: Box<DriveFolderBase>,
}
pack!(
PackDriveFolderWithParent,
Id as id & DriveFolderBase as folder & DriveFolderParentExt as parent
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct DriveFileBase {
pub name: String,
pub created_at: DateTime<Utc>,
pub size: u64,
pub sha256: String,
pub mime_type: String,
pub image_meta: ImageMeta,
pub url: Option<String>,
pub thumbnail_url: Option<String>,
pub sensitive: bool,
pub comment: Option<String>,
pub folder_id: Option<String>,
pub user_id: String,
}
pack!(PackDriveFileBase, Id as id & DriveFileBase as file);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct DriveFileFolderExt {
pub folder: Box<PackDriveFolderBase>,
}
pack!(
PackDriveFileWithFolder,
Id as id & DriveFileBase as file & DriveFileFolderExt as folder
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct DriveFileUserExt {
pub user: Box<PackUserBase>,
}
pack!(
PackDriveFileWithUser,
Id as id & DriveFileBase as file & DriveFileUserExt as user
);
pack!(
PackDriveFileFull,
Id as id & DriveFileBase as file & DriveFileFolderExt as folder & DriveFileUserExt as user
);

View File

@ -1,22 +0,0 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::types::Id;
use magnetar_sdk_macros::pack;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct EmojiBase {
pub shortcode: String,
pub url: String,
pub static_url: String,
pub visible_in_picker: bool,
pub category: Option<String>,
}
pack!(PackEmojiBase, Id as id & EmojiBase as emoji);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[repr(transparent)]
pub struct EmojiContext(pub Vec<String>);

View File

@ -1,44 +0,0 @@
pub mod drive;
pub mod emoji;
pub mod note;
pub mod user;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[repr(transparent)]
pub struct Id {
pub id: String,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum NotificationType {
Follow,
Mention,
Reply,
Renote,
Quote,
Reaction,
PollVote,
PollEnded,
FollowRequest,
FollowRequestAccepted,
App,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct NotificationSettings {
pub enabled: Vec<NotificationType>,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum FollowVisibility {
Public,
Followers,
Private,
}

View File

@ -1,103 +0,0 @@
use crate::types::emoji::EmojiContext;
use crate::types::user::{PackUserBase, UserBase};
use crate::types::Id;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::types::drive::PackDriveFileBase;
use magnetar_sdk_macros::pack;
#[derive(Copy, Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum NoteVisibility {
Public,
Home,
Followers,
Specified,
Direct,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct PollChoice {
pub title: String,
pub votes_count: u64,
pub voted: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct PollBase {
pub expires_at: DateTime<Utc>,
pub expired: bool,
pub multiple_choice: bool,
pub options: Vec<PollChoice>,
}
pack!(PackPollBase, Id as id & PollBase as poll);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct NoteBase {
pub created_at: DateTime<Utc>,
pub cw: Option<String>,
pub uri: Option<String>,
pub url: Option<String>,
pub text: String,
pub visibility: NoteVisibility,
pub user: Box<UserBase>,
pub parent_note_id: Option<String>,
pub renoted_note_id: Option<String>,
pub reply_count: u64,
pub renote_count: u64,
pub hashtags: Vec<String>,
pub reactions: Vec<PackReactionBase>,
pub emojis: EmojiContext,
pub local_only: bool,
}
pack!(PackNoteBase, Id as id & NoteBase as note);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct NoteAttachmentExt {
pub attachments: Vec<PackDriveFileBase>,
pub poll: Option<PollBase>,
}
pack!(
PackNoteWithAttachments,
Id as id & NoteBase as note & NoteAttachmentExt as attachment
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct NoteDetailExt {
pub poll: Option<PollBase>,
pub parent_note: Option<Box<NoteBase>>,
pub renoted_note: Option<Box<NoteBase>>,
}
pack!(
PackNoteFull,
Id as id & NoteBase as note & NoteAttachmentExt as attachment & NoteDetailExt as detail
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct ReactionBase {
pub created_at: DateTime<Utc>,
pub user_id: String,
}
pack!(PackReactionBase, Id as id & ReactionBase as reaction);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct ReactionUserExt {
pub user: Box<PackUserBase>,
}
pack!(
PackReactionWithUser,
Id as id & ReactionBase as reaction & ReactionUserExt as user
);

View File

@ -1,187 +0,0 @@
use crate::types::emoji::EmojiContext;
use crate::types::note::PackNoteBase;
use crate::types::{Id, NotificationSettings};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use magnetar_sdk_macros::pack;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum AvatarDecoration {
None,
CatEars,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum SpeechTransform {
None,
Cat,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct ProfileField {
name: String,
value: String,
verified_at: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserBase {
pub acct: String,
pub username: String,
pub display_name: String,
pub host: Option<String>,
pub speech_transform: SpeechTransform,
pub created_at: DateTime<Utc>,
pub avatar_url: Option<String>,
pub avatar_blurhash: Option<String>,
pub avatar_color: Option<String>,
pub avatar_decoration: AvatarDecoration,
pub admin: bool,
pub moderator: bool,
pub bot: bool,
pub emojis: EmojiContext,
}
pack!(PackUserBase, Id as id & UserBase as user);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserProfileExt {
pub locked: bool,
pub silenced: bool,
pub suspended: bool,
pub description: Option<String>,
pub location: Option<String>,
pub birthday: Option<DateTime<Utc>>,
pub fields: Vec<ProfileField>,
pub follower_count: u64,
pub following_count: u64,
pub note_count: u64,
pub url: Option<String>,
pub moved_to_uri: Option<String>,
pub also_known_as: Option<String>,
pub banner_url: Option<String>,
pub banner_color: Option<String>,
pub banner_blurhash: Option<String>,
pub public_reactions: bool,
}
pack!(
PackUserProfile,
Id as id & UserBase as user & UserProfileExt as profile
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserProfilePinsEx {
pub pinned_notes: Vec<PackNoteBase>,
// pub pinned_page: Option<Page>,
}
pack!(
PackUserProfileFull,
Id as id
& UserBase as user
& UserProfileExt as profile
& UserProfilePinsEx as pins
& UserRelationExt as relation
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserRelationExt {
pub last_fetched_at: Option<DateTime<Utc>>,
pub follows_you: bool,
pub you_follow: bool,
pub blocks_you: bool,
pub you_block: bool,
pub mute: bool,
pub mute_renotes: bool,
}
pack!(
PackUserRelation,
Id as id & UserBase as user & UserRelationExt as relation
);
pack!(
PackUserProfileRelation,
Id as id & UserBase as user & UserProfileExt as profile & UserRelationExt as relation
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserAuthOverviewExt {
pub two_factor_enabled: bool,
pub use_passwordless_login: bool,
pub security_keys: bool,
}
pack!(
PackUserAuthOverview,
Id as id & UserBase as user & UserAuthOverviewExt as auth
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserSelfExt {
pub avatar_id: Option<String>,
pub banner_id: Option<String>,
pub email_announcements: bool,
pub always_mark_sensitive: bool,
pub reject_bot_follow_requests: bool,
pub reject_crawlers: bool,
pub reject_ai_training: bool,
pub has_unread_announcements: bool,
pub has_unread_antenna: bool,
pub has_unread_notifications: bool,
pub has_pending_follow_requests: bool,
pub word_mutes: Vec<String>,
pub instance_mutes: Vec<String>,
pub notification_settings: NotificationSettings,
}
pack!(
PackUserSelf,
Id as id & UserBase as user & UserSelfExt as self_info
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserDetailExt {
pub uri: Option<String>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct SecurityKeyBase {
pub name: String,
pub created_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
}
pack!(PackSecurityKeyBase, Id as id & SecurityKeyBase as key);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserSecretsExt {
pub email: Option<String>,
pub email_verified: bool,
pub security_keys: Vec<SecurityKeyBase>,
}
pack!(
PackUserSelfAll,
Id as id & UserBase as user & UserSelfExt as self_info & UserSecretsExt as secrets
);