Full user packing in the backend and Magnetar-filled preview cards in the frontend
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2023-11-09 21:35:55 +01:00
parent d1ca62a807
commit 81d0c678d8
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
22 changed files with 655 additions and 186 deletions

View File

@ -114,6 +114,26 @@ impl CalckeyModel {
.await?) .await?)
} }
pub async fn get_user_profile_by_id(
&self,
id: &str,
) -> Result<Option<user_profile::Model>, CalckeyDbError> {
Ok(user_profile::Entity::find()
.filter(user_profile::Column::UserId.eq(id))
.one(&self.0)
.await?)
}
pub async fn get_user_security_keys_by_id(
&self,
id: &str,
) -> Result<Vec<user_security_key::Model>, CalckeyDbError> {
Ok(user_security_key::Entity::find()
.filter(user_security_key::Column::UserId.eq(id))
.all(&self.0)
.await?)
}
pub async fn get_many_users_by_id( pub async fn get_many_users_by_id(
&self, &self,
id: &[String], id: &[String],
@ -160,6 +180,21 @@ impl CalckeyModel {
.await?) .await?)
} }
pub async fn get_follow_request_status(
&self,
from: &str,
to: &str,
) -> Result<Option<follow_request::Model>, CalckeyDbError> {
Ok(follow_request::Entity::find()
.filter(
follow_request::Column::FollowerId
.eq(from)
.and(follow_request::Column::FolloweeId.eq(to)),
)
.one(&self.0)
.await?)
}
pub async fn get_block_status( pub async fn get_block_status(
&self, &self,
from: &str, from: &str,

View File

@ -1,12 +1,14 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use sea_orm::sea_query::{Alias, Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr}; use sea_orm::sea_query::{
Alias, Asterisk, Expr, IntoCondition, IntoIden, Query, SelectExpr, SimpleExpr,
};
use sea_orm::{ use sea_orm::{
ColumnTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, JoinType, ColumnTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, JoinType,
QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ck::{drive_file, note, note_reaction, user}; use ck::{drive_file, note, note_reaction, user, user_note_pining};
use magnetar_sdk::types::RangeFilter; use magnetar_sdk::types::RangeFilter;
use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel}; use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel};
@ -53,6 +55,7 @@ pub struct NoteData {
pub renote: Option<Box<NoteData>>, pub renote: Option<Box<NoteData>>,
} }
const PIN: &str = "pin.";
const INTERACTION_REACTION: &str = "interaction.reaction."; const INTERACTION_REACTION: &str = "interaction.reaction.";
const INTERACTION_RENOTE: &str = "interaction.renote."; const INTERACTION_RENOTE: &str = "interaction.renote.";
const USER: &str = "user."; const USER: &str = "user.";
@ -154,6 +157,7 @@ pub struct NoteResolveOptions {
pub with_reply_target: bool, pub with_reply_target: bool,
pub with_renote_target: bool, pub with_renote_target: bool,
pub with_interactions_from: Option<String>, // User ID pub with_interactions_from: Option<String>, // User ID
pub only_pins_from: Option<String>, // User ID
} }
trait SelectColumnsExt { trait SelectColumnsExt {
@ -262,6 +266,7 @@ impl SelectColumnsExt for Select<note::Entity> {
} }
lazy_static! { lazy_static! {
static ref ALIAS_PIN: Alias = Alias::new(PIN);
static ref ALIAS_INTERACTION_RENOTE: Alias = Alias::new(INTERACTION_RENOTE); static ref ALIAS_INTERACTION_RENOTE: Alias = Alias::new(INTERACTION_RENOTE);
static ref ALIAS_INTERACTION_REACTION: Alias = Alias::new(INTERACTION_REACTION); static ref ALIAS_INTERACTION_REACTION: Alias = Alias::new(INTERACTION_REACTION);
static ref ALIAS_USER: Alias = Alias::new(USER); static ref ALIAS_USER: Alias = Alias::new(USER);
@ -281,6 +286,24 @@ lazy_static! {
static ref ALIAS_RENOTE_USER_BANNER: Alias = Alias::new(RENOTE_USER_BANNER); static ref ALIAS_RENOTE_USER_BANNER: Alias = Alias::new(RENOTE_USER_BANNER);
} }
fn range_into_expr(filter: &RangeFilter) -> SimpleExpr {
match filter {
RangeFilter::TimeStart(start) => note::Column::CreatedAt.gte(*start),
RangeFilter::TimeRange(range) => {
note::Column::CreatedAt.between(*range.start(), *range.end())
}
RangeFilter::TimeEnd(end) => note::Column::CreatedAt.lt(*end),
}
}
fn ids_into_expr(ids: &Vec<String>) -> SimpleExpr {
if ids.len() == 1 {
note::Column::Id.eq(&ids[0])
} else {
note::Column::Id.is_in(ids)
}
}
impl NoteResolver { impl NoteResolver {
pub fn new(db: CalckeyModel) -> Self { pub fn new(db: CalckeyModel) -> Self {
NoteResolver { db } NoteResolver { db }
@ -292,21 +315,8 @@ impl NoteResolver {
) -> Result<Option<NoteData>, CalckeyDbError> { ) -> Result<Option<NoteData>, CalckeyDbError> {
let select = self.resolve(options); let select = self.resolve(options);
let visibility_filter = options.visibility_filter.with_note_and_user_tables(None); let visibility_filter = options.visibility_filter.with_note_and_user_tables(None);
let time_filter = options.time_range.as_ref().map(|f| match f { let time_filter = options.time_range.as_ref().map(range_into_expr);
RangeFilter::TimeStart(start) => note::Column::CreatedAt.gte(*start), let id_filter = options.ids.as_ref().map(ids_into_expr);
RangeFilter::TimeRange(range) => {
note::Column::CreatedAt.between(*range.start(), *range.end())
}
RangeFilter::TimeEnd(end) => note::Column::CreatedAt.lt(*end),
});
let id_filter = options.ids.as_ref().map(|ids| {
if ids.len() == 1 {
note::Column::Id.eq(&ids[0])
} else {
note::Column::Id.is_in(ids)
}
});
let notes = select let notes = select
.filter(visibility_filter) .filter(visibility_filter)
@ -319,9 +329,49 @@ impl NoteResolver {
Ok(notes) Ok(notes)
} }
pub async fn get_many(
&self,
options: &NoteResolveOptions,
) -> Result<Vec<NoteData>, CalckeyDbError> {
let select = self.resolve(options);
let visibility_filter = options.visibility_filter.with_note_and_user_tables(None);
let time_filter = options.time_range.as_ref().map(range_into_expr);
let id_filter = options.ids.as_ref().map(ids_into_expr);
let notes = 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((
ALIAS_PIN.clone(),
user_note_pining::Column::CreatedAt,
)))
})
.into_model::<NoteData>()
.all(self.db.inner())
.await?;
Ok(notes)
}
pub fn resolve(&self, options: &NoteResolveOptions) -> Select<note::Entity> { pub fn resolve(&self, options: &NoteResolveOptions) -> Select<note::Entity> {
let mut select = note::Entity::find().add_aliased_columns(Some(USER), user::Entity); let mut select = note::Entity::find().add_aliased_columns(Some(USER), user::Entity);
if let Some(pins_user) = options.only_pins_from.clone() {
select = select.join_as(
JoinType::InnerJoin,
note::Relation::UserNotePining
.def()
.on_condition(move |left, _right| {
Expr::col((left, note::Column::UserId))
.eq(&pins_user)
.into_condition()
}),
ALIAS_PIN.clone(),
)
}
if let Some(user_id) = &options.with_interactions_from { if let Some(user_id) = &options.with_interactions_from {
select = select select = select
.add_sub_select_reaction(None, INTERACTION_REACTION, user_id) .add_sub_select_reaction(None, INTERACTION_REACTION, user_id)

View File

@ -1,7 +1,6 @@
<template> <template>
<MkA <MkA
v-if="url.startsWith('/')" v-user-preview="{ username, host }"
v-user-preview="canonical"
class="mention" class="mention"
:class="{ isMe }" :class="{ isMe }"
:to="url" :to="url"
@ -17,28 +16,12 @@
> >
</span> </span>
</MkA> </MkA>
<a
v-else
class="mention"
:href="url"
target="_blank"
rel="noopener"
:style="{ background: bgCss }"
@click.stop
>
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host">@{{ toUnicode(host) }}</span>
</span>
</a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toUnicode } from "punycode"; import { toUnicode } from "punycode";
import {} from "vue";
import { host as localHost } from "@/config"; import { host as localHost } from "@/config";
import { $i } from "@/account"; import { $i } from "@/account";
const props = defineProps<{ const props = defineProps<{
username: string; username: string;
host: string; host: string;

View File

@ -19,25 +19,23 @@
} }
" "
> >
<div v-if="user != null" class="info"> <div v-if="user" class="info">
<div <div
class="banner" class="banner"
:style=" :style="
user.bannerUrl user.banner_url
? `background-image: url(${user.bannerUrl})` ? `background-image: url(${user.banner_url})`
: '' : ''
" "
> >
<span <span
v-if="$i && $i.id != user.id && user.isFollowed" v-if="$i && $i.id != user.id && user.follows_you"
class="followed" class="followed"
>{{ i18n.ts.followsYou }}</span >{{ i18n.ts.followsYou }}</span
> >
<span <span
v-if=" v-if="
$i && $i && $i.id != user.id && user.they_request_follow
$i.id != user.id &&
user.hasPendingFollowRequestToYou
" "
class="followed" class="followed"
> >
@ -100,15 +98,15 @@
<div class="status"> <div class="status">
<div> <div>
<p>{{ i18n.ts.notes }}</p> <p>{{ i18n.ts.notes }}</p>
<span>{{ user.notesCount }}</span> <span>{{ user.note_count }}</span>
</div> </div>
<div> <div>
<p>{{ i18n.ts.following }}</p> <p>{{ i18n.ts.following }}</p>
<span>{{ user.followingCount }}</span> <span>{{ user.following_count }}</span>
</div> </div>
<div> <div>
<p>{{ i18n.ts.followers }}</p> <p>{{ i18n.ts.followers }}</p>
<span>{{ user.followersCount }}</span> <span>{{ user.follower_count }}</span>
</div> </div>
</div> </div>
<div class="follow-button-container"> <div class="follow-button-container">
@ -127,19 +125,20 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { computed, onMounted } from "vue";
import * as Acct from "calckey-js/built/acct";
import type * as misskey from "calckey-js";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import XShowMoreButton from "@/components/MkShowMoreButton.vue"; import XShowMoreButton from "@/components/MkShowMoreButton.vue";
import * as os from "@/os"; import * as os from "@/os";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed, endpoints } from "magnetar-common";
import { host as localHost } from "@/config";
import { toUnicode } from "punycode";
const props = defineProps<{ const props = defineProps<{
showing: boolean; showing: boolean;
q: string; userTag: string | { username: string; host?: string };
source: HTMLElement; source: HTMLElement;
}>(); }>();
@ -150,40 +149,51 @@ const emit = defineEmits<{
}>(); }>();
const zIndex = os.claimZIndex("middle"); const zIndex = os.claimZIndex("middle");
let user = $ref<misskey.entities.UserDetailed | null>(null); let user = $ref<packed.PackUserMaybeAll | null>(null);
let top = $ref(0); let top = $ref(0);
let left = $ref(0); let left = $ref(0);
let isLong = $ref(false); let isLong = computed(() => {
if (!user?.description) return;
return (
user.description.split("\n").length > 9 || user.description.length > 400
);
});
let collapsed = $ref(!isLong); let collapsed = $ref(!isLong);
onMounted(() => { onMounted(() => {
if (typeof props.q === "object") { const options = { detail: true, profile: true, relation: true };
user = props.q;
isLong =
user.description.split("\n").length > 9 ||
user.description.length > 400;
} else {
const query = props.q.startsWith("@")
? Acct.parse(props.q.substr(1))
: { userId: props.q };
os.api("users/show", query).then((res) => { debugger;
if (!props.showing) return; if (typeof props.userTag === "object") {
user = res; const canonical =
isLong = !props.userTag.host || props.userTag.host === localHost
user.description.split("\n").length > 9 || ? `${props.userTag.username}`
user.description.length > 400; : `${props.userTag.username}@${toUnicode(props.userTag.host)}`;
os.magApi(endpoints.GetUserByAcct, options, {
user_acct: canonical,
}).then((u) => {
user = u;
}); });
} else {
const acctQuery = props.userTag.startsWith("@");
const apiCall = acctQuery
? os.magApi(endpoints.GetUserByAcct, options, {
user_acct: props.userTag,
})
: os.magApi(endpoints.GetUserById, options, {
user_id: props.userTag,
});
apiCall.then((u) => (user = u));
} }
const rect = props.source.getBoundingClientRect(); const rect = props.source.getBoundingClientRect();
const x = left = rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.scrollX;
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset; top = rect.top + props.source.offsetHeight + window.scrollY;
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
top = y;
left = x;
}); });
</script> </script>

View File

@ -5,12 +5,12 @@
style="margin-right: 8px" style="margin-right: 8px"
/> />
{{ i18n.ts.accountMoved }} {{ i18n.ts.accountMoved }}
<MkMention class="link" :username="acct" :host="host" /> <MagMention class="link" :username="acct" :host="host" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MkMention from "./MkMention.vue"; import MagMention from "./MagMention.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
defineProps<{ defineProps<{

View File

@ -176,9 +176,7 @@
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small"> <I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name> <template #name>
<MkA <MkA
v-user-preview=" v-user-preview="note.user.id"
magTransMap(note, 'user', 'userId', (u) => u.id)
"
class="name" class="name"
:to="userPage(note.user)" :to="userPage(note.user)"
> >

View File

@ -3,7 +3,7 @@ import * as mfm from "mfm-js";
import type { VNode } from "vue"; import type { VNode } from "vue";
import MkUrl from "@/components/global/MkUrl.vue"; import MkUrl from "@/components/global/MkUrl.vue";
import MkLink from "@/components/MkLink.vue"; import MkLink from "@/components/MkLink.vue";
import MkMention from "@/components/MkMention.vue"; import MagMention from "@/components/MagMention.vue";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
import MkFormula from "@/components/MkFormula.vue"; import MkFormula from "@/components/MkFormula.vue";
import MkCode from "@/components/MkCode.vue"; import MkCode from "@/components/MkCode.vue";
@ -476,15 +476,15 @@ export default defineComponent({
case "mention": { case "mention": {
return [ return [
h(MkMention, { h(MagMention, {
key: Math.random(), key: Math.random(),
username: token.props.username,
host: host:
(token.props.host == null && (token.props.host == null &&
this.author && this.author &&
this.author.host != null this.author.host != null
? this.author.host ? this.author.host
: token.props.host) || host, : token.props.host) || host,
username: token.props.username,
}), }),
]; ];
} }

View File

@ -9,7 +9,6 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from "vue"; import { computed, defineComponent, PropType } from "vue";
import MkButton from "../MkButton.vue"; import MkButton from "../MkButton.vue";
import * as os from "@/os";
import { CounterVarBlock } from "@/scripts/hpml/block"; import { CounterVarBlock } from "@/scripts/hpml/block";
import { Hpml } from "@/scripts/hpml/evaluator"; import { Hpml } from "@/scripts/hpml/evaluator";

View File

@ -4,7 +4,7 @@ import { popup } from "@/os";
export class UserPreview { export class UserPreview {
private el; private el;
private user; private user: string | { username: string; host?: string };
private showTimer; private showTimer;
private hideTimer; private hideTimer;
private checkTimer; private checkTimer;
@ -26,11 +26,11 @@ export class UserPreview {
popup( popup(
defineAsyncComponent( defineAsyncComponent(
() => import("@/components/MkUserPreview.vue") () => import("@/components/MagUserPreview.vue")
), ),
{ {
showing, showing,
q: this.user, userTag: this.user,
source: this.el, source: this.el,
}, },
{ {

View File

@ -127,7 +127,7 @@ export function magIsRenote(
note: packed.PackNoteMaybeFull | Misskey.entities.Note note: packed.PackNoteMaybeFull | Misskey.entities.Note
): boolean { ): boolean {
return ( return (
magTransProperty(note, "renoted_note", "renote") != null && (magTransProperty(note, "renoted_note", "renote") || null) !== null &&
(note.text || null) === null && (note.text || null) === null &&
magTransProperty(note, "file_ids", "fileIds").length === 0 && magTransProperty(note, "file_ids", "fileIds").length === 0 &&
(note.poll || null) === null (note.poll || null) === null

View File

@ -3,8 +3,8 @@ import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq"; import type { UserByIdReq } from "../UserByIdReq";
export const GetUserByAcct = { export const GetUserByAcct = {
endpoint: "/users/by-acct/:user_id", endpoint: "/users/by-acct/:user_acct",
pathParams: ["user_id"] as ["user_id"], pathParams: ["user_acct"] as ["user_acct"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as UserByIdReq, request: undefined as unknown as UserByIdReq,
response: undefined as unknown as PackUserMaybeAll response: undefined as unknown as PackUserMaybeAll

View File

@ -76,7 +76,7 @@ pub struct GetManyUsersById;
#[derive(Endpoint)] #[derive(Endpoint)]
#[endpoint( #[endpoint(
endpoint = "/users/by-acct/:user_id", endpoint = "/users/by-acct/:user_acct",
method = Method::GET, method = Method::GET,
request = "UserByIdReq", request = "UserByIdReq",
response = "PackUserMaybeAll" response = "PackUserMaybeAll"

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use ts_rs::TS; use ts_rs::TS;
use crate::types::Id; use crate::types::Id;
@ -21,3 +22,19 @@ pack!(PackEmojiBase, Required<Id> as id & Required<EmojiBase> as emoji);
#[ts(export)] #[ts(export)]
#[repr(transparent)] #[repr(transparent)]
pub struct EmojiContext(pub Vec<PackEmojiBase>); pub struct EmojiContext(pub Vec<PackEmojiBase>);
impl EmojiContext {
pub fn extend_from(&mut self, more: &[PackEmojiBase]) {
let existing = self
.0
.iter()
.map(|e| e.emoji.0.shortcode.clone())
.collect::<HashSet<_>>();
self.0.extend(
more.iter()
.filter(|&e| existing.contains(&e.emoji.0.shortcode))
.cloned(),
);
}
}

View File

@ -28,10 +28,10 @@ pub enum SpeechTransform {
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
pub struct ProfileField { pub struct ProfileField {
name: String, pub name: String,
value: String, pub value: String,
value_mm: Option<MmXml>, pub value_mm: Option<MmXml>,
verified_at: Option<DateTime<Utc>>, pub verified_at: Option<DateTime<Utc>>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]

View File

@ -11,8 +11,6 @@ use crate::web::auth::AuthState;
use axum::middleware::from_fn_with_state; use axum::middleware::from_fn_with_state;
use axum::routing::get; use axum::routing::get;
use axum::Router; use axum::Router;
use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer};
use std::sync::Arc; use std::sync::Arc;
pub fn create_api_router(service: Arc<MagnetarService>) -> Router { pub fn create_api_router(service: Arc<MagnetarService>) -> Router {

View File

@ -25,7 +25,7 @@ pub async fn handle_note(
attachments: attachments.unwrap_or_default(), attachments: attachments.unwrap_or_default(),
with_context: context.unwrap_or_default(), with_context: context.unwrap_or_default(),
} }
.fetch_single(&ctx, self_user.as_deref(), &id) .fetch_single(&ctx, &id)
.await? .await?
.ok_or(ObjectNotFound(id))?; .ok_or(ObjectNotFound(id))?;

View File

@ -9,46 +9,32 @@ use axum::Json;
use futures::StreamExt; use futures::StreamExt;
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use itertools::Itertools; use itertools::Itertools;
use magnetar_common::util::lenient_parse_tag; use magnetar_common::util::lenient_parse_tag_decode;
use magnetar_sdk::endpoints::user::{ use magnetar_sdk::endpoints::user::{
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, UserByIdReq, GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq,
UserSelfReq,
}; };
use magnetar_sdk::endpoints::{Req, Res}; use magnetar_sdk::endpoints::{Req, Res};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub async fn handle_user_info_self( pub async fn handle_user_info_self(
Query(UserSelfReq { Query(req): Query<Req<GetUserSelf>>,
detail: _,
pins: _,
profile: _,
secrets: _,
}): Query<Req<GetUserSelf>>,
State(service): State<Arc<MagnetarService>>, State(service): State<Arc<MagnetarService>>,
AuthenticatedUser(user): AuthenticatedUser, AuthenticatedUser(user): AuthenticatedUser,
) -> Result<Json<Res<GetUserSelf>>, ApiError> { ) -> Result<Json<Res<GetUserSelf>>, ApiError> {
// TODO: Extended properties!
let ctx = PackingContext::new(service, Some(user.clone())).await?; let ctx = PackingContext::new(service, Some(user.clone())).await?;
let user = UserModel.base_from_existing(&ctx, user.as_ref()).await?; let user = UserModel
Ok(Json(user.into())) .self_full_from_base(&ctx, user.as_ref(), &req, None, None)
.await?;
Ok(Json(user))
} }
pub async fn handle_user_info( pub async fn handle_user_info(
Path(id): Path<String>, Path(id): Path<String>,
Query(UserByIdReq { Query(req): Query<Req<GetUserById>>,
detail: _,
pins: _,
profile: _,
relation: _,
auth: _,
}): Query<Req<GetUserById>>,
State(service): State<Arc<MagnetarService>>, State(service): State<Arc<MagnetarService>>,
MaybeUser(self_user): MaybeUser, MaybeUser(self_user): MaybeUser,
) -> Result<Json<Res<GetUserById>>, ApiError> { ) -> Result<Json<Res<GetUserById>>, ApiError> {
// TODO: Extended properties!
let ctx = PackingContext::new(service.clone(), self_user).await?; let ctx = PackingContext::new(service.clone(), self_user).await?;
let user_model = service let user_model = service
.db .db
@ -56,25 +42,19 @@ pub async fn handle_user_info(
.await? .await?
.ok_or(ObjectNotFound(id))?; .ok_or(ObjectNotFound(id))?;
let user = UserModel.base_from_existing(&ctx, &user_model).await?; let user = UserModel
Ok(Json(user.into())) .foreign_full_from_base(&ctx, &user_model, &req, None, None)
.await?;
Ok(Json(user))
} }
pub async fn handle_user_info_by_acct( pub async fn handle_user_info_by_acct(
Path(tag_str): Path<String>, Path(tag_str): Path<String>,
Query(UserByIdReq { Query(req): Query<Req<GetUserByAcct>>,
detail: _,
pins: _,
profile: _,
relation: _,
auth: _,
}): Query<Req<GetUserByAcct>>,
State(service): State<Arc<MagnetarService>>, State(service): State<Arc<MagnetarService>>,
MaybeUser(self_user): MaybeUser, MaybeUser(self_user): MaybeUser,
) -> Result<Json<Res<GetUserByAcct>>, ApiError> { ) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
// TODO: Extended properties! let mut tag = lenient_parse_tag_decode(&tag_str)?;
let mut tag = lenient_parse_tag(&tag_str)?;
if matches!(&tag.host, Some(host) if host == &service.config.networking.host) { if matches!(&tag.host, Some(host) if host == &service.config.networking.host) {
tag.host = None; tag.host = None;
} }
@ -86,8 +66,10 @@ pub async fn handle_user_info_by_acct(
.await? .await?
.ok_or(ObjectNotFound(tag_str))?; .ok_or(ObjectNotFound(tag_str))?;
let user = UserModel.base_from_existing(&ctx, &user_model).await?; let user = UserModel
Ok(Json(user.into())) .foreign_full_from_base(&ctx, &user_model, &req, None, None)
.await?;
Ok(Json(user))
} }
pub async fn handle_user_by_id_many( pub async fn handle_user_by_id_many(
@ -109,7 +91,7 @@ pub async fn handle_user_by_id_many(
let futures = users let futures = users
.iter() .iter()
.map(|u| user_model.base_from_existing(&ctx, u)) .map(|u| user_model.base_from_existing(&ctx, u, None))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let users_proc = futures::stream::iter(futures) let users_proc = futures::stream::iter(futures)

View File

@ -6,7 +6,8 @@ use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::note::PackNoteMaybeFull; use magnetar_sdk::types::note::PackNoteMaybeFull;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform, AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserRelationExt, UserSecretsExt, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx,
UserRelationExt, UserSecretsExt,
}; };
use magnetar_sdk::types::MmXml; use magnetar_sdk::types::MmXml;
use url::Url; use url::Url;
@ -76,8 +77,7 @@ pub struct UserProfileExtSource<'a> {
pub user: &'a ck::user::Model, pub user: &'a ck::user::Model,
pub profile: &'a ck::user_profile::Model, pub profile: &'a ck::user_profile::Model,
pub profile_fields: &'a Vec<ProfileField>, pub profile_fields: &'a Vec<ProfileField>,
pub banner_url: Option<&'a Url>, pub banner: Option<&'a PackDriveFileBase>,
pub banner: Option<&'a ck::drive_file::Model>,
pub description_mm: Option<&'a MmXml>, pub description_mm: Option<&'a MmXml>,
pub relation: Option<&'a UserRelationExt>, pub relation: Option<&'a UserRelationExt>,
} }
@ -89,7 +89,6 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
user, user,
profile, profile,
profile_fields, profile_fields,
banner_url,
banner, banner,
description_mm, description_mm,
relation, relation,
@ -97,7 +96,9 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
) -> Self { ) -> Self {
let follow_visibility = match profile.ff_visibility { let follow_visibility = match profile.ff_visibility {
UserProfileFfvisibilityEnum::Public => true, UserProfileFfvisibilityEnum::Public => true,
UserProfileFfvisibilityEnum::Followers => relation.is_some_and(|r| r.follows_you), UserProfileFfvisibilityEnum::Followers => {
relation.is_some_and(|r| r.follows_you) || context.is_self(user)
}
UserProfileFfvisibilityEnum::Private => false, UserProfileFfvisibilityEnum::Private => false,
} || context.is_self(user); } || context.is_self(user);
@ -119,8 +120,8 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
url: profile.url.clone(), url: profile.url.clone(),
moved_to_uri: user.moved_to_uri.clone(), moved_to_uri: user.moved_to_uri.clone(),
also_known_as: user.also_known_as.clone(), also_known_as: user.also_known_as.clone(),
banner_url: banner_url.map(Url::to_string), banner_url: banner.and_then(|b| b.file.0.url.to_owned()),
banner_blurhash: banner.and_then(|b| b.blurhash.clone()), banner_blurhash: banner.and_then(|b| b.file.0.blurhash.clone()),
has_public_reactions: profile.public_reactions, has_public_reactions: profile.public_reactions,
} }
} }
@ -238,3 +239,13 @@ impl PackType<UserSecretsExtSource<'_>> for UserSecretsExt {
} }
} }
} }
impl PackType<&ck::user_profile::Model> for UserAuthOverviewExt {
fn extract(_context: &PackingContext, data: &ck::user_profile::Model) -> Self {
UserAuthOverviewExt {
has_passwordless_login: data.use_password_less_login,
has_security_keys: data.security_keys_available,
has_two_factor_enabled: data.two_factor_enabled,
}
}
}

View File

@ -24,6 +24,7 @@ impl Default for ProcessingLimits {
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
enum UserRelationship { enum UserRelationship {
Follow, Follow,
FollowRequest,
Mute, Mute,
Block, Block,
RenoteMute, RenoteMute,
@ -120,6 +121,12 @@ impl PackingContext {
.get_follower_status(&link.from, &link.to) .get_follower_status(&link.from, &link.to)
.await? .await?
.is_some(), .is_some(),
UserRelationship::FollowRequest => self
.service
.db
.get_follow_request_status(&link.from, &link.to)
.await?
.is_some(),
UserRelationship::Mute => self UserRelationship::Mute => self
.service .service
.db .db

View File

@ -19,6 +19,8 @@ pub enum PackError {
#[error("Calckey database wrapper error: {0}")] #[error("Calckey database wrapper error: {0}")]
CalckeyDbError(#[from] CalckeyDbError), CalckeyDbError(#[from] CalckeyDbError),
#[error("Emoji cache error: {0}")] #[error("Emoji cache error: {0}")]
DataError(String),
#[error("Emoji cache error: {0}")]
EmojiCacheError(#[from] EmojiCacheError), EmojiCacheError(#[from] EmojiCacheError),
#[error("Instance cache error: {0}")] #[error("Instance cache error: {0}")]
InstanceMetaCacheError(#[from] InstanceMetaCacheError), InstanceMetaCacheError(#[from] InstanceMetaCacheError),

View File

@ -7,7 +7,7 @@ use crate::model::{PackType, PackingContext, UserRelationship};
use compact_str::CompactString; use compact_str::CompactString;
use either::Either; use either::Either;
use futures_util::future::try_join_all; use futures_util::future::try_join_all;
use futures_util::TryFutureExt; use futures_util::{StreamExt, TryFutureExt, TryStreamExt};
use magnetar_calckey_model::ck::sea_orm_active_enums::NoteVisibilityEnum; use magnetar_calckey_model::ck::sea_orm_active_enums::NoteVisibilityEnum;
use magnetar_calckey_model::emoji::EmojiTag; use magnetar_calckey_model::emoji::EmojiTag;
use magnetar_calckey_model::note_model::{ use magnetar_calckey_model::note_model::{
@ -197,7 +197,7 @@ impl NoteModel {
note_data: &NoteData, note_data: &NoteData,
) -> PackResult<PackNoteBase> { ) -> PackResult<PackNoteBase> {
let Required(ref user) = UserModel let Required(ref user) = UserModel
.base_from_existing(ctx, &note_data.user) .base_from_existing(ctx, &note_data.user, None)
.await? .await?
.user; .user;
@ -378,7 +378,6 @@ impl NoteModel {
async fn extract_poll( async fn extract_poll(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
as_user: Option<&ck::user::Model>,
note: &ck::note::Model, note: &ck::note::Model,
) -> PackResult<Option<PackPollBase>> { ) -> PackResult<Option<PackPollBase>> {
if !note.has_poll { if !note.has_poll {
@ -391,7 +390,7 @@ impl NoteModel {
return Ok(None); return Ok(None);
}; };
let votes = match as_user { let votes = match ctx.self_user.as_deref() {
Some(u) => Some(poll_resolver.get_poll_votes_by(&note.id, &u.id).await?), Some(u) => Some(poll_resolver.get_poll_votes_by(&note.id, &u.id).await?),
None => None, None => None,
}; };
@ -406,13 +405,12 @@ impl NoteModel {
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
drive_model: &DriveModel, drive_model: &DriveModel,
as_user: Option<&ck::user::Model>,
note_data: &NoteData, note_data: &NoteData,
) -> PackResult<PackNoteMaybeAttachments> { ) -> PackResult<PackNoteMaybeAttachments> {
let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!( let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!(
self.extract_base(ctx, note_data), self.extract_base(ctx, note_data),
self.extract_attachments(ctx, drive_model, &note_data.note), self.extract_attachments(ctx, drive_model, &note_data.note),
self.extract_poll(ctx, as_user, &note_data.note) self.extract_poll(ctx, &note_data.note)
)?; )?;
Ok(PackNoteMaybeAttachments::pack_from(( Ok(PackNoteMaybeAttachments::pack_from((
@ -431,40 +429,17 @@ impl NoteModel {
))) )))
} }
pub async fn fetch_single( async fn pack_full_single(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
as_user: Option<&ck::user::Model>, note: NoteData,
id: &str, ) -> PackResult<PackNoteMaybeFull> {
) -> PackResult<Option<PackNoteMaybeFull>> {
let note_resolver = ctx.service.db.get_note_resolver();
let Some(note) = note_resolver
.get_one(&NoteResolveOptions {
ids: Some(vec![id.to_owned()]),
visibility_filter: Box::new(
NoteVisibilityFilterModel
.new_note_visibility_filter(as_user.map(ck::user::Model::get_id)),
),
time_range: None,
with_user: self.with_context,
with_reply_target: self.with_context,
with_renote_target: self.with_context,
with_interactions_from: self
.with_context
.then(|| as_user.map(ck::user::Model::get_id).map(str::to_string))
.flatten(),
})
.await?
else {
return Ok(None);
};
let drive_model = DriveModel; let drive_model = DriveModel;
let reply_target = async { let reply_target = async {
match note.reply.as_ref() { match note.reply.as_ref() {
Some(r) if self.with_context => self Some(r) if self.with_context => self
.pack_single_attachments(ctx, &drive_model, as_user, r) .pack_single_attachments(ctx, &drive_model, r)
.await .await
.map(Some), .map(Some),
_ => Ok(None), _ => Ok(None),
@ -474,7 +449,7 @@ impl NoteModel {
let renote_target = async { let renote_target = async {
match note.renote.as_ref() { match note.renote.as_ref() {
Some(r) if self.with_context => self Some(r) if self.with_context => self
.pack_single_attachments(ctx, &drive_model, as_user, r) .pack_single_attachments(ctx, &drive_model, r)
.await .await
.map(Some), .map(Some),
_ => Ok(None), _ => Ok(None),
@ -491,7 +466,7 @@ impl NoteModel {
reply_target_pack, reply_target_pack,
renote_target_pack, renote_target_pack,
) = try_join!( ) = try_join!(
self.pack_single_attachments(ctx, &drive_model, as_user, &note), self.pack_single_attachments(ctx, &drive_model, &note),
reply_target, reply_target,
renote_target renote_target
)?; )?;
@ -506,12 +481,89 @@ impl NoteModel {
) )
}); });
Ok(Some(PackNoteMaybeFull::pack_from(( Ok(PackNoteMaybeFull::pack_from((
id, id,
note, note,
user_context, user_context,
attachment, attachment,
Optional(detail), Optional(detail),
)))) )))
}
pub async fn fetch_single(
&self,
ctx: &PackingContext,
id: &str,
) -> PackResult<Option<PackNoteMaybeFull>> {
let note_resolver = ctx.service.db.get_note_resolver();
let Some(note) = note_resolver
.get_one(&NoteResolveOptions {
ids: Some(vec![id.to_owned()]),
visibility_filter: Box::new(NoteVisibilityFilterModel.new_note_visibility_filter(
ctx.self_user.as_deref().map(ck::user::Model::get_id),
)),
time_range: None,
with_user: self.with_context,
with_reply_target: self.with_context,
with_renote_target: self.with_context,
with_interactions_from: self
.with_context
.then(|| {
ctx.self_user
.as_deref()
.map(ck::user::Model::get_id)
.map(str::to_string)
})
.flatten(),
only_pins_from: None,
})
.await?
else {
return Ok(None);
};
Ok(Some(self.pack_full_single(ctx, note).await?))
}
pub async fn fetch_pins(
&self,
ctx: &PackingContext,
pin_user: &ck::user::Model,
) -> PackResult<Vec<PackNoteMaybeFull>> {
let note_resolver = ctx.service.db.get_note_resolver();
let notes = note_resolver
.get_many(&NoteResolveOptions {
ids: None,
visibility_filter: Box::new(NoteVisibilityFilterModel.new_note_visibility_filter(
ctx.self_user.as_deref().map(ck::user::Model::get_id),
)),
time_range: None,
with_user: self.with_context,
with_reply_target: self.with_context,
with_renote_target: self.with_context,
with_interactions_from: self
.with_context
.then(|| {
ctx.self_user
.as_deref()
.map(ck::user::Model::get_id)
.map(str::to_string)
})
.flatten(),
only_pins_from: Some(pin_user.id.clone()),
})
.await?;
let fut_iter = notes
.into_iter()
.map(|note| self.pack_full_single(ctx, note));
let processed = futures::stream::iter(fut_iter)
.buffered(10)
.err_into::<PackError>()
.try_collect::<Vec<_>>()
.await?;
Ok(processed)
} }
} }

View File

@ -1,19 +1,35 @@
use crate::model::data::user::UserBaseSource; use crate::model::data::user::{UserBaseSource, UserProfileExtSource};
use crate::model::processing::drive::DriveModel; use crate::model::processing::drive::DriveModel;
use crate::model::processing::emoji::EmojiModel; use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::{get_mm_token_emoji, PackResult}; use crate::model::processing::note::NoteModel;
use crate::model::{PackType, PackingContext}; use crate::model::processing::{get_mm_token_emoji, PackError, PackResult};
use crate::model::{PackType, PackingContext, UserRelationship};
use either::Either;
use futures_util::future::OptionFuture;
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
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;
use magnetar_sdk::types::emoji::EmojiContext; use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::instance::InstanceTicker; use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{PackUserBase, UserBase}; use magnetar_sdk::types::user::{
PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll, ProfileField,
SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
UserProfilePinsEx, UserRelationExt, UserSecretsExt,
};
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Packed, Required}; use magnetar_sdk::{mmm, Optional, Packed, Required};
use serde::{Deserialize, Serialize};
use tokio::{join, try_join};
use tracing::warn; use tracing::warn;
use url::Url; use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileFieldRaw<'a> {
name: &'a str,
value: &'a str,
}
pub struct UserModel; pub struct UserModel;
impl UserModel { impl UserModel {
@ -21,6 +37,12 @@ impl UserModel {
mmm::Context::default().parse_ui(user.name.as_deref().unwrap_or(&user.username)) mmm::Context::default().parse_ui(user.name.as_deref().unwrap_or(&user.username))
} }
pub fn tokenize_description(&self, user: &ck::user_profile::Model) -> Option<Token> {
user.description
.as_deref()
.map(|d| mmm::Context::default().parse_inline(d))
}
pub fn get_effective_avatar_url( pub fn get_effective_avatar_url(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
@ -56,15 +78,17 @@ impl UserModel {
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user: &ck::user::Model,
hint_avatar_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserBase> { ) -> PackResult<PackUserBase> {
let drive_file_pack = DriveModel; let drive_file_pack = DriveModel;
let avatar = match &user.avatar_id { let avatar = match (hint_avatar_file, &user.avatar_id) {
Some(av_id) => drive_file_pack.get_cached_base(ctx, av_id).await?, (Some(avatar_file), _) => Some(drive_file_pack.pack_existing(ctx, avatar_file)),
None => None, (None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
_ => None,
}; };
let avatar_url = &self.get_effective_avatar_url(ctx, user, avatar.as_ref())?; let avatar_url = &self.get_effective_avatar_url(ctx, user, avatar.as_ref())?;
let username_mm = self.tokenize_username(&user); let username_mm = self.tokenize_username(user);
let emoji_model = EmojiModel; let emoji_model = EmojiModel;
let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&username_mm)); let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&username_mm));
@ -100,4 +124,305 @@ impl UserModel {
Required(base), Required(base),
))) )))
} }
async fn get_profile(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
) -> PackResult<ck::user_profile::Model> {
ctx.service
.db
.get_user_profile_by_id(&user.id)
.await?
.ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
}
pub async fn profile_from_base(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
profile: &ck::user_profile::Model,
relation: Option<&UserRelationExt>,
emoji_out: &mut EmojiContext,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<UserProfileExt> {
let drive_file_pack = DriveModel;
let banner = match (hint_banner_file, &user.banner_id) {
(Some(banner_file), _) => Some(drive_file_pack.pack_existing(ctx, banner_file)),
(None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
_ => None,
};
let description_mm = self.tokenize_description(&profile);
let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?;
let parser = mmm::Context::new(2);
let profile_fields = fields
.into_iter()
.map(|f| {
let tok = parser.parse_profile_fields(&f.value);
ProfileField {
name: f.name.to_string(),
value: f.value.to_string(),
value_mm: mmm::to_xml_string(&tok).map(MmXml).ok(),
verified_at: None,
}
})
.collect::<Vec<_>>();
if let Some(desc_mm) = &description_mm {
let emoji_model = EmojiModel;
let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&desc_mm));
let emojis = emoji_model
.fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
.await?;
emoji_out.extend_from(&emojis);
}
Ok(UserProfileExt::extract(
ctx,
UserProfileExtSource {
user,
profile: &profile,
profile_fields: &profile_fields,
banner: banner.as_ref(),
description_mm: description_mm
.and_then(|desc_mm| mmm::to_xml_string(&desc_mm).map(MmXml).ok())
.as_ref(),
relation,
},
))
}
pub async fn secrets_from_base(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
profile: &ck::user_profile::Model,
) -> PackResult<UserSecretsExt> {
let secrets = ctx
.service
.db
.get_user_security_keys_by_id(&user.id)
.await?
.into_iter()
.map(|k| {
PackSecurityKeyBase::pack_from((
Required(Id::from(&k.id)),
Required(SecurityKeyBase {
name: k.name,
last_used_at: Some(k.last_used.into()),
}),
))
})
.collect::<Vec<_>>();
Ok(UserSecretsExt::extract(ctx, (profile, &secrets)))
}
pub async fn self_full_from_base(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
req: &UserSelfReq,
hint_avatar_file: Option<&ck::drive_file::Model>,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserSelfMaybeAll> {
let should_fetch_profile =
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, hint_avatar_file),
profile_raw_promise
);
let mut base = base_res?;
let profile_raw = profile_res.transpose()?;
let detail = req
.detail
.unwrap_or_default()
.then(|| UserDetailExt::extract(ctx, user));
let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
self.profile_from_base(
ctx,
user,
profile_raw.as_ref().unwrap(),
None,
&mut base.user.0.emojis,
hint_banner_file,
)
}));
let note_model = NoteModel {
with_context: true,
attachments: true,
};
let pins = OptionFuture::from(
req.pins
.unwrap_or_default()
.then(|| note_model.fetch_pins(ctx, user)),
);
let secrets = OptionFuture::from(
req.secrets
.unwrap_or_default()
.then(|| self.secrets_from_base(ctx, user, profile_raw.as_ref().unwrap())),
);
let (profile_res, pins_res, secrets_res) = join!(profile, pins, secrets);
let profile_resolved = profile_res.transpose()?;
let pins_resolved = pins_res
.transpose()?
.map(|notes| UserProfilePinsEx::extract(ctx, &notes));
let secrets_resolved = secrets_res.transpose()?;
Ok(PackUserSelfMaybeAll {
id: base.id,
user: base.user,
profile: Optional(profile_resolved),
pins: Optional(pins_resolved),
detail: Optional(detail),
secrets: Optional(secrets_resolved),
})
}
pub async fn relations_from_base(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
) -> PackResult<UserRelationExt> {
let Some(me_user) = ctx.self_user.as_deref() else {
return Ok(UserRelationExt {
follows_you: false,
you_follow: false,
you_request_follow: false,
they_request_follow: false,
blocks_you: false,
you_block: false,
mute: false,
mute_renotes: false,
});
};
let me = Either::Right(me_user);
let them = Either::Right(user);
let (
follows_you,
you_follow,
you_request_follow,
they_request_follow,
blocks_you,
you_block,
mute,
mute_renotes,
) = try_join!(
ctx.is_relationship_between(them, me, UserRelationship::Follow),
ctx.is_relationship_between(me, them, UserRelationship::Follow),
ctx.is_relationship_between(them, me, UserRelationship::FollowRequest),
ctx.is_relationship_between(me, them, UserRelationship::FollowRequest),
ctx.is_relationship_between(them, me, UserRelationship::Block),
ctx.is_relationship_between(me, them, UserRelationship::Block),
ctx.is_relationship_between(me, them, UserRelationship::Mute),
ctx.is_relationship_between(me, them, UserRelationship::RenoteMute)
)?;
Ok(UserRelationExt {
follows_you,
you_follow,
you_request_follow,
they_request_follow,
blocks_you,
you_block,
mute,
mute_renotes,
})
}
pub fn auth_overview_from_profile(
&self,
ctx: &PackingContext,
profile: &ck::user_profile::Model,
) -> PackResult<UserAuthOverviewExt> {
Ok(UserAuthOverviewExt::extract(ctx, profile))
}
pub async fn foreign_full_from_base(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
req: &UserByIdReq,
hint_avatar_file: Option<&ck::drive_file::Model>,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserMaybeAll> {
let should_fetch_profile = req.profile.unwrap_or_default() || req.auth.unwrap_or_default();
let profile_raw_promise =
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
let (base, profile) = join!(
self.base_from_existing(ctx, user, hint_avatar_file),
profile_raw_promise
);
let mut base = base?;
let profile_raw = profile.transpose()?;
let detail = req
.detail
.unwrap_or_default()
.then(|| UserDetailExt::extract(ctx, user));
let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
self.profile_from_base(
ctx,
user,
profile_raw.as_ref().unwrap(),
None,
&mut base.user.0.emojis,
hint_banner_file,
)
}));
let note_model = NoteModel {
with_context: true,
attachments: true,
};
let pins = OptionFuture::from(
req.pins
.unwrap_or_default()
.then(|| note_model.fetch_pins(ctx, user)),
);
let relations = OptionFuture::from(
req.relation
.unwrap_or_default()
.then(|| self.relations_from_base(ctx, user)),
);
let auth = req
.auth
.unwrap_or_default()
.then(|| self.auth_overview_from_profile(ctx, profile_raw.as_ref().unwrap()))
.transpose()?;
let (profile_res, pins_res, relations_res) = join!(profile, pins, relations);
let profile_resolved = profile_res.transpose()?;
let pins_resolved = pins_res
.transpose()?
.map(|notes| UserProfilePinsEx::extract(ctx, &notes));
let relations_resolved = relations_res.transpose()?;
Ok(PackUserMaybeAll {
id: base.id,
user: base.user,
profile: Optional(profile_resolved),
pins: Optional(pins_resolved),
detail: Optional(detail),
relation: Optional(relations_resolved),
auth: Optional(auth),
})
}
} }