Implemented pagination and user follower listing
This commit is contained in:
parent
a658452138
commit
1bef42ead5
|
@ -1519,6 +1519,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -115,6 +115,7 @@ percent-encoding = { workspace = true }
|
||||||
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
serde_urlencoded = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
|
|
|
@ -112,6 +112,8 @@ pub mod rel {
|
||||||
|
|
||||||
link_rel!(pub RelWebFingerProfilePage, "http://webfinger.net/rel/profile-page");
|
link_rel!(pub RelWebFingerProfilePage, "http://webfinger.net/rel/profile-page");
|
||||||
link_rel!(pub RelSelf, "self");
|
link_rel!(pub RelSelf, "self");
|
||||||
|
link_rel!(pub RelNext, "next");
|
||||||
|
link_rel!(pub RelPrev, "prev");
|
||||||
link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe");
|
link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe");
|
||||||
link_rel!(pub RelNodeInfo20, "http://nodeinfo.diaspora.software/ns/schema/2.0");
|
link_rel!(pub RelNodeInfo20, "http://nodeinfo.diaspora.software/ns/schema/2.0");
|
||||||
link_rel!(pub RelNodeInfo21, "http://nodeinfo.diaspora.software/ns/schema/2.1");
|
link_rel!(pub RelNodeInfo21, "http://nodeinfo.diaspora.software/ns/schema/2.1");
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod model_ext;
|
pub mod model_ext;
|
||||||
pub mod note_model;
|
pub mod note_model;
|
||||||
pub mod paginated;
|
|
||||||
pub mod poll;
|
pub mod poll;
|
||||||
pub mod user_model;
|
pub mod user_model;
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ use thiserror::Error;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::log::LevelFilter;
|
use tracing::log::LevelFilter;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info, trace};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ConnectorConfig {
|
pub struct ConnectorConfig {
|
||||||
|
@ -445,7 +444,7 @@ impl CalckeySub {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Got message: {:#?}", parsed);
|
trace!("Got message: {:#?}", parsed);
|
||||||
|
|
||||||
handler(parsed).await;
|
handler(parsed).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use ext_calckey_model_migration::{
|
use ext_calckey_model_migration::{
|
||||||
Alias, Expr, IntoIden, Quote, SelectExpr, SelectStatement, TableRef,
|
Alias, Expr, IntoIden, Quote, SelectExpr, SelectStatement, TableRef,
|
||||||
};
|
};
|
||||||
|
use magnetar_sdk::types::SpanFilter;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait, Condition, DynIden, EntityTrait, Iden, Iterable, JoinType, RelationDef,
|
ColumnTrait, Condition, ConnectionTrait, Cursor, CursorTrait, DbErr, DynIden, EntityTrait,
|
||||||
RelationTrait,
|
FromQueryResult, Iden, IntoIdentity, Iterable, JoinType, RelationDef, RelationTrait, Select,
|
||||||
|
SelectModel, SelectorTrait,
|
||||||
};
|
};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
@ -126,12 +129,21 @@ impl SelectColumnsExt for SelectStatement {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AliasColumnExt {
|
pub trait AliasColumnExt {
|
||||||
|
fn col_tuple(&self, col: impl IntoIden) -> (MagIden, MagIden);
|
||||||
|
|
||||||
fn col(&self, col: impl IntoIden) -> Expr;
|
fn col(&self, col: impl IntoIden) -> Expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Iden + ?Sized> AliasColumnExt for T {
|
impl<T: Iden + ?Sized> AliasColumnExt for T {
|
||||||
|
fn col_tuple(&self, col: impl IntoIden) -> (MagIden, MagIden) {
|
||||||
|
(
|
||||||
|
MagIden::alias(&self.to_string()),
|
||||||
|
MagIden::DynIden(col.into_iden()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn col(&self, col: impl IntoIden) -> Expr {
|
fn col(&self, col: impl IntoIden) -> Expr {
|
||||||
Expr::col((Alias::new(self.to_string()).into_iden(), col.into_iden()))
|
Expr::col(self.col_tuple(col))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,6 +193,8 @@ impl<T: EntityTrait> EntityPrefixExt for T {
|
||||||
pub trait AliasSourceExt {
|
pub trait AliasSourceExt {
|
||||||
fn with_from_alias(&self, alias: &MagIden) -> RelationDef;
|
fn with_from_alias(&self, alias: &MagIden) -> RelationDef;
|
||||||
|
|
||||||
|
fn with_to_alias(&self, alias: &MagIden) -> RelationDef;
|
||||||
|
|
||||||
fn with_alias(&self, from: &MagIden, to: &MagIden) -> RelationDef;
|
fn with_alias(&self, from: &MagIden, to: &MagIden) -> RelationDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,6 +205,12 @@ impl<T: RelationTrait> AliasSourceExt for T {
|
||||||
def
|
def
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_to_alias(&self, alias: &MagIden) -> RelationDef {
|
||||||
|
let mut def = self.def();
|
||||||
|
def.to_tbl = def.to_tbl.alias(Alias::new(alias.clone().to_string()));
|
||||||
|
def
|
||||||
|
}
|
||||||
|
|
||||||
fn with_alias(&self, from: &MagIden, to: &MagIden) -> RelationDef {
|
fn with_alias(&self, from: &MagIden, to: &MagIden) -> RelationDef {
|
||||||
let mut def = self.def();
|
let mut def = self.def();
|
||||||
def.from_tbl = def.from_tbl.alias(from.clone().into_iden());
|
def.from_tbl = def.from_tbl.alias(from.clone().into_iden());
|
||||||
|
@ -198,3 +218,109 @@ impl<T: RelationTrait> AliasSourceExt for T {
|
||||||
def
|
def
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CursorPaginationExt<E> {
|
||||||
|
type Selector: SelectorTrait + Send + Sync;
|
||||||
|
|
||||||
|
fn cursor_by_columns_and_span<C>(
|
||||||
|
self,
|
||||||
|
order_columns: C,
|
||||||
|
pagination: &SpanFilter,
|
||||||
|
limit: Option<u64>,
|
||||||
|
) -> Cursor<Self::Selector>
|
||||||
|
where
|
||||||
|
C: IntoIdentity;
|
||||||
|
|
||||||
|
async fn get_paginated_model<M, C, T>(
|
||||||
|
self,
|
||||||
|
db: &T,
|
||||||
|
columns: C,
|
||||||
|
curr: &SpanFilter,
|
||||||
|
prev: &mut Option<SpanFilter>,
|
||||||
|
next: &mut Option<SpanFilter>,
|
||||||
|
limit: u64,
|
||||||
|
) -> Result<Vec<M>, DbErr>
|
||||||
|
where
|
||||||
|
M: FromQueryResult + ModelPagination,
|
||||||
|
C: IntoIdentity,
|
||||||
|
T: ConnectionTrait;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, M> CursorPaginationExt<E> for Select<E>
|
||||||
|
where
|
||||||
|
E: EntityTrait<Model = M>,
|
||||||
|
M: FromQueryResult + Sized + Send + Sync,
|
||||||
|
{
|
||||||
|
type Selector = SelectModel<M>;
|
||||||
|
|
||||||
|
fn cursor_by_columns_and_span<C>(
|
||||||
|
self,
|
||||||
|
order_columns: C,
|
||||||
|
pagination: &SpanFilter,
|
||||||
|
limit: Option<u64>,
|
||||||
|
) -> Cursor<Self::Selector>
|
||||||
|
where
|
||||||
|
C: IntoIdentity,
|
||||||
|
{
|
||||||
|
let mut cursor = self.cursor_by(order_columns);
|
||||||
|
|
||||||
|
if let Some(start) = pagination.start() {
|
||||||
|
cursor.after(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = pagination.end() {
|
||||||
|
cursor.before(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lim) = limit {
|
||||||
|
if pagination.is_desc() {
|
||||||
|
cursor.last(lim);
|
||||||
|
} else {
|
||||||
|
cursor.first(lim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_paginated_model<Q, C, T>(
|
||||||
|
self,
|
||||||
|
db: &T,
|
||||||
|
columns: C,
|
||||||
|
curr: &SpanFilter,
|
||||||
|
prev: &mut Option<SpanFilter>,
|
||||||
|
next: &mut Option<SpanFilter>,
|
||||||
|
limit: u64,
|
||||||
|
) -> Result<Vec<Q>, DbErr>
|
||||||
|
where
|
||||||
|
Q: FromQueryResult + ModelPagination,
|
||||||
|
C: IntoIdentity,
|
||||||
|
T: ConnectionTrait,
|
||||||
|
{
|
||||||
|
let mut result = self
|
||||||
|
.cursor_by_columns_and_span(columns, curr, Some(limit + 1))
|
||||||
|
.into_model::<Q>()
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if curr.is_desc() {
|
||||||
|
result.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.len() > limit as usize {
|
||||||
|
result.pop();
|
||||||
|
let last = result.last();
|
||||||
|
*next = last.and_then(|c| curr.next(c.time(), c.id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = result.first();
|
||||||
|
*prev = first.and_then(|c| curr.prev(c.time(), c.id()));
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ModelPagination {
|
||||||
|
fn id(&self) -> &str;
|
||||||
|
fn time(&self) -> DateTime<Utc>;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use crate::model_ext::{AliasSuffixExt, EntityPrefixExt, MagIden};
|
use crate::model_ext::{AliasSuffixExt, EntityPrefixExt, MagIden, ModelPagination};
|
||||||
use crate::note_model::{INTERACTION_REACTION, INTERACTION_RENOTE, RENOTE, REPLY, USER};
|
use crate::note_model::{INTERACTION_REACTION, INTERACTION_RENOTE, RENOTE, REPLY, USER};
|
||||||
use crate::user_model::UserData;
|
use crate::user_model::UserData;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use ck::note;
|
use ck::note;
|
||||||
use ext_calckey_model_migration::IntoIden;
|
|
||||||
use sea_orm::sea_query::Alias;
|
|
||||||
use sea_orm::Iden;
|
use sea_orm::Iden;
|
||||||
use sea_orm::{DbErr, FromQueryResult, QueryResult};
|
use sea_orm::{DbErr, FromQueryResult, QueryResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -48,6 +47,16 @@ pub struct NoteData {
|
||||||
pub renote: Option<Box<NoteData>>,
|
pub renote: Option<Box<NoteData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ModelPagination for NoteData {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
&self.note.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time(&self) -> DateTime<Utc> {
|
||||||
|
self.note.created_at.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromQueryResult for NoteData {
|
impl FromQueryResult for NoteData {
|
||||||
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||||
let prefix = if prefix.is_empty() {
|
let prefix = if prefix.is_empty() {
|
||||||
|
|
|
@ -3,8 +3,8 @@ pub mod data;
|
||||||
use ext_calckey_model_migration::SelectStatement;
|
use ext_calckey_model_migration::SelectStatement;
|
||||||
use sea_orm::sea_query::{Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr};
|
use sea_orm::sea_query::{Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
Condition, CursorTrait, EntityName, EntityTrait, Iden, JoinType, QueryFilter, QueryOrder,
|
Condition, EntityTrait, Iden, JoinType, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
|
||||||
QuerySelect, QueryTrait, Select,
|
Select,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ck::{note, note_reaction, user, user_note_pining};
|
use ck::{note, note_reaction, user, user_note_pining};
|
||||||
|
@ -12,10 +12,9 @@ use data::{sub_interaction_reaction, sub_interaction_renote, NoteData};
|
||||||
use magnetar_sdk::types::SpanFilter;
|
use magnetar_sdk::types::SpanFilter;
|
||||||
|
|
||||||
use crate::model_ext::{
|
use crate::model_ext::{
|
||||||
join_columns_default, AliasColumnExt, AliasSourceExt, AliasSuffixExt, EntityPrefixExt, MagIden,
|
join_columns_default, AliasColumnExt, AliasSourceExt, AliasSuffixExt, CursorPaginationExt,
|
||||||
SelectColumnsExt,
|
EntityPrefixExt, MagIden, SelectColumnsExt,
|
||||||
};
|
};
|
||||||
use crate::paginated::PaginatedModel;
|
|
||||||
use crate::user_model::{UserResolveOptions, UserResolver};
|
use crate::user_model::{UserResolveOptions, UserResolver};
|
||||||
use crate::{CalckeyDbError, CalckeyModel};
|
use crate::{CalckeyDbError, CalckeyModel};
|
||||||
|
|
||||||
|
@ -138,21 +137,17 @@ impl NoteResolver {
|
||||||
let visibility_filter = options
|
let visibility_filter = options
|
||||||
.visibility_filter
|
.visibility_filter
|
||||||
.with_note_and_user_tables(¬e::Entity.base_prefix());
|
.with_note_and_user_tables(¬e::Entity.base_prefix());
|
||||||
let time_filter = options
|
|
||||||
.time_range
|
|
||||||
.as_ref()
|
|
||||||
.and_then(note::Entity::range_into_expr);
|
|
||||||
let id_filter = options.ids.as_ref().map(ids_into_expr);
|
let id_filter = options.ids.as_ref().map(ids_into_expr);
|
||||||
|
|
||||||
let notes = select
|
let note = select
|
||||||
.filter(visibility_filter)
|
.filter(visibility_filter)
|
||||||
.apply_if(id_filter, Select::<note::Entity>::filter)
|
.apply_if(id_filter, Select::<note::Entity>::filter)
|
||||||
.apply_if(time_filter, Select::<note::Entity>::filter)
|
|
||||||
.into_model::<NoteData>()
|
.into_model::<NoteData>()
|
||||||
.one(self.db.inner())
|
.one(self.db.inner())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(notes)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_many(
|
pub async fn get_many(
|
||||||
|
@ -163,26 +158,36 @@ impl NoteResolver {
|
||||||
let visibility_filter = options
|
let visibility_filter = options
|
||||||
.visibility_filter
|
.visibility_filter
|
||||||
.with_note_and_user_tables(¬e::Entity.base_prefix());
|
.with_note_and_user_tables(¬e::Entity.base_prefix());
|
||||||
let time_filter = options
|
|
||||||
.time_range
|
|
||||||
.as_ref()
|
|
||||||
.and_then(note::Entity::range_into_expr);
|
|
||||||
let id_filter = options.ids.as_ref().map(ids_into_expr);
|
let id_filter = options.ids.as_ref().map(ids_into_expr);
|
||||||
|
|
||||||
let notes = select
|
let notes_select = select
|
||||||
.filter(visibility_filter)
|
.filter(visibility_filter)
|
||||||
.apply_if(id_filter, Select::<note::Entity>::filter)
|
.apply_if(id_filter, Select::<note::Entity>::filter)
|
||||||
.apply_if(time_filter, Select::<note::Entity>::filter)
|
|
||||||
.apply_if(options.only_pins_from.as_deref(), |s, _| {
|
.apply_if(options.only_pins_from.as_deref(), |s, _| {
|
||||||
s.order_by_desc(Expr::col((
|
s.order_by_desc(Expr::col((
|
||||||
note::Entity.base_prefix().into_iden().join_str(PIN),
|
note::Entity.base_prefix().into_iden().join_str(PIN),
|
||||||
user_note_pining::Column::CreatedAt,
|
user_note_pining::Column::CreatedAt,
|
||||||
)))
|
)))
|
||||||
})
|
});
|
||||||
|
|
||||||
|
let notes = if let Some(pagination) = &options.time_range {
|
||||||
|
notes_select
|
||||||
|
.get_paginated_model::<NoteData, _, _>(
|
||||||
|
self.db.inner(),
|
||||||
|
(note::Column::CreatedAt, note::Column::Id),
|
||||||
|
pagination,
|
||||||
|
&mut None,
|
||||||
|
&mut None,
|
||||||
|
options.limit.unwrap_or(20),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
notes_select
|
||||||
.apply_if(options.limit, Select::<note::Entity>::limit)
|
.apply_if(options.limit, Select::<note::Entity>::limit)
|
||||||
.into_model::<NoteData>()
|
.into_model::<NoteData>()
|
||||||
.all(self.db.inner())
|
.all(self.db.inner())
|
||||||
.await?;
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
Ok(notes)
|
Ok(notes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
use ext_calckey_model_migration::{Expr, SimpleExpr};
|
|
||||||
use magnetar_sdk::types::{EndFilter, RangeFilter, SpanFilter, StartFilter};
|
|
||||||
use sea_orm::Iden;
|
|
||||||
|
|
||||||
pub trait PaginatedModel: 'static {
|
|
||||||
fn time_column() -> impl Iden;
|
|
||||||
fn id_column() -> impl Iden;
|
|
||||||
|
|
||||||
fn range_into_expr(filter: &SpanFilter) -> Option<SimpleExpr> {
|
|
||||||
match filter {
|
|
||||||
SpanFilter::Range(RangeFilter {
|
|
||||||
time_start,
|
|
||||||
time_end,
|
|
||||||
id_start,
|
|
||||||
id_end,
|
|
||||||
}) => Some(
|
|
||||||
Expr::tuple([
|
|
||||||
Expr::col(Self::time_column()).into(),
|
|
||||||
Expr::col(Self::id_column()).into(),
|
|
||||||
])
|
|
||||||
.between(
|
|
||||||
Expr::tuple([
|
|
||||||
Expr::value(time_start.clone()),
|
|
||||||
Expr::value(id_start.clone()),
|
|
||||||
]),
|
|
||||||
Expr::tuple([Expr::value(time_end.clone()), Expr::value(id_end.clone())]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SpanFilter::Start(StartFilter {
|
|
||||||
id_start,
|
|
||||||
time_start,
|
|
||||||
}) => Some(
|
|
||||||
Expr::tuple([
|
|
||||||
Expr::col(Self::time_column()).into(),
|
|
||||||
Expr::col(Self::id_column()).into(),
|
|
||||||
])
|
|
||||||
.gt(Expr::tuple([
|
|
||||||
Expr::value(time_start.clone()),
|
|
||||||
Expr::value(id_start.clone()),
|
|
||||||
])),
|
|
||||||
),
|
|
||||||
SpanFilter::End(EndFilter { id_end, time_end }) => Some(
|
|
||||||
Expr::tuple([
|
|
||||||
Expr::col(Self::time_column()).into(),
|
|
||||||
Expr::col(Self::id_column()).into(),
|
|
||||||
])
|
|
||||||
.lt(Expr::tuple([
|
|
||||||
Expr::value(time_end.clone()),
|
|
||||||
Expr::value(id_end.clone()),
|
|
||||||
])),
|
|
||||||
),
|
|
||||||
SpanFilter::None(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginatedModel for ck::note::Entity {
|
|
||||||
fn time_column() -> impl Iden {
|
|
||||||
ck::note::Column::CreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id_column() -> impl Iden {
|
|
||||||
ck::note::Column::Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginatedModel for ck::user::Entity {
|
|
||||||
fn time_column() -> impl Iden {
|
|
||||||
ck::user::Column::CreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id_column() -> impl Iden {
|
|
||||||
ck::user::Column::Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginatedModel for ck::following::Entity {
|
|
||||||
fn time_column() -> impl Iden {
|
|
||||||
ck::following::Column::CreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id_column() -> impl Iden {
|
|
||||||
ck::following::Column::Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginatedModel for ck::follow_request::Entity {
|
|
||||||
fn time_column() -> impl Iden {
|
|
||||||
ck::follow_request::Column::CreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id_column() -> impl Iden {
|
|
||||||
ck::follow_request::Column::Id
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +1,25 @@
|
||||||
use crate::model_ext::{AliasSourceExt, AliasSuffixExt, EntityPrefixExt, MagIden};
|
use crate::model_ext::{
|
||||||
use crate::{model_ext::SelectColumnsExt, CalckeyModel};
|
AliasSourceExt, AliasSuffixExt, CursorPaginationExt, EntityPrefixExt, MagIden, ModelPagination,
|
||||||
use ck::{drive_file, user};
|
};
|
||||||
|
use crate::{model_ext::SelectColumnsExt, CalckeyDbError, CalckeyModel};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use ck::{drive_file, follow_request, following, user, user_profile};
|
||||||
use ext_calckey_model_migration::{IntoIden, SelectStatement};
|
use ext_calckey_model_migration::{IntoIden, SelectStatement};
|
||||||
use sea_orm::sea_query::Alias;
|
use magnetar_sdk::types::SpanFilter;
|
||||||
use sea_orm::{DbErr, FromQueryResult, Iden, JoinType, QueryResult};
|
use sea_orm::{
|
||||||
|
ColumnTrait, DbErr, EntityTrait, FromQueryResult, Iden, JoinType, QueryFilter, QueryResult,
|
||||||
|
QuerySelect,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserData {
|
pub struct UserData {
|
||||||
pub user: user::Model,
|
pub user: user::Model,
|
||||||
|
pub profile: Option<user_profile::Model>,
|
||||||
pub avatar: Option<drive_file::Model>,
|
pub avatar: Option<drive_file::Model>,
|
||||||
pub banner: Option<drive_file::Model>,
|
pub banner: Option<drive_file::Model>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserResolveOptions {
|
|
||||||
pub with_avatar_and_banner: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AVATAR: &str = "avatar.";
|
|
||||||
const BANNER: &str = "banner.";
|
|
||||||
|
|
||||||
impl FromQueryResult for UserData {
|
impl FromQueryResult for UserData {
|
||||||
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||||
let prefix = if prefix.is_empty() {
|
let prefix = if prefix.is_empty() {
|
||||||
|
@ -30,6 +30,10 @@ impl FromQueryResult for UserData {
|
||||||
|
|
||||||
Ok(UserData {
|
Ok(UserData {
|
||||||
user: user::Model::from_query_result(res, &prefix.to_string())?,
|
user: user::Model::from_query_result(res, &prefix.to_string())?,
|
||||||
|
profile: user_profile::Model::from_query_result_optional(
|
||||||
|
res,
|
||||||
|
&prefix.join_str_as_str(PROFILE),
|
||||||
|
)?,
|
||||||
avatar: drive_file::Model::from_query_result_optional(
|
avatar: drive_file::Model::from_query_result_optional(
|
||||||
res,
|
res,
|
||||||
&prefix.join_str_as_str(AVATAR),
|
&prefix.join_str_as_str(AVATAR),
|
||||||
|
@ -42,6 +46,75 @@ impl FromQueryResult for UserData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ModelPagination for UserData {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
&self.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time(&self) -> DateTime<Utc> {
|
||||||
|
self.user.created_at.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UserFollowData {
|
||||||
|
pub follow: following::Model,
|
||||||
|
pub user: UserData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromQueryResult for UserFollowData {
|
||||||
|
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||||
|
Ok(UserFollowData {
|
||||||
|
user: UserData::from_query_result(res, prefix)?,
|
||||||
|
follow: following::Model::from_query_result(res, prefix)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelPagination for UserFollowData {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
&self.follow.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time(&self) -> DateTime<Utc> {
|
||||||
|
self.follow.created_at.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UserFollowRequestData {
|
||||||
|
pub follow_request: follow_request::Model,
|
||||||
|
pub user: UserData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromQueryResult for UserFollowRequestData {
|
||||||
|
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||||
|
Ok(UserFollowRequestData {
|
||||||
|
user: UserData::from_query_result(res, prefix)?,
|
||||||
|
follow_request: follow_request::Model::from_query_result(res, prefix)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelPagination for UserFollowRequestData {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
&self.follow_request.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time(&self) -> DateTime<Utc> {
|
||||||
|
self.follow_request.created_at.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UserResolveOptions {
|
||||||
|
pub with_avatar_and_banner: bool,
|
||||||
|
pub with_profile: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROFILE: &str = "profile.";
|
||||||
|
const AVATAR: &str = "avatar.";
|
||||||
|
const BANNER: &str = "banner.";
|
||||||
|
|
||||||
pub struct UserResolver {
|
pub struct UserResolver {
|
||||||
db: CalckeyModel,
|
db: CalckeyModel,
|
||||||
}
|
}
|
||||||
|
@ -57,10 +130,23 @@ impl UserResolver {
|
||||||
user_tbl: &MagIden,
|
user_tbl: &MagIden,
|
||||||
UserResolveOptions {
|
UserResolveOptions {
|
||||||
with_avatar_and_banner,
|
with_avatar_and_banner,
|
||||||
|
with_profile,
|
||||||
}: &UserResolveOptions,
|
}: &UserResolveOptions,
|
||||||
) {
|
) {
|
||||||
q.add_aliased_columns::<user::Entity>(user_tbl);
|
q.add_aliased_columns::<user::Entity>(user_tbl);
|
||||||
|
|
||||||
|
if *with_profile {
|
||||||
|
let profile_tbl = user_tbl.join_str(PROFILE);
|
||||||
|
|
||||||
|
q.add_aliased_columns::<user_profile::Entity>(&profile_tbl);
|
||||||
|
|
||||||
|
q.join_columns(
|
||||||
|
JoinType::LeftJoin,
|
||||||
|
user::Relation::UserProfile.with_from_alias(user_tbl),
|
||||||
|
&profile_tbl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if *with_avatar_and_banner {
|
if *with_avatar_and_banner {
|
||||||
let avatar_tbl = user_tbl.join_str(AVATAR);
|
let avatar_tbl = user_tbl.join_str(AVATAR);
|
||||||
let banner_tbl = user_tbl.join_str(BANNER);
|
let banner_tbl = user_tbl.join_str(BANNER);
|
||||||
|
@ -81,7 +167,120 @@ impl UserResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_followers(&self, user_id: &str) -> Vec<UserData> {
|
pub async fn get_follow_requests(
|
||||||
todo!()
|
&self,
|
||||||
|
options: &UserResolveOptions,
|
||||||
|
followee: &str,
|
||||||
|
pagination: &SpanFilter,
|
||||||
|
prev: &mut Option<SpanFilter>,
|
||||||
|
next: &mut Option<SpanFilter>,
|
||||||
|
limit: u64,
|
||||||
|
) -> Result<Vec<UserData>, CalckeyDbError> {
|
||||||
|
let user_tbl = user::Entity.base_prefix();
|
||||||
|
|
||||||
|
let mut select = follow_request::Entity::find().join_as(
|
||||||
|
JoinType::InnerJoin,
|
||||||
|
follow_request::Relation::User1.with_to_alias(&user_tbl),
|
||||||
|
user_tbl.clone().into_iden(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = QuerySelect::query(&mut select);
|
||||||
|
self.resolve(query, &user_tbl, options);
|
||||||
|
|
||||||
|
let followers = select
|
||||||
|
.filter(follow_request::Column::FolloweeId.eq(followee))
|
||||||
|
.get_paginated_model::<UserFollowRequestData, _, _>(
|
||||||
|
&self.db.0,
|
||||||
|
(
|
||||||
|
follow_request::Column::CreatedAt,
|
||||||
|
follow_request::Column::Id,
|
||||||
|
),
|
||||||
|
pagination,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| u.user)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(followers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_followees(
|
||||||
|
&self,
|
||||||
|
options: &UserResolveOptions,
|
||||||
|
follower: &str,
|
||||||
|
pagination: &SpanFilter,
|
||||||
|
prev: &mut Option<SpanFilter>,
|
||||||
|
next: &mut Option<SpanFilter>,
|
||||||
|
limit: u64,
|
||||||
|
) -> Result<Vec<UserData>, CalckeyDbError> {
|
||||||
|
let user_tbl = user::Entity.base_prefix();
|
||||||
|
|
||||||
|
let mut select = following::Entity::find().join_as(
|
||||||
|
JoinType::InnerJoin,
|
||||||
|
following::Relation::User2.with_to_alias(&user_tbl),
|
||||||
|
user_tbl.clone().into_iden(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = QuerySelect::query(&mut select);
|
||||||
|
self.resolve(query, &user_tbl, options);
|
||||||
|
|
||||||
|
let followers = select
|
||||||
|
.filter(following::Column::FollowerId.eq(follower))
|
||||||
|
.get_paginated_model::<UserFollowData, _, _>(
|
||||||
|
&self.db.0,
|
||||||
|
(following::Column::CreatedAt, following::Column::Id),
|
||||||
|
pagination,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| u.user)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(followers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_followers(
|
||||||
|
&self,
|
||||||
|
options: &UserResolveOptions,
|
||||||
|
followee: &str,
|
||||||
|
pagination: &SpanFilter,
|
||||||
|
prev: &mut Option<SpanFilter>,
|
||||||
|
next: &mut Option<SpanFilter>,
|
||||||
|
limit: u64,
|
||||||
|
) -> Result<Vec<UserData>, CalckeyDbError> {
|
||||||
|
let user_tbl = user::Entity.base_prefix();
|
||||||
|
|
||||||
|
let mut select = following::Entity::find().join_as(
|
||||||
|
JoinType::InnerJoin,
|
||||||
|
following::Relation::User1.with_to_alias(&user_tbl),
|
||||||
|
user_tbl.clone().into_iden(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = QuerySelect::query(&mut select);
|
||||||
|
self.resolve(query, &user_tbl, options);
|
||||||
|
|
||||||
|
let followers = select
|
||||||
|
.filter(following::Column::FolloweeId.eq(followee))
|
||||||
|
.get_paginated_model::<UserFollowData, _, _>(
|
||||||
|
&self.db.0,
|
||||||
|
(following::Column::CreatedAt, following::Column::Id),
|
||||||
|
pagination,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| u.user)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(followers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub enum ErrorKind {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct Empty;
|
pub struct Empty {}
|
||||||
|
|
||||||
impl From<Empty> for () {
|
impl From<Empty> for () {
|
||||||
fn from(_: Empty) -> Self {}
|
fn from(_: Empty) -> Self {}
|
||||||
|
|
|
@ -88,7 +88,7 @@ pub struct GetUserByAcct;
|
||||||
endpoint = "/users/:id/followers",
|
endpoint = "/users/:id/followers",
|
||||||
method = Method::GET,
|
method = Method::GET,
|
||||||
request = "Empty",
|
request = "Empty",
|
||||||
response = "Vec<PackUserBase>"
|
response = "Vec<PackUserMaybeAll>"
|
||||||
)]
|
)]
|
||||||
pub struct GetFollowersById;
|
pub struct GetFollowersById;
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ pub struct GetFollowersById;
|
||||||
endpoint = "/users/:id/following",
|
endpoint = "/users/:id/following",
|
||||||
method = Method::GET,
|
method = Method::GET,
|
||||||
request = "Empty",
|
request = "Empty",
|
||||||
response = "Vec<PackUserBase>"
|
response = "Vec<PackUserMaybeAll>"
|
||||||
)]
|
)]
|
||||||
pub struct GetFollowingById;
|
pub struct GetFollowingById;
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ pub struct GetFollowingById;
|
||||||
endpoint = "/users/@self/followers",
|
endpoint = "/users/@self/followers",
|
||||||
method = Method::GET,
|
method = Method::GET,
|
||||||
request = "Empty",
|
request = "Empty",
|
||||||
response = "Vec<PackUserBase>"
|
response = "Vec<PackUserMaybeAll>"
|
||||||
)]
|
)]
|
||||||
pub struct GetFollowersSelf;
|
pub struct GetFollowersSelf;
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ pub struct GetFollowersSelf;
|
||||||
endpoint = "/users/@self/following",
|
endpoint = "/users/@self/following",
|
||||||
method = Method::GET,
|
method = Method::GET,
|
||||||
request = "Empty",
|
request = "Empty",
|
||||||
response = "Vec<PackUserBase>"
|
response = "Vec<PackUserMaybeAll>"
|
||||||
)]
|
)]
|
||||||
pub struct GetFollowingSelf;
|
pub struct GetFollowingSelf;
|
||||||
|
|
||||||
|
@ -124,6 +124,6 @@ pub struct GetFollowingSelf;
|
||||||
endpoint = "/users/@self/follow-requests",
|
endpoint = "/users/@self/follow-requests",
|
||||||
method = Method::GET,
|
method = Method::GET,
|
||||||
request = "Empty",
|
request = "Empty",
|
||||||
response = "Vec<PackUserBase>"
|
response = "Vec<PackUserMaybeAll>"
|
||||||
)]
|
)]
|
||||||
pub struct GetFollowRequestsSelf;
|
pub struct GetFollowRequestsSelf;
|
||||||
|
|
|
@ -5,8 +5,11 @@ pub mod note;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
|
use crate::endpoints::ErrorKind;
|
||||||
|
use crate::util_types::U64Range;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::de::Error;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
@ -14,22 +17,22 @@ use ts_rs::TS;
|
||||||
pub struct RangeFilter {
|
pub struct RangeFilter {
|
||||||
pub time_start: DateTime<Utc>,
|
pub time_start: DateTime<Utc>,
|
||||||
pub time_end: DateTime<Utc>,
|
pub time_end: DateTime<Utc>,
|
||||||
pub id_start: DateTime<Utc>,
|
pub id_start: String,
|
||||||
pub id_end: DateTime<Utc>,
|
pub id_end: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct StartFilter {
|
pub struct StartFilter {
|
||||||
pub time_start: DateTime<Utc>,
|
pub time_start: DateTime<Utc>,
|
||||||
pub id_start: DateTime<Utc>,
|
pub id_start: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct EndFilter {
|
pub struct EndFilter {
|
||||||
pub time_end: DateTime<Utc>,
|
pub time_end: DateTime<Utc>,
|
||||||
pub id_end: DateTime<Utc>,
|
pub id_end: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
@ -46,6 +49,119 @@ pub enum SpanFilter {
|
||||||
None(NoFilter),
|
None(NoFilter),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SpanFilter {
|
||||||
|
pub fn is_desc(&self) -> bool {
|
||||||
|
!matches!(self, Self::Start(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> Option<(DateTime<Utc>, String)> {
|
||||||
|
match self {
|
||||||
|
Self::Start(StartFilter {
|
||||||
|
time_start,
|
||||||
|
id_start,
|
||||||
|
})
|
||||||
|
| Self::Range(RangeFilter {
|
||||||
|
time_start,
|
||||||
|
id_start,
|
||||||
|
..
|
||||||
|
}) => Some((*time_start, id_start.clone())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(&self) -> Option<(DateTime<Utc>, String)> {
|
||||||
|
match self {
|
||||||
|
Self::End(EndFilter { time_end, id_end })
|
||||||
|
| Self::Range(RangeFilter {
|
||||||
|
time_end, id_end, ..
|
||||||
|
}) => Some((*time_end, id_end.clone())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self, first_date: DateTime<Utc>, first_id: &str) -> Option<SpanFilter> {
|
||||||
|
match self {
|
||||||
|
Self::Start(_) => Some(SpanFilter::End(EndFilter {
|
||||||
|
time_end: first_date,
|
||||||
|
id_end: first_id.to_string(),
|
||||||
|
})),
|
||||||
|
Self::End(_) => Some(SpanFilter::Start(StartFilter {
|
||||||
|
time_start: first_date,
|
||||||
|
id_start: first_id.to_string(),
|
||||||
|
})),
|
||||||
|
// TODO: This is jank
|
||||||
|
// It forgets the bounds of the cursor
|
||||||
|
Self::Range(_) => Some(SpanFilter::Start(StartFilter {
|
||||||
|
time_start: first_date,
|
||||||
|
id_start: first_id.to_string(),
|
||||||
|
})),
|
||||||
|
Self::None(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self, last_date: DateTime<Utc>, last_id: &str) -> Option<SpanFilter> {
|
||||||
|
match self {
|
||||||
|
Self::Start(_) => Some(SpanFilter::Start(StartFilter {
|
||||||
|
time_start: last_date,
|
||||||
|
id_start: last_id.to_string(),
|
||||||
|
})),
|
||||||
|
Self::End(_) | Self::None(_) => Some(SpanFilter::End(EndFilter {
|
||||||
|
time_end: last_date,
|
||||||
|
id_end: last_id.to_string(),
|
||||||
|
})),
|
||||||
|
Self::Range(RangeFilter { time_start, .. }) if *time_start > last_date => None,
|
||||||
|
Self::Range(RangeFilter {
|
||||||
|
time_start,
|
||||||
|
id_start,
|
||||||
|
..
|
||||||
|
}) => Some(SpanFilter::Range(RangeFilter {
|
||||||
|
time_start: *time_start,
|
||||||
|
id_start: id_start.clone(),
|
||||||
|
time_end: last_date,
|
||||||
|
id_end: last_id.to_string(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct PaginationShape {
|
||||||
|
#[serde(default = "pagination_default")]
|
||||||
|
#[serde(deserialize_with = "deserialize_pagination")]
|
||||||
|
pub pagination: SpanFilter,
|
||||||
|
#[serde(default = "limit_defalt")]
|
||||||
|
#[serde(deserialize_with = "deserialize_limit")]
|
||||||
|
pub limit: U64Range<10, 100>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pagination_default() -> SpanFilter {
|
||||||
|
SpanFilter::None(NoFilter {})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limit_defalt() -> U64Range<10, 100> {
|
||||||
|
U64Range::try_from(20).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_limit<'de, D>(deserializer: D) -> Result<U64Range<10, 100>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let str_val = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
U64Range::try_from(str_val.parse::<u64>().map_err(Error::custom)?)
|
||||||
|
.map_err(|_| Error::custom("number out of range"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_pagination<'de, D>(deserializer: D) -> Result<SpanFilter, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let str_val = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
serde_urlencoded::from_str::<SpanFilter>(&str_val).map_err(Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
|
|
|
@ -3,7 +3,9 @@ mod user;
|
||||||
|
|
||||||
use crate::api_v1::note::handle_note;
|
use crate::api_v1::note::handle_note;
|
||||||
use crate::api_v1::user::{
|
use crate::api_v1::user::{
|
||||||
handle_user_by_id_many, handle_user_info, handle_user_info_by_acct, handle_user_info_self,
|
handle_follow_requests_self, handle_followers, handle_followers_self, handle_following,
|
||||||
|
handle_following_self, handle_user_by_id_many, handle_user_info, handle_user_info_by_acct,
|
||||||
|
handle_user_info_self,
|
||||||
};
|
};
|
||||||
use crate::service::MagnetarService;
|
use crate::service::MagnetarService;
|
||||||
use crate::web::auth;
|
use crate::web::auth;
|
||||||
|
@ -19,6 +21,14 @@ pub fn create_api_router(service: Arc<MagnetarService>) -> Router {
|
||||||
.route("/users/by-acct/:id", get(handle_user_info_by_acct))
|
.route("/users/by-acct/:id", get(handle_user_info_by_acct))
|
||||||
.route("/users/lookup-many", get(handle_user_by_id_many))
|
.route("/users/lookup-many", get(handle_user_by_id_many))
|
||||||
.route("/users/:id", get(handle_user_info))
|
.route("/users/:id", get(handle_user_info))
|
||||||
|
.route(
|
||||||
|
"/users/@self/follow-requests",
|
||||||
|
get(handle_follow_requests_self),
|
||||||
|
)
|
||||||
|
.route("/users/@self/following", get(handle_following_self))
|
||||||
|
.route("/users/:id/following", get(handle_following))
|
||||||
|
.route("/users/@self/followers", get(handle_followers_self))
|
||||||
|
.route("/users/:id/followers", get(handle_followers))
|
||||||
.route("/notes/:id", get(handle_note))
|
.route("/notes/:id", get(handle_note))
|
||||||
.layer(from_fn_with_state(
|
.layer(from_fn_with_state(
|
||||||
AuthState::new(service.clone()),
|
AuthState::new(service.clone()),
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use crate::model::processing::user::{UserBorrowedData, UserModel};
|
use crate::model::processing::user::{UserBorrowedData, UserModel, UserShapedData};
|
||||||
use crate::model::processing::PackError;
|
|
||||||
use crate::model::PackingContext;
|
use crate::model::PackingContext;
|
||||||
use crate::service::MagnetarService;
|
use crate::service::MagnetarService;
|
||||||
use crate::web::auth::{AuthenticatedUser, MaybeUser};
|
use crate::web::auth::{AuthenticatedUser, MaybeUser};
|
||||||
|
use crate::web::pagination::Pagination;
|
||||||
use crate::web::{ApiError, ArgumentOutOfRange, ObjectNotFound};
|
use crate::web::{ApiError, ArgumentOutOfRange, ObjectNotFound};
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use futures::StreamExt;
|
|
||||||
use futures_util::TryStreamExt;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use magnetar_common::util::lenient_parse_tag_decode;
|
use magnetar_common::util::lenient_parse_tag_decode;
|
||||||
use magnetar_sdk::endpoints::user::{
|
use magnetar_sdk::endpoints::user::{
|
||||||
|
@ -29,6 +27,7 @@ pub async fn handle_user_info_self(
|
||||||
&ctx,
|
&ctx,
|
||||||
&UserBorrowedData {
|
&UserBorrowedData {
|
||||||
user: user.as_ref(),
|
user: user.as_ref(),
|
||||||
|
profile: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
},
|
},
|
||||||
|
@ -56,6 +55,7 @@ pub async fn handle_user_info(
|
||||||
&ctx,
|
&ctx,
|
||||||
&UserBorrowedData {
|
&UserBorrowedData {
|
||||||
user: &user_model,
|
user: &user_model,
|
||||||
|
profile: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
},
|
},
|
||||||
|
@ -88,6 +88,7 @@ pub async fn handle_user_info_by_acct(
|
||||||
&ctx,
|
&ctx,
|
||||||
&UserBorrowedData {
|
&UserBorrowedData {
|
||||||
user: &user_model,
|
user: &user_model,
|
||||||
|
profile: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
},
|
},
|
||||||
|
@ -118,20 +119,15 @@ pub async fn handle_user_by_id_many(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|user| UserBorrowedData {
|
.map(|user| UserBorrowedData {
|
||||||
user,
|
user,
|
||||||
|
profile: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
})
|
})
|
||||||
|
.map(|u| Box::new(u) as Box<dyn UserShapedData>)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let futures = user_data
|
let users_proc = user_model
|
||||||
.iter()
|
.pack_many_base(&ctx, &user_data.iter().map(Box::as_ref).collect::<Vec<_>>())
|
||||||
.map(|user| user_model.base_from_existing(&ctx, user))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let users_proc = futures::stream::iter(futures)
|
|
||||||
.buffered(20)
|
|
||||||
.err_into::<PackError>()
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| (u.id.0.id.clone(), u))
|
.map(|u| (u.id.0.id.clone(), u))
|
||||||
|
@ -144,3 +140,76 @@ pub async fn handle_user_by_id_many(
|
||||||
|
|
||||||
Ok(Json(users_ordered))
|
Ok(Json(users_ordered))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_following_self(
|
||||||
|
Query(_): Query<Req<GetFollowingSelf>>,
|
||||||
|
State(service): State<Arc<MagnetarService>>,
|
||||||
|
AuthenticatedUser(user): AuthenticatedUser,
|
||||||
|
mut pagination: Pagination,
|
||||||
|
) -> Result<(Pagination, Json<Res<GetFollowingSelf>>), ApiError> {
|
||||||
|
let ctx = PackingContext::new(service, Some(user.clone())).await?;
|
||||||
|
let user_model = UserModel;
|
||||||
|
let users = user_model
|
||||||
|
.get_followees(&ctx, &user.id, &mut pagination)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((pagination, Json(users)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_followers_self(
|
||||||
|
Query(_): Query<Req<GetFollowersSelf>>,
|
||||||
|
State(service): State<Arc<MagnetarService>>,
|
||||||
|
AuthenticatedUser(user): AuthenticatedUser,
|
||||||
|
mut pagination: Pagination,
|
||||||
|
) -> Result<(Pagination, Json<Res<GetFollowersSelf>>), ApiError> {
|
||||||
|
let ctx = PackingContext::new(service, Some(user.clone())).await?;
|
||||||
|
let user_model = UserModel;
|
||||||
|
let users = user_model
|
||||||
|
.get_followers(&ctx, &user.id, &mut pagination)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((pagination, Json(users)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_following(
|
||||||
|
Query(_): Query<Req<GetFollowingById>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(service): State<Arc<MagnetarService>>,
|
||||||
|
MaybeUser(user): MaybeUser,
|
||||||
|
mut pagination: Pagination,
|
||||||
|
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
|
||||||
|
let ctx = PackingContext::new(service, user.clone()).await?;
|
||||||
|
let user_model = UserModel;
|
||||||
|
let users = user_model.get_followees(&ctx, &id, &mut pagination).await?;
|
||||||
|
|
||||||
|
Ok((pagination, Json(users)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_followers(
|
||||||
|
Query(_): Query<Req<GetFollowersById>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(service): State<Arc<MagnetarService>>,
|
||||||
|
MaybeUser(user): MaybeUser,
|
||||||
|
mut pagination: Pagination,
|
||||||
|
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
|
||||||
|
let ctx = PackingContext::new(service, user.clone()).await?;
|
||||||
|
let user_model = UserModel;
|
||||||
|
let users = user_model.get_followers(&ctx, &id, &mut pagination).await?;
|
||||||
|
|
||||||
|
Ok((pagination, Json(users)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_follow_requests_self(
|
||||||
|
Query(_): Query<Req<GetFollowRequestsSelf>>,
|
||||||
|
State(service): State<Arc<MagnetarService>>,
|
||||||
|
AuthenticatedUser(user): AuthenticatedUser,
|
||||||
|
mut pagination: Pagination,
|
||||||
|
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
|
||||||
|
let ctx = PackingContext::new(service, Some(user.clone())).await?;
|
||||||
|
let user_model = UserModel;
|
||||||
|
let users = user_model
|
||||||
|
.get_follow_requests(&ctx, &user.id, &mut pagination)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((pagination, Json(users)))
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ use magnetar_calckey_model::note_model::data::{
|
||||||
};
|
};
|
||||||
use magnetar_calckey_model::note_model::{NoteResolveOptions, NoteVisibilityFilterFactory};
|
use magnetar_calckey_model::note_model::{NoteResolveOptions, NoteVisibilityFilterFactory};
|
||||||
use magnetar_calckey_model::poll::PollResolver;
|
use magnetar_calckey_model::poll::PollResolver;
|
||||||
use magnetar_calckey_model::sea_orm::sea_query::{IntoIden, PgFunc, Query, SimpleExpr};
|
use magnetar_calckey_model::sea_orm::sea_query::{PgFunc, Query, SimpleExpr};
|
||||||
use magnetar_calckey_model::sea_orm::{ActiveEnum, ColumnTrait, Iden, IntoSimpleExpr};
|
use magnetar_calckey_model::sea_orm::{ActiveEnum, ColumnTrait, Iden, IntoSimpleExpr};
|
||||||
use magnetar_calckey_model::user_model::UserResolveOptions;
|
use magnetar_calckey_model::user_model::UserResolveOptions;
|
||||||
use magnetar_calckey_model::{ck, CalckeyDbError};
|
use magnetar_calckey_model::{ck, CalckeyDbError};
|
||||||
|
@ -581,6 +581,7 @@ impl NoteModel {
|
||||||
limit: None,
|
limit: None,
|
||||||
user_options: UserResolveOptions {
|
user_options: UserResolveOptions {
|
||||||
with_avatar_and_banner: self.with_context,
|
with_avatar_and_banner: self.with_context,
|
||||||
|
with_profile: false,
|
||||||
},
|
},
|
||||||
with_reply_target: self.with_context,
|
with_reply_target: self.with_context,
|
||||||
with_renote_target: self.with_context,
|
with_renote_target: self.with_context,
|
||||||
|
@ -619,6 +620,7 @@ impl NoteModel {
|
||||||
limit: None,
|
limit: None,
|
||||||
user_options: UserResolveOptions {
|
user_options: UserResolveOptions {
|
||||||
with_avatar_and_banner: self.with_context,
|
with_avatar_and_banner: self.with_context,
|
||||||
|
with_profile: false,
|
||||||
},
|
},
|
||||||
with_reply_target: self.with_context,
|
with_reply_target: self.with_context,
|
||||||
with_renote_target: self.with_context,
|
with_renote_target: self.with_context,
|
||||||
|
|
|
@ -4,10 +4,14 @@ use crate::model::processing::emoji::EmojiModel;
|
||||||
use crate::model::processing::note::NoteModel;
|
use crate::model::processing::note::NoteModel;
|
||||||
use crate::model::processing::{get_mm_token_emoji, PackError, PackResult};
|
use crate::model::processing::{get_mm_token_emoji, PackError, PackResult};
|
||||||
use crate::model::{PackType, PackingContext};
|
use crate::model::{PackType, PackingContext};
|
||||||
|
use crate::web::pagination::Pagination;
|
||||||
|
use crate::web::{AccessForbidden, ApiError};
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use futures_util::future::OptionFuture;
|
use futures_util::future::OptionFuture;
|
||||||
|
use futures_util::{StreamExt, TryStreamExt};
|
||||||
use magnetar_calckey_model::ck;
|
use magnetar_calckey_model::ck;
|
||||||
use magnetar_calckey_model::user_model::UserData;
|
use magnetar_calckey_model::ck::sea_orm_active_enums::UserProfileFfvisibilityEnum;
|
||||||
|
use magnetar_calckey_model::user_model::{UserData, UserResolveOptions};
|
||||||
use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq};
|
use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq};
|
||||||
use magnetar_sdk::mmm::Token;
|
use magnetar_sdk::mmm::Token;
|
||||||
use magnetar_sdk::types::drive::PackDriveFileBase;
|
use magnetar_sdk::types::drive::PackDriveFileBase;
|
||||||
|
@ -27,12 +31,14 @@ use url::Url;
|
||||||
|
|
||||||
pub trait UserShapedData<'a>: Send + Sync {
|
pub trait UserShapedData<'a>: Send + Sync {
|
||||||
fn user(&self) -> &'a ck::user::Model;
|
fn user(&self) -> &'a ck::user::Model;
|
||||||
|
fn profile(&self) -> Option<&'a ck::user_profile::Model>;
|
||||||
fn avatar(&self) -> Option<&'a ck::drive_file::Model>;
|
fn avatar(&self) -> Option<&'a ck::drive_file::Model>;
|
||||||
fn banner(&self) -> Option<&'a ck::drive_file::Model>;
|
fn banner(&self) -> Option<&'a ck::drive_file::Model>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserBorrowedData<'a> {
|
pub struct UserBorrowedData<'a> {
|
||||||
pub user: &'a ck::user::Model,
|
pub user: &'a ck::user::Model,
|
||||||
|
pub profile: Option<&'a ck::user_profile::Model>,
|
||||||
pub avatar: Option<&'a ck::drive_file::Model>,
|
pub avatar: Option<&'a ck::drive_file::Model>,
|
||||||
pub banner: Option<&'a ck::drive_file::Model>,
|
pub banner: Option<&'a ck::drive_file::Model>,
|
||||||
}
|
}
|
||||||
|
@ -42,6 +48,10 @@ impl<'a> UserShapedData<'a> for &'a UserData {
|
||||||
&self.user
|
&self.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn profile(&self) -> Option<&'a ck::user_profile::Model> {
|
||||||
|
self.profile.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
||||||
self.avatar.as_ref()
|
self.avatar.as_ref()
|
||||||
}
|
}
|
||||||
|
@ -56,6 +66,10 @@ impl<'a> UserShapedData<'a> for UserBorrowedData<'a> {
|
||||||
self.user
|
self.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn profile(&self) -> Option<&'a ck::user_profile::Model> {
|
||||||
|
self.profile
|
||||||
|
}
|
||||||
|
|
||||||
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
||||||
self.avatar
|
self.avatar
|
||||||
}
|
}
|
||||||
|
@ -179,6 +193,18 @@ impl UserModel {
|
||||||
.ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
|
.ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_profile_by_id(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
id: &str,
|
||||||
|
) -> PackResult<ck::user_profile::Model> {
|
||||||
|
ctx.service
|
||||||
|
.db
|
||||||
|
.get_user_profile_by_id(id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn profile_from_base<'a>(
|
pub async fn profile_from_base<'a>(
|
||||||
&self,
|
&self,
|
||||||
ctx: &PackingContext,
|
ctx: &PackingContext,
|
||||||
|
@ -295,14 +321,15 @@ impl UserModel {
|
||||||
) -> PackResult<PackUserSelfMaybeAll> {
|
) -> PackResult<PackUserSelfMaybeAll> {
|
||||||
let user = user_data.user();
|
let user = user_data.user();
|
||||||
|
|
||||||
let should_fetch_profile =
|
let should_fetch_profile = user_data.profile().is_none()
|
||||||
req.profile.unwrap_or_default() || req.secrets.unwrap_or_default();
|
&& (req.profile.unwrap_or_default() || req.secrets.unwrap_or_default());
|
||||||
let profile_raw_promise =
|
let profile_raw_promise =
|
||||||
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
|
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
|
||||||
let (base_res, profile_res) =
|
let (base_res, profile_res) =
|
||||||
join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
|
join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
|
||||||
let mut base = base_res?;
|
let mut base = base_res?;
|
||||||
let profile_raw = profile_res.transpose()?;
|
let profile_raw = profile_res.transpose()?;
|
||||||
|
let profile_ref = user_data.profile().or(profile_raw.as_ref());
|
||||||
|
|
||||||
let detail = req
|
let detail = req
|
||||||
.detail
|
.detail
|
||||||
|
@ -313,7 +340,7 @@ impl UserModel {
|
||||||
self.profile_from_base(
|
self.profile_from_base(
|
||||||
ctx,
|
ctx,
|
||||||
user_data,
|
user_data,
|
||||||
profile_raw.as_ref().unwrap(),
|
profile_ref.unwrap(),
|
||||||
None,
|
None,
|
||||||
&mut base.user.0.emojis,
|
&mut base.user.0.emojis,
|
||||||
)
|
)
|
||||||
|
@ -332,7 +359,7 @@ impl UserModel {
|
||||||
let secrets = OptionFuture::from(
|
let secrets = OptionFuture::from(
|
||||||
req.secrets
|
req.secrets
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.then(|| self.secrets_from_base(ctx, user, profile_raw.as_ref().unwrap())),
|
.then(|| self.secrets_from_base(ctx, user, profile_ref.unwrap())),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (profile_res, pins_res, secrets_res) = join!(profile, pins, secrets);
|
let (profile_res, pins_res, secrets_res) = join!(profile, pins, secrets);
|
||||||
|
@ -486,4 +513,205 @@ impl UserModel {
|
||||||
auth: Optional(auth),
|
auth: Optional(auth),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn pack_many_base(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
users: &[&dyn UserShapedData<'_>],
|
||||||
|
) -> PackResult<Vec<PackUserBase>> {
|
||||||
|
let futures = users
|
||||||
|
.iter()
|
||||||
|
.map(|&user| self.base_from_existing(ctx, user))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let users_proc = futures::stream::iter(futures)
|
||||||
|
.buffered(20)
|
||||||
|
.err_into::<PackError>()
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(users_proc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn pack_many_maybe_full(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
users: &[&dyn UserShapedData<'_>],
|
||||||
|
req: &UserByIdReq,
|
||||||
|
) -> PackResult<Vec<PackUserMaybeAll>> {
|
||||||
|
let futures = users
|
||||||
|
.iter()
|
||||||
|
.map(|&user| self.foreign_full_from_base(ctx, user, req))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let users_proc = futures::stream::iter(futures)
|
||||||
|
.buffered(20)
|
||||||
|
.err_into::<PackError>()
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(users_proc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn follower_visibility_check(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
if !ctx.is_id_self(id) {
|
||||||
|
let profile = self.get_profile_by_id(ctx, id).await?;
|
||||||
|
|
||||||
|
match (&ctx.self_user, &profile.ff_visibility) {
|
||||||
|
(_, UserProfileFfvisibilityEnum::Public) => {}
|
||||||
|
(Some(self_user), UserProfileFfvisibilityEnum::Followers)
|
||||||
|
if ctx
|
||||||
|
.is_relationship_between(
|
||||||
|
Either::Right(self_user),
|
||||||
|
Either::Left(id),
|
||||||
|
UserRelationship::Follow,
|
||||||
|
)
|
||||||
|
.await? => {}
|
||||||
|
_ => {
|
||||||
|
Err(AccessForbidden(
|
||||||
|
"Follower information not visible".to_string(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_followers(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
id: &str,
|
||||||
|
pagination: &mut Pagination,
|
||||||
|
) -> Result<Vec<PackUserMaybeAll>, ApiError> {
|
||||||
|
self.follower_visibility_check(ctx, id).await?;
|
||||||
|
|
||||||
|
let users = ctx
|
||||||
|
.service
|
||||||
|
.db
|
||||||
|
.get_user_resolver()
|
||||||
|
.get_followers(
|
||||||
|
&UserResolveOptions {
|
||||||
|
with_avatar_and_banner: true,
|
||||||
|
with_profile: true,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
&pagination.current,
|
||||||
|
&mut pagination.prev,
|
||||||
|
&mut pagination.next,
|
||||||
|
pagination.limit.into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let users_ref = users
|
||||||
|
.iter()
|
||||||
|
.map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.pack_many_maybe_full(
|
||||||
|
ctx,
|
||||||
|
&users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
|
||||||
|
&UserByIdReq {
|
||||||
|
profile: Some(true),
|
||||||
|
auth: None,
|
||||||
|
detail: None,
|
||||||
|
pins: None,
|
||||||
|
relation: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_followees(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
id: &str,
|
||||||
|
pagination: &mut Pagination,
|
||||||
|
) -> Result<Vec<PackUserMaybeAll>, ApiError> {
|
||||||
|
self.follower_visibility_check(ctx, id).await?;
|
||||||
|
|
||||||
|
let users = ctx
|
||||||
|
.service
|
||||||
|
.db
|
||||||
|
.get_user_resolver()
|
||||||
|
.get_followees(
|
||||||
|
&UserResolveOptions {
|
||||||
|
with_avatar_and_banner: true,
|
||||||
|
with_profile: true,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
&pagination.current,
|
||||||
|
&mut pagination.prev,
|
||||||
|
&mut pagination.next,
|
||||||
|
pagination.limit.into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let users_ref = users
|
||||||
|
.iter()
|
||||||
|
.map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.pack_many_maybe_full(
|
||||||
|
ctx,
|
||||||
|
&users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
|
||||||
|
&UserByIdReq {
|
||||||
|
profile: Some(true),
|
||||||
|
auth: None,
|
||||||
|
detail: None,
|
||||||
|
pins: None,
|
||||||
|
relation: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_follow_requests(
|
||||||
|
&self,
|
||||||
|
ctx: &PackingContext,
|
||||||
|
id: &str,
|
||||||
|
pagination: &mut Pagination,
|
||||||
|
) -> Result<Vec<PackUserMaybeAll>, ApiError> {
|
||||||
|
let users = ctx
|
||||||
|
.service
|
||||||
|
.db
|
||||||
|
.get_user_resolver()
|
||||||
|
.get_follow_requests(
|
||||||
|
&UserResolveOptions {
|
||||||
|
with_avatar_and_banner: true,
|
||||||
|
with_profile: true,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
&pagination.current,
|
||||||
|
&mut pagination.prev,
|
||||||
|
&mut pagination.next,
|
||||||
|
pagination.limit.into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let users_ref = users
|
||||||
|
.iter()
|
||||||
|
.map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.pack_many_maybe_full(
|
||||||
|
ctx,
|
||||||
|
&users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
|
||||||
|
&UserByIdReq {
|
||||||
|
profile: Some(true),
|
||||||
|
auth: None,
|
||||||
|
detail: None,
|
||||||
|
pins: None,
|
||||||
|
relation: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
152
src/util.rs
152
src/util.rs
|
@ -1,118 +1,48 @@
|
||||||
use cached::{Cached, TimedCache};
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
use std::future::Future;
|
use serde_json::Value;
|
||||||
use std::hash::Hash;
|
pub fn serialize_as_urlenc(value: &Value) -> String {
|
||||||
use std::sync::Arc;
|
match value {
|
||||||
use tokio::sync::Mutex;
|
Value::Null => "null".to_string(),
|
||||||
|
Value::Bool(b) => b.to_string(),
|
||||||
|
Value::Number(n) => utf8_percent_encode(&n.to_string(), NON_ALPHANUMERIC).to_string(),
|
||||||
|
Value::String(s) => utf8_percent_encode(s, NON_ALPHANUMERIC).to_string(),
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let pairs = arr
|
||||||
|
.iter()
|
||||||
|
.map(|k| {
|
||||||
|
utf8_percent_encode(
|
||||||
|
&if matches!(k, Value::Array(_) | Value::Object(_)) {
|
||||||
|
utf8_percent_encode(&serialize_as_urlenc(k), NON_ALPHANUMERIC)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
serialize_as_urlenc(k)
|
||||||
|
},
|
||||||
|
NON_ALPHANUMERIC,
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pairs.join("&")
|
||||||
pub struct SingleTimedAsyncCache<V: Clone + Send + 'static> {
|
|
||||||
inner: Arc<Mutex<TimedCache<(), V>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: Clone + Send + 'static> SingleTimedAsyncCache<V> {
|
|
||||||
pub fn with_lifespan(lifespan: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(Mutex::new(TimedCache::with_lifespan(lifespan))),
|
|
||||||
}
|
}
|
||||||
|
Value::Object(entries) => {
|
||||||
|
let pairs = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
format!(
|
||||||
|
"{}={}",
|
||||||
|
utf8_percent_encode(&k, NON_ALPHANUMERIC).to_string(),
|
||||||
|
if matches!(v, Value::Array(_) | Value::Object(_)) {
|
||||||
|
utf8_percent_encode(&serialize_as_urlenc(v), NON_ALPHANUMERIC)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
serialize_as_urlenc(v)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
pub async fn put(&self, val: V) -> Option<V> {
|
pairs.join("&")
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
cache.cache_set((), val)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_opt(&self) -> Option<V> {
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
cache.cache_get(&()).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get<E, FT, F>(&self, f: F) -> Result<V, E>
|
|
||||||
where
|
|
||||||
FT: Future<Output = Result<V, E>>,
|
|
||||||
F: FnOnce() -> FT,
|
|
||||||
{
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
|
|
||||||
if let Some(val) = cache.cache_get(&()) {
|
|
||||||
return Ok(val.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let val = f().await?;
|
|
||||||
cache.cache_set((), val.clone());
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sync<E, F>(&self, f: F) -> Result<V, E>
|
|
||||||
where
|
|
||||||
F: FnOnce() -> Result<V, E>,
|
|
||||||
{
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
|
|
||||||
if let Some(val) = cache.cache_get(&()) {
|
|
||||||
return Ok(val.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let val = f()?;
|
|
||||||
cache.cache_set((), val.clone());
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TimedAsyncCache<K: Clone + Send, V: Clone + Send + 'static> {
|
|
||||||
inner: Arc<Mutex<TimedCache<K, V>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Send + Eq + Hash, V: Clone + Send + 'static> TimedAsyncCache<K, V> {
|
|
||||||
pub fn with_lifespan(lifespan: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(Mutex::new(TimedCache::with_lifespan(lifespan))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn put(&self, key: K, val: V) -> Option<V> {
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
cache.cache_set(key, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove(&self, key: &K) -> Option<V> {
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
cache.cache_remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_opt(&self, key: &K) -> Option<V> {
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
cache.cache_get(key).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get<E, FT, F>(&self, key: K, f: F) -> Result<V, E>
|
|
||||||
where
|
|
||||||
FT: Future<Output = Result<V, E>>,
|
|
||||||
F: FnOnce() -> FT,
|
|
||||||
{
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
|
|
||||||
if let Some(val) = cache.cache_get(&key) {
|
|
||||||
return Ok(val.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let val = f().await?;
|
|
||||||
cache.cache_set(key, val.clone());
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sync<E, F>(&self, key: K, f: F) -> Result<V, E>
|
|
||||||
where
|
|
||||||
F: FnOnce() -> Result<V, E>,
|
|
||||||
{
|
|
||||||
let mut cache = self.inner.lock().await;
|
|
||||||
|
|
||||||
if let Some(val) = cache.cache_get(&key) {
|
|
||||||
return Ok(val.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let val = f()?;
|
|
||||||
cache.cache_set(key, val.clone());
|
|
||||||
Ok(val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod pagination;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
|
@ -42,6 +43,15 @@ pub struct ApiError {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AccessForbidden(pub String);
|
||||||
|
|
||||||
|
impl From<&AccessForbidden> for &str {
|
||||||
|
fn from(_: &AccessForbidden) -> &'static str {
|
||||||
|
"AccessForbidden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
(
|
(
|
||||||
|
@ -56,6 +66,20 @@ impl IntoResponse for ApiError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AccessForbidden> for ApiError {
|
||||||
|
fn from(err: AccessForbidden) -> Self {
|
||||||
|
Self {
|
||||||
|
status: StatusCode::FORBIDDEN,
|
||||||
|
code: err.error_code(),
|
||||||
|
message: if cfg!(debug_assertions) {
|
||||||
|
format!("Forbidden: {}", err.0)
|
||||||
|
} else {
|
||||||
|
"Forbidden".to_string()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<FediverseTagParseError> for ApiError {
|
impl From<FediverseTagParseError> for ApiError {
|
||||||
fn from(err: FediverseTagParseError) -> Self {
|
fn from(err: FediverseTagParseError) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
use crate::service::MagnetarService;
|
||||||
|
use crate::util::serialize_as_urlenc;
|
||||||
|
use crate::web::{ApiError, IntoErrorCode};
|
||||||
|
use axum::extract::rejection::QueryRejection;
|
||||||
|
use axum::extract::{FromRequestParts, OriginalUri, Query};
|
||||||
|
use axum::http::header::InvalidHeaderValue;
|
||||||
|
use axum::http::request::Parts;
|
||||||
|
use axum::http::{HeaderValue, StatusCode, Uri};
|
||||||
|
use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
|
||||||
|
use axum::RequestPartsExt;
|
||||||
|
use either::Either;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use magnetar_calckey_model::sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use magnetar_core::web_model::rel::{RelNext, RelPrev};
|
||||||
|
use magnetar_sdk::types::{NoFilter, PaginationShape, SpanFilter};
|
||||||
|
use magnetar_sdk::util_types::U64Range;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use strum::IntoStaticStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Pagination {
|
||||||
|
base_uri: Uri,
|
||||||
|
pub current: SpanFilter,
|
||||||
|
pub prev: Option<SpanFilter>,
|
||||||
|
pub next: Option<SpanFilter>,
|
||||||
|
pub limit: U64Range<10, 100>,
|
||||||
|
query_rest: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct PaginationQuery {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pagination: PaginationShape,
|
||||||
|
#[serde(flatten)]
|
||||||
|
query_rest: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, IntoStaticStr)]
|
||||||
|
pub enum PaginationBuilderError {
|
||||||
|
#[error("Query rejection: {0}")]
|
||||||
|
QueryRejection(#[from] QueryRejection),
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
HttpError(#[from] axum::http::Error),
|
||||||
|
#[error("Value of out of range error")]
|
||||||
|
OutOfRange,
|
||||||
|
#[error("Invalid header value")]
|
||||||
|
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||||
|
#[error("Query string serialization error: {0}")]
|
||||||
|
SerializationErrorQuery(#[from] serde_urlencoded::ser::Error),
|
||||||
|
#[error("Query string serialization error: {0}")]
|
||||||
|
SerializationErrorJson(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PaginationBuilderError> for ApiError {
|
||||||
|
fn from(err: PaginationBuilderError) -> Self {
|
||||||
|
Self {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
code: err.error_code(),
|
||||||
|
message: if cfg!(debug_assertions) {
|
||||||
|
format!("Pagination builder error: {}", err)
|
||||||
|
} else {
|
||||||
|
"Pagination builder error".to_string()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for PaginationBuilderError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
ApiError::from(self).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FromRequestParts<Arc<MagnetarService>> for Pagination {
|
||||||
|
type Rejection = PaginationBuilderError;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut Parts,
|
||||||
|
state: &Arc<MagnetarService>,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let OriginalUri(original_uri) = parts.extract::<OriginalUri>().await.unwrap();
|
||||||
|
|
||||||
|
let base_uri = Uri::builder()
|
||||||
|
.scheme(state.config.networking.protocol.as_ref())
|
||||||
|
.authority(state.config.networking.host.as_str())
|
||||||
|
.path_and_query(original_uri.path())
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let Query(PaginationQuery {
|
||||||
|
pagination,
|
||||||
|
query_rest,
|
||||||
|
}) = parts.extract::<Query<PaginationQuery>>().await?;
|
||||||
|
|
||||||
|
Ok(Pagination {
|
||||||
|
base_uri,
|
||||||
|
prev: None,
|
||||||
|
next: None,
|
||||||
|
current: pagination.pagination,
|
||||||
|
limit: pagination.limit,
|
||||||
|
query_rest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponseParts for Pagination {
|
||||||
|
type Error = PaginationBuilderError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
let wrap = |uri: Uri, query: String, rel: Either<RelPrev, RelNext>| {
|
||||||
|
format!(
|
||||||
|
"<{}?{}>; rel=\"{}\"",
|
||||||
|
uri.to_string(),
|
||||||
|
query,
|
||||||
|
rel.as_ref()
|
||||||
|
.map_either(RelPrev::as_ref, RelNext::as_ref)
|
||||||
|
.into_inner()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_prev = if let Some(prev) = self.prev {
|
||||||
|
let query_prev = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
|
||||||
|
pagination: PaginationShape {
|
||||||
|
pagination: prev,
|
||||||
|
limit: self.limit,
|
||||||
|
},
|
||||||
|
query_rest: self.query_rest.clone(),
|
||||||
|
})?);
|
||||||
|
|
||||||
|
Some(wrap(
|
||||||
|
self.base_uri.clone(),
|
||||||
|
query_prev,
|
||||||
|
Either::Left(RelPrev),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_next = if let Some(next) = self.next {
|
||||||
|
let query_next = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
|
||||||
|
pagination: PaginationShape {
|
||||||
|
pagination: next,
|
||||||
|
limit: self.limit,
|
||||||
|
},
|
||||||
|
query_rest: self.query_rest,
|
||||||
|
})?);
|
||||||
|
|
||||||
|
Some(wrap(self.base_uri, query_next, Either::Right(RelNext)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts = [url_prev, url_next].iter().flatten().join(", ");
|
||||||
|
if !parts.is_empty() {
|
||||||
|
res.headers_mut()
|
||||||
|
.insert(axum::http::header::LINK, HeaderValue::from_str(&parts)?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue