Implemented pagination and user follower listing
This commit is contained in:
parent
a658452138
commit
1bef42ead5
|
@ -1519,6 +1519,7 @@ dependencies = [
|
|||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"strum",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
|
@ -115,6 +115,7 @@ percent-encoding = { workspace = true }
|
|||
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
toml = { 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 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 RelNodeInfo20, "http://nodeinfo.diaspora.software/ns/schema/2.0");
|
||||
link_rel!(pub RelNodeInfo21, "http://nodeinfo.diaspora.software/ns/schema/2.1");
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
pub mod emoji;
|
||||
pub mod model_ext;
|
||||
pub mod note_model;
|
||||
pub mod paginated;
|
||||
pub mod poll;
|
||||
pub mod user_model;
|
||||
|
||||
|
@ -27,7 +26,7 @@ use thiserror::Error;
|
|||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::log::LevelFilter;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectorConfig {
|
||||
|
@ -445,7 +444,7 @@ impl CalckeySub {
|
|||
}
|
||||
};
|
||||
|
||||
println!("Got message: {:#?}", parsed);
|
||||
trace!("Got message: {:#?}", parsed);
|
||||
|
||||
handler(parsed).await;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use ext_calckey_model_migration::{
|
||||
Alias, Expr, IntoIden, Quote, SelectExpr, SelectStatement, TableRef,
|
||||
};
|
||||
use magnetar_sdk::types::SpanFilter;
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, DynIden, EntityTrait, Iden, Iterable, JoinType, RelationDef,
|
||||
RelationTrait,
|
||||
ColumnTrait, Condition, ConnectionTrait, Cursor, CursorTrait, DbErr, DynIden, EntityTrait,
|
||||
FromQueryResult, Iden, IntoIdentity, Iterable, JoinType, RelationDef, RelationTrait, Select,
|
||||
SelectModel, SelectorTrait,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
|
||||
|
@ -126,12 +129,21 @@ impl SelectColumnsExt for SelectStatement {
|
|||
}
|
||||
|
||||
pub trait AliasColumnExt {
|
||||
fn col_tuple(&self, col: impl IntoIden) -> (MagIden, MagIden);
|
||||
|
||||
fn col(&self, col: impl IntoIden) -> Expr;
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -191,6 +205,12 @@ impl<T: RelationTrait> AliasSourceExt for T {
|
|||
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 {
|
||||
let mut def = self.def();
|
||||
def.from_tbl = def.from_tbl.alias(from.clone().into_iden());
|
||||
|
@ -198,3 +218,109 @@ impl<T: RelationTrait> AliasSourceExt for T {
|
|||
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::user_model::UserData;
|
||||
use chrono::{DateTime, Utc};
|
||||
use ck::note;
|
||||
use ext_calckey_model_migration::IntoIden;
|
||||
use sea_orm::sea_query::Alias;
|
||||
use sea_orm::Iden;
|
||||
use sea_orm::{DbErr, FromQueryResult, QueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -48,6 +47,16 @@ pub struct 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 {
|
||||
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||
let prefix = if prefix.is_empty() {
|
||||
|
|
|
@ -3,8 +3,8 @@ pub mod data;
|
|||
use ext_calckey_model_migration::SelectStatement;
|
||||
use sea_orm::sea_query::{Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr};
|
||||
use sea_orm::{
|
||||
Condition, CursorTrait, EntityName, EntityTrait, Iden, JoinType, QueryFilter, QueryOrder,
|
||||
QuerySelect, QueryTrait, Select,
|
||||
Condition, EntityTrait, Iden, JoinType, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
|
||||
Select,
|
||||
};
|
||||
|
||||
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 crate::model_ext::{
|
||||
join_columns_default, AliasColumnExt, AliasSourceExt, AliasSuffixExt, EntityPrefixExt, MagIden,
|
||||
SelectColumnsExt,
|
||||
join_columns_default, AliasColumnExt, AliasSourceExt, AliasSuffixExt, CursorPaginationExt,
|
||||
EntityPrefixExt, MagIden, SelectColumnsExt,
|
||||
};
|
||||
use crate::paginated::PaginatedModel;
|
||||
use crate::user_model::{UserResolveOptions, UserResolver};
|
||||
use crate::{CalckeyDbError, CalckeyModel};
|
||||
|
||||
|
@ -138,21 +137,17 @@ impl NoteResolver {
|
|||
let visibility_filter = options
|
||||
.visibility_filter
|
||||
.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 notes = select
|
||||
let note = select
|
||||
.filter(visibility_filter)
|
||||
.apply_if(id_filter, Select::<note::Entity>::filter)
|
||||
.apply_if(time_filter, Select::<note::Entity>::filter)
|
||||
.into_model::<NoteData>()
|
||||
.one(self.db.inner())
|
||||
.await?;
|
||||
|
||||
Ok(notes)
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub async fn get_many(
|
||||
|
@ -163,26 +158,36 @@ impl NoteResolver {
|
|||
let visibility_filter = options
|
||||
.visibility_filter
|
||||
.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 notes = select
|
||||
let notes_select = select
|
||||
.filter(visibility_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, _| {
|
||||
s.order_by_desc(Expr::col((
|
||||
note::Entity.base_prefix().into_iden().join_str(PIN),
|
||||
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)
|
||||
.into_model::<NoteData>()
|
||||
.all(self.db.inner())
|
||||
.await?;
|
||||
.await?
|
||||
};
|
||||
|
||||
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::SelectColumnsExt, CalckeyModel};
|
||||
use ck::{drive_file, user};
|
||||
use crate::model_ext::{
|
||||
AliasSourceExt, AliasSuffixExt, CursorPaginationExt, EntityPrefixExt, MagIden, ModelPagination,
|
||||
};
|
||||
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 sea_orm::sea_query::Alias;
|
||||
use sea_orm::{DbErr, FromQueryResult, Iden, JoinType, QueryResult};
|
||||
use magnetar_sdk::types::SpanFilter;
|
||||
use sea_orm::{
|
||||
ColumnTrait, DbErr, EntityTrait, FromQueryResult, Iden, JoinType, QueryFilter, QueryResult,
|
||||
QuerySelect,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserData {
|
||||
pub user: user::Model,
|
||||
pub profile: Option<user_profile::Model>,
|
||||
pub avatar: 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 {
|
||||
fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
|
||||
let prefix = if prefix.is_empty() {
|
||||
|
@ -30,6 +30,10 @@ impl FromQueryResult for UserData {
|
|||
|
||||
Ok(UserData {
|
||||
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(
|
||||
res,
|
||||
&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 {
|
||||
db: CalckeyModel,
|
||||
}
|
||||
|
@ -57,10 +130,23 @@ impl UserResolver {
|
|||
user_tbl: &MagIden,
|
||||
UserResolveOptions {
|
||||
with_avatar_and_banner,
|
||||
with_profile,
|
||||
}: &UserResolveOptions,
|
||||
) {
|
||||
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 {
|
||||
let avatar_tbl = user_tbl.join_str(AVATAR);
|
||||
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> {
|
||||
todo!()
|
||||
pub async fn get_follow_requests(
|
||||
&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)]
|
||||
#[ts(export)]
|
||||
pub struct Empty;
|
||||
pub struct Empty {}
|
||||
|
||||
impl From<Empty> for () {
|
||||
fn from(_: Empty) -> Self {}
|
||||
|
|
|
@ -88,7 +88,7 @@ pub struct GetUserByAcct;
|
|||
endpoint = "/users/:id/followers",
|
||||
method = Method::GET,
|
||||
request = "Empty",
|
||||
response = "Vec<PackUserBase>"
|
||||
response = "Vec<PackUserMaybeAll>"
|
||||
)]
|
||||
pub struct GetFollowersById;
|
||||
|
||||
|
@ -97,7 +97,7 @@ pub struct GetFollowersById;
|
|||
endpoint = "/users/:id/following",
|
||||
method = Method::GET,
|
||||
request = "Empty",
|
||||
response = "Vec<PackUserBase>"
|
||||
response = "Vec<PackUserMaybeAll>"
|
||||
)]
|
||||
pub struct GetFollowingById;
|
||||
|
||||
|
@ -106,7 +106,7 @@ pub struct GetFollowingById;
|
|||
endpoint = "/users/@self/followers",
|
||||
method = Method::GET,
|
||||
request = "Empty",
|
||||
response = "Vec<PackUserBase>"
|
||||
response = "Vec<PackUserMaybeAll>"
|
||||
)]
|
||||
pub struct GetFollowersSelf;
|
||||
|
||||
|
@ -115,7 +115,7 @@ pub struct GetFollowersSelf;
|
|||
endpoint = "/users/@self/following",
|
||||
method = Method::GET,
|
||||
request = "Empty",
|
||||
response = "Vec<PackUserBase>"
|
||||
response = "Vec<PackUserMaybeAll>"
|
||||
)]
|
||||
pub struct GetFollowingSelf;
|
||||
|
||||
|
@ -124,6 +124,6 @@ pub struct GetFollowingSelf;
|
|||
endpoint = "/users/@self/follow-requests",
|
||||
method = Method::GET,
|
||||
request = "Empty",
|
||||
response = "Vec<PackUserBase>"
|
||||
response = "Vec<PackUserMaybeAll>"
|
||||
)]
|
||||
pub struct GetFollowRequestsSelf;
|
||||
|
|
|
@ -5,8 +5,11 @@ pub mod note;
|
|||
pub mod timeline;
|
||||
pub mod user;
|
||||
|
||||
use crate::endpoints::ErrorKind;
|
||||
use crate::util_types::U64Range;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
|
@ -14,22 +17,22 @@ use ts_rs::TS;
|
|||
pub struct RangeFilter {
|
||||
pub time_start: DateTime<Utc>,
|
||||
pub time_end: DateTime<Utc>,
|
||||
pub id_start: DateTime<Utc>,
|
||||
pub id_end: DateTime<Utc>,
|
||||
pub id_start: String,
|
||||
pub id_end: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct StartFilter {
|
||||
pub time_start: DateTime<Utc>,
|
||||
pub id_start: DateTime<Utc>,
|
||||
pub id_start: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct EndFilter {
|
||||
pub time_end: DateTime<Utc>,
|
||||
pub id_end: DateTime<Utc>,
|
||||
pub id_end: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
|
@ -46,6 +49,119 @@ pub enum SpanFilter {
|
|||
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)]
|
||||
#[ts(export)]
|
||||
#[repr(transparent)]
|
||||
|
|
|
@ -3,7 +3,9 @@ mod user;
|
|||
|
||||
use crate::api_v1::note::handle_note;
|
||||
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::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/lookup-many", get(handle_user_by_id_many))
|
||||
.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))
|
||||
.layer(from_fn_with_state(
|
||||
AuthState::new(service.clone()),
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use crate::model::processing::user::{UserBorrowedData, UserModel};
|
||||
use crate::model::processing::PackError;
|
||||
use crate::model::processing::user::{UserBorrowedData, UserModel, UserShapedData};
|
||||
use crate::model::PackingContext;
|
||||
use crate::service::MagnetarService;
|
||||
use crate::web::auth::{AuthenticatedUser, MaybeUser};
|
||||
use crate::web::pagination::Pagination;
|
||||
use crate::web::{ApiError, ArgumentOutOfRange, ObjectNotFound};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::Json;
|
||||
use futures::StreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use magnetar_common::util::lenient_parse_tag_decode;
|
||||
use magnetar_sdk::endpoints::user::{
|
||||
|
@ -29,6 +27,7 @@ pub async fn handle_user_info_self(
|
|||
&ctx,
|
||||
&UserBorrowedData {
|
||||
user: user.as_ref(),
|
||||
profile: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
},
|
||||
|
@ -56,6 +55,7 @@ pub async fn handle_user_info(
|
|||
&ctx,
|
||||
&UserBorrowedData {
|
||||
user: &user_model,
|
||||
profile: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
},
|
||||
|
@ -88,6 +88,7 @@ pub async fn handle_user_info_by_acct(
|
|||
&ctx,
|
||||
&UserBorrowedData {
|
||||
user: &user_model,
|
||||
profile: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
},
|
||||
|
@ -118,20 +119,15 @@ pub async fn handle_user_by_id_many(
|
|||
.iter()
|
||||
.map(|user| UserBorrowedData {
|
||||
user,
|
||||
profile: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
})
|
||||
.map(|u| Box::new(u) as Box<dyn UserShapedData>)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let futures = user_data
|
||||
.iter()
|
||||
.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<_>>()
|
||||
let users_proc = user_model
|
||||
.pack_many_base(&ctx, &user_data.iter().map(Box::as_ref).collect::<Vec<_>>())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| (u.id.0.id.clone(), u))
|
||||
|
@ -144,3 +140,76 @@ pub async fn handle_user_by_id_many(
|
|||
|
||||
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::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::user_model::UserResolveOptions;
|
||||
use magnetar_calckey_model::{ck, CalckeyDbError};
|
||||
|
@ -581,6 +581,7 @@ impl NoteModel {
|
|||
limit: None,
|
||||
user_options: UserResolveOptions {
|
||||
with_avatar_and_banner: self.with_context,
|
||||
with_profile: false,
|
||||
},
|
||||
with_reply_target: self.with_context,
|
||||
with_renote_target: self.with_context,
|
||||
|
@ -619,6 +620,7 @@ impl NoteModel {
|
|||
limit: None,
|
||||
user_options: UserResolveOptions {
|
||||
with_avatar_and_banner: self.with_context,
|
||||
with_profile: false,
|
||||
},
|
||||
with_reply_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::{get_mm_token_emoji, PackError, PackResult};
|
||||
use crate::model::{PackType, PackingContext};
|
||||
use crate::web::pagination::Pagination;
|
||||
use crate::web::{AccessForbidden, ApiError};
|
||||
use either::Either;
|
||||
use futures_util::future::OptionFuture;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
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::mmm::Token;
|
||||
use magnetar_sdk::types::drive::PackDriveFileBase;
|
||||
|
@ -27,12 +31,14 @@ use url::Url;
|
|||
|
||||
pub trait UserShapedData<'a>: Send + Sync {
|
||||
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 banner(&self) -> Option<&'a ck::drive_file::Model>;
|
||||
}
|
||||
|
||||
pub struct UserBorrowedData<'a> {
|
||||
pub user: &'a ck::user::Model,
|
||||
pub profile: Option<&'a ck::user_profile::Model>,
|
||||
pub avatar: 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
|
||||
}
|
||||
|
||||
fn profile(&self) -> Option<&'a ck::user_profile::Model> {
|
||||
self.profile.as_ref()
|
||||
}
|
||||
|
||||
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
||||
self.avatar.as_ref()
|
||||
}
|
||||
|
@ -56,6 +66,10 @@ impl<'a> UserShapedData<'a> for UserBorrowedData<'a> {
|
|||
self.user
|
||||
}
|
||||
|
||||
fn profile(&self) -> Option<&'a ck::user_profile::Model> {
|
||||
self.profile
|
||||
}
|
||||
|
||||
fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
|
||||
self.avatar
|
||||
}
|
||||
|
@ -179,6 +193,18 @@ impl UserModel {
|
|||
.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>(
|
||||
&self,
|
||||
ctx: &PackingContext,
|
||||
|
@ -295,14 +321,15 @@ impl UserModel {
|
|||
) -> PackResult<PackUserSelfMaybeAll> {
|
||||
let user = user_data.user();
|
||||
|
||||
let should_fetch_profile =
|
||||
req.profile.unwrap_or_default() || req.secrets.unwrap_or_default();
|
||||
let should_fetch_profile = user_data.profile().is_none()
|
||||
&& (req.profile.unwrap_or_default() || req.secrets.unwrap_or_default());
|
||||
let profile_raw_promise =
|
||||
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
|
||||
let (base_res, profile_res) =
|
||||
join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
|
||||
let mut base = base_res?;
|
||||
let profile_raw = profile_res.transpose()?;
|
||||
let profile_ref = user_data.profile().or(profile_raw.as_ref());
|
||||
|
||||
let detail = req
|
||||
.detail
|
||||
|
@ -313,7 +340,7 @@ impl UserModel {
|
|||
self.profile_from_base(
|
||||
ctx,
|
||||
user_data,
|
||||
profile_raw.as_ref().unwrap(),
|
||||
profile_ref.unwrap(),
|
||||
None,
|
||||
&mut base.user.0.emojis,
|
||||
)
|
||||
|
@ -332,7 +359,7 @@ impl UserModel {
|
|||
let secrets = OptionFuture::from(
|
||||
req.secrets
|
||||
.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);
|
||||
|
@ -486,4 +513,205 @@ impl UserModel {
|
|||
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 std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use serde_json::Value;
|
||||
pub fn serialize_as_urlenc(value: &Value) -> String {
|
||||
match value {
|
||||
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)]
|
||||
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))),
|
||||
pairs.join("&")
|
||||
}
|
||||
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> {
|
||||
let mut cache = self.inner.lock().await;
|
||||
cache.cache_set((), val)
|
||||
pairs.join("&")
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
pub mod auth;
|
||||
pub mod pagination;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[repr(transparent)]
|
||||
|
@ -42,6 +43,15 @@ pub struct ApiError {
|
|||
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 {
|
||||
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 {
|
||||
fn from(err: FediverseTagParseError) -> 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