Full user packing in the backend and Magnetar-filled preview cards in the frontend
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
This commit is contained in:
parent
d1ca62a807
commit
81d0c678d8
|
@ -114,6 +114,26 @@ impl CalckeyModel {
|
|||
.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(
|
||||
&self,
|
||||
id: &[String],
|
||||
|
@ -160,6 +180,21 @@ impl CalckeyModel {
|
|||
.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(
|
||||
&self,
|
||||
from: &str,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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::{
|
||||
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 ck::{drive_file, note, note_reaction, user};
|
||||
use ck::{drive_file, note, note_reaction, user, user_note_pining};
|
||||
use magnetar_sdk::types::RangeFilter;
|
||||
|
||||
use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel};
|
||||
|
@ -53,6 +55,7 @@ pub struct NoteData {
|
|||
pub renote: Option<Box<NoteData>>,
|
||||
}
|
||||
|
||||
const PIN: &str = "pin.";
|
||||
const INTERACTION_REACTION: &str = "interaction.reaction.";
|
||||
const INTERACTION_RENOTE: &str = "interaction.renote.";
|
||||
const USER: &str = "user.";
|
||||
|
@ -154,6 +157,7 @@ pub struct NoteResolveOptions {
|
|||
pub with_reply_target: bool,
|
||||
pub with_renote_target: bool,
|
||||
pub with_interactions_from: Option<String>, // User ID
|
||||
pub only_pins_from: Option<String>, // User ID
|
||||
}
|
||||
|
||||
trait SelectColumnsExt {
|
||||
|
@ -262,6 +266,7 @@ impl SelectColumnsExt for Select<note::Entity> {
|
|||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ALIAS_PIN: Alias = Alias::new(PIN);
|
||||
static ref ALIAS_INTERACTION_RENOTE: Alias = Alias::new(INTERACTION_RENOTE);
|
||||
static ref ALIAS_INTERACTION_REACTION: Alias = Alias::new(INTERACTION_REACTION);
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn new(db: CalckeyModel) -> Self {
|
||||
NoteResolver { db }
|
||||
|
@ -292,21 +315,8 @@ impl NoteResolver {
|
|||
) -> Result<Option<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(|f| match f {
|
||||
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),
|
||||
});
|
||||
|
||||
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 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)
|
||||
|
@ -319,9 +329,49 @@ impl NoteResolver {
|
|||
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> {
|
||||
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 {
|
||||
select = select
|
||||
.add_sub_select_reaction(None, INTERACTION_REACTION, user_id)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<MkA
|
||||
v-if="url.startsWith('/')"
|
||||
v-user-preview="canonical"
|
||||
v-user-preview="{ username, host }"
|
||||
class="mention"
|
||||
:class="{ isMe }"
|
||||
:to="url"
|
||||
|
@ -17,28 +16,12 @@
|
|||
>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toUnicode } from "punycode";
|
||||
import {} from "vue";
|
||||
import { host as localHost } from "@/config";
|
||||
import { $i } from "@/account";
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
|
@ -19,25 +19,23 @@
|
|||
}
|
||||
"
|
||||
>
|
||||
<div v-if="user != null" class="info">
|
||||
<div v-if="user" class="info">
|
||||
<div
|
||||
class="banner"
|
||||
:style="
|
||||
user.bannerUrl
|
||||
? `background-image: url(${user.bannerUrl})`
|
||||
user.banner_url
|
||||
? `background-image: url(${user.banner_url})`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="$i && $i.id != user.id && user.isFollowed"
|
||||
v-if="$i && $i.id != user.id && user.follows_you"
|
||||
class="followed"
|
||||
>{{ i18n.ts.followsYou }}</span
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
$i &&
|
||||
$i.id != user.id &&
|
||||
user.hasPendingFollowRequestToYou
|
||||
$i && $i.id != user.id && user.they_request_follow
|
||||
"
|
||||
class="followed"
|
||||
>
|
||||
|
@ -100,15 +98,15 @@
|
|||
<div class="status">
|
||||
<div>
|
||||
<p>{{ i18n.ts.notes }}</p>
|
||||
<span>{{ user.notesCount }}</span>
|
||||
<span>{{ user.note_count }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.following }}</p>
|
||||
<span>{{ user.followingCount }}</span>
|
||||
<span>{{ user.following_count }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.followers }}</p>
|
||||
<span>{{ user.followersCount }}</span>
|
||||
<span>{{ user.follower_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="follow-button-container">
|
||||
|
@ -127,19 +125,20 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import * as Acct from "calckey-js/built/acct";
|
||||
import type * as misskey from "calckey-js";
|
||||
import { computed, onMounted } from "vue";
|
||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||
import { userPage } from "@/filters/user";
|
||||
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
|
||||
import * as os from "@/os";
|
||||
import { $i } from "@/account";
|
||||
import { i18n } from "@/i18n";
|
||||
import { packed, endpoints } from "magnetar-common";
|
||||
import { host as localHost } from "@/config";
|
||||
import { toUnicode } from "punycode";
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
q: string;
|
||||
userTag: string | { username: string; host?: string };
|
||||
source: HTMLElement;
|
||||
}>();
|
||||
|
||||
|
@ -150,40 +149,51 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
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 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);
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.q === "object") {
|
||||
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 };
|
||||
const options = { detail: true, profile: true, relation: true };
|
||||
|
||||
os.api("users/show", query).then((res) => {
|
||||
if (!props.showing) return;
|
||||
user = res;
|
||||
isLong =
|
||||
user.description.split("\n").length > 9 ||
|
||||
user.description.length > 400;
|
||||
debugger;
|
||||
if (typeof props.userTag === "object") {
|
||||
const canonical =
|
||||
!props.userTag.host || props.userTag.host === localHost
|
||||
? `${props.userTag.username}`
|
||||
: `${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 x =
|
||||
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset;
|
||||
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
|
||||
|
||||
top = y;
|
||||
left = x;
|
||||
left = rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.scrollX;
|
||||
top = rect.top + props.source.offsetHeight + window.scrollY;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -5,12 +5,12 @@
|
|||
style="margin-right: 8px"
|
||||
/>
|
||||
{{ i18n.ts.accountMoved }}
|
||||
<MkMention class="link" :username="acct" :host="host" />
|
||||
<MagMention class="link" :username="acct" :host="host" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkMention from "./MkMention.vue";
|
||||
import MagMention from "./MagMention.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
defineProps<{
|
||||
|
|
|
@ -176,9 +176,7 @@
|
|||
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
|
||||
<template #name>
|
||||
<MkA
|
||||
v-user-preview="
|
||||
magTransMap(note, 'user', 'userId', (u) => u.id)
|
||||
"
|
||||
v-user-preview="note.user.id"
|
||||
class="name"
|
||||
:to="userPage(note.user)"
|
||||
>
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as mfm from "mfm-js";
|
|||
import type { VNode } from "vue";
|
||||
import MkUrl from "@/components/global/MkUrl.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 MkFormula from "@/components/MkFormula.vue";
|
||||
import MkCode from "@/components/MkCode.vue";
|
||||
|
@ -476,15 +476,15 @@ export default defineComponent({
|
|||
|
||||
case "mention": {
|
||||
return [
|
||||
h(MkMention, {
|
||||
h(MagMention, {
|
||||
key: Math.random(),
|
||||
username: token.props.username,
|
||||
host:
|
||||
(token.props.host == null &&
|
||||
this.author &&
|
||||
this.author.host != null
|
||||
? this.author.host
|
||||
: token.props.host) || host,
|
||||
username: token.props.username,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from "vue";
|
||||
import MkButton from "../MkButton.vue";
|
||||
import * as os from "@/os";
|
||||
import { CounterVarBlock } from "@/scripts/hpml/block";
|
||||
import { Hpml } from "@/scripts/hpml/evaluator";
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { popup } from "@/os";
|
|||
|
||||
export class UserPreview {
|
||||
private el;
|
||||
private user;
|
||||
private user: string | { username: string; host?: string };
|
||||
private showTimer;
|
||||
private hideTimer;
|
||||
private checkTimer;
|
||||
|
@ -26,11 +26,11 @@ export class UserPreview {
|
|||
|
||||
popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkUserPreview.vue")
|
||||
() => import("@/components/MagUserPreview.vue")
|
||||
),
|
||||
{
|
||||
showing,
|
||||
q: this.user,
|
||||
userTag: this.user,
|
||||
source: this.el,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -127,7 +127,7 @@ export function magIsRenote(
|
|||
note: packed.PackNoteMaybeFull | Misskey.entities.Note
|
||||
): boolean {
|
||||
return (
|
||||
magTransProperty(note, "renoted_note", "renote") != null &&
|
||||
(magTransProperty(note, "renoted_note", "renote") || null) !== null &&
|
||||
(note.text || null) === null &&
|
||||
magTransProperty(note, "file_ids", "fileIds").length === 0 &&
|
||||
(note.poll || null) === null
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
|
|||
import type { UserByIdReq } from "../UserByIdReq";
|
||||
|
||||
export const GetUserByAcct = {
|
||||
endpoint: "/users/by-acct/:user_id",
|
||||
pathParams: ["user_id"] as ["user_id"],
|
||||
endpoint: "/users/by-acct/:user_acct",
|
||||
pathParams: ["user_acct"] as ["user_acct"],
|
||||
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
|
||||
request: undefined as unknown as UserByIdReq,
|
||||
response: undefined as unknown as PackUserMaybeAll
|
||||
|
|
|
@ -76,7 +76,7 @@ pub struct GetManyUsersById;
|
|||
|
||||
#[derive(Endpoint)]
|
||||
#[endpoint(
|
||||
endpoint = "/users/by-acct/:user_id",
|
||||
endpoint = "/users/by-acct/:user_acct",
|
||||
method = Method::GET,
|
||||
request = "UserByIdReq",
|
||||
response = "PackUserMaybeAll"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::types::Id;
|
||||
|
@ -21,3 +22,19 @@ pack!(PackEmojiBase, Required<Id> as id & Required<EmojiBase> as emoji);
|
|||
#[ts(export)]
|
||||
#[repr(transparent)]
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,10 @@ pub enum SpeechTransform {
|
|||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ProfileField {
|
||||
name: String,
|
||||
value: String,
|
||||
value_mm: Option<MmXml>,
|
||||
verified_at: Option<DateTime<Utc>>,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub value_mm: Option<MmXml>,
|
||||
pub verified_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
|
|
|
@ -11,8 +11,6 @@ use crate::web::auth::AuthState;
|
|||
use axum::middleware::from_fn_with_state;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use serde::de::{DeserializeOwned, Error};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn create_api_router(service: Arc<MagnetarService>) -> Router {
|
||||
|
|
|
@ -25,7 +25,7 @@ pub async fn handle_note(
|
|||
attachments: attachments.unwrap_or_default(),
|
||||
with_context: context.unwrap_or_default(),
|
||||
}
|
||||
.fetch_single(&ctx, self_user.as_deref(), &id)
|
||||
.fetch_single(&ctx, &id)
|
||||
.await?
|
||||
.ok_or(ObjectNotFound(id))?;
|
||||
|
||||
|
|
|
@ -9,46 +9,32 @@ use axum::Json;
|
|||
use futures::StreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use magnetar_common::util::lenient_parse_tag;
|
||||
use magnetar_common::util::lenient_parse_tag_decode;
|
||||
use magnetar_sdk::endpoints::user::{
|
||||
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, UserByIdReq,
|
||||
UserSelfReq,
|
||||
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq,
|
||||
};
|
||||
use magnetar_sdk::endpoints::{Req, Res};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn handle_user_info_self(
|
||||
Query(UserSelfReq {
|
||||
detail: _,
|
||||
pins: _,
|
||||
profile: _,
|
||||
secrets: _,
|
||||
}): Query<Req<GetUserSelf>>,
|
||||
Query(req): Query<Req<GetUserSelf>>,
|
||||
State(service): State<Arc<MagnetarService>>,
|
||||
AuthenticatedUser(user): AuthenticatedUser,
|
||||
) -> Result<Json<Res<GetUserSelf>>, ApiError> {
|
||||
// TODO: Extended properties!
|
||||
|
||||
let ctx = PackingContext::new(service, Some(user.clone())).await?;
|
||||
let user = UserModel.base_from_existing(&ctx, user.as_ref()).await?;
|
||||
Ok(Json(user.into()))
|
||||
let user = UserModel
|
||||
.self_full_from_base(&ctx, user.as_ref(), &req, None, None)
|
||||
.await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
pub async fn handle_user_info(
|
||||
Path(id): Path<String>,
|
||||
Query(UserByIdReq {
|
||||
detail: _,
|
||||
pins: _,
|
||||
profile: _,
|
||||
relation: _,
|
||||
auth: _,
|
||||
}): Query<Req<GetUserById>>,
|
||||
Query(req): Query<Req<GetUserById>>,
|
||||
State(service): State<Arc<MagnetarService>>,
|
||||
MaybeUser(self_user): MaybeUser,
|
||||
) -> Result<Json<Res<GetUserById>>, ApiError> {
|
||||
// TODO: Extended properties!
|
||||
|
||||
let ctx = PackingContext::new(service.clone(), self_user).await?;
|
||||
let user_model = service
|
||||
.db
|
||||
|
@ -56,25 +42,19 @@ pub async fn handle_user_info(
|
|||
.await?
|
||||
.ok_or(ObjectNotFound(id))?;
|
||||
|
||||
let user = UserModel.base_from_existing(&ctx, &user_model).await?;
|
||||
Ok(Json(user.into()))
|
||||
let user = UserModel
|
||||
.foreign_full_from_base(&ctx, &user_model, &req, None, None)
|
||||
.await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
pub async fn handle_user_info_by_acct(
|
||||
Path(tag_str): Path<String>,
|
||||
Query(UserByIdReq {
|
||||
detail: _,
|
||||
pins: _,
|
||||
profile: _,
|
||||
relation: _,
|
||||
auth: _,
|
||||
}): Query<Req<GetUserByAcct>>,
|
||||
Query(req): Query<Req<GetUserByAcct>>,
|
||||
State(service): State<Arc<MagnetarService>>,
|
||||
MaybeUser(self_user): MaybeUser,
|
||||
) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
|
||||
// TODO: Extended properties!
|
||||
|
||||
let mut tag = lenient_parse_tag(&tag_str)?;
|
||||
let mut tag = lenient_parse_tag_decode(&tag_str)?;
|
||||
if matches!(&tag.host, Some(host) if host == &service.config.networking.host) {
|
||||
tag.host = None;
|
||||
}
|
||||
|
@ -86,8 +66,10 @@ pub async fn handle_user_info_by_acct(
|
|||
.await?
|
||||
.ok_or(ObjectNotFound(tag_str))?;
|
||||
|
||||
let user = UserModel.base_from_existing(&ctx, &user_model).await?;
|
||||
Ok(Json(user.into()))
|
||||
let user = UserModel
|
||||
.foreign_full_from_base(&ctx, &user_model, &req, None, None)
|
||||
.await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
pub async fn handle_user_by_id_many(
|
||||
|
@ -109,7 +91,7 @@ pub async fn handle_user_by_id_many(
|
|||
|
||||
let futures = users
|
||||
.iter()
|
||||
.map(|u| user_model.base_from_existing(&ctx, u))
|
||||
.map(|u| user_model.base_from_existing(&ctx, u, None))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let users_proc = futures::stream::iter(futures)
|
||||
|
|
|
@ -6,7 +6,8 @@ use magnetar_sdk::types::instance::InstanceTicker;
|
|||
use magnetar_sdk::types::note::PackNoteMaybeFull;
|
||||
use magnetar_sdk::types::user::{
|
||||
AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
|
||||
UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserRelationExt, UserSecretsExt,
|
||||
UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx,
|
||||
UserRelationExt, UserSecretsExt,
|
||||
};
|
||||
use magnetar_sdk::types::MmXml;
|
||||
use url::Url;
|
||||
|
@ -76,8 +77,7 @@ pub struct UserProfileExtSource<'a> {
|
|||
pub user: &'a ck::user::Model,
|
||||
pub profile: &'a ck::user_profile::Model,
|
||||
pub profile_fields: &'a Vec<ProfileField>,
|
||||
pub banner_url: Option<&'a Url>,
|
||||
pub banner: Option<&'a ck::drive_file::Model>,
|
||||
pub banner: Option<&'a PackDriveFileBase>,
|
||||
pub description_mm: Option<&'a MmXml>,
|
||||
pub relation: Option<&'a UserRelationExt>,
|
||||
}
|
||||
|
@ -89,7 +89,6 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
|
|||
user,
|
||||
profile,
|
||||
profile_fields,
|
||||
banner_url,
|
||||
banner,
|
||||
description_mm,
|
||||
relation,
|
||||
|
@ -97,7 +96,9 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
|
|||
) -> Self {
|
||||
let follow_visibility = match profile.ff_visibility {
|
||||
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,
|
||||
} || context.is_self(user);
|
||||
|
||||
|
@ -119,8 +120,8 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
|
|||
url: profile.url.clone(),
|
||||
moved_to_uri: user.moved_to_uri.clone(),
|
||||
also_known_as: user.also_known_as.clone(),
|
||||
banner_url: banner_url.map(Url::to_string),
|
||||
banner_blurhash: banner.and_then(|b| b.blurhash.clone()),
|
||||
banner_url: banner.and_then(|b| b.file.0.url.to_owned()),
|
||||
banner_blurhash: banner.and_then(|b| b.file.0.blurhash.clone()),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ impl Default for ProcessingLimits {
|
|||
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
enum UserRelationship {
|
||||
Follow,
|
||||
FollowRequest,
|
||||
Mute,
|
||||
Block,
|
||||
RenoteMute,
|
||||
|
@ -120,6 +121,12 @@ impl PackingContext {
|
|||
.get_follower_status(&link.from, &link.to)
|
||||
.await?
|
||||
.is_some(),
|
||||
UserRelationship::FollowRequest => self
|
||||
.service
|
||||
.db
|
||||
.get_follow_request_status(&link.from, &link.to)
|
||||
.await?
|
||||
.is_some(),
|
||||
UserRelationship::Mute => self
|
||||
.service
|
||||
.db
|
||||
|
|
|
@ -19,6 +19,8 @@ pub enum PackError {
|
|||
#[error("Calckey database wrapper error: {0}")]
|
||||
CalckeyDbError(#[from] CalckeyDbError),
|
||||
#[error("Emoji cache error: {0}")]
|
||||
DataError(String),
|
||||
#[error("Emoji cache error: {0}")]
|
||||
EmojiCacheError(#[from] EmojiCacheError),
|
||||
#[error("Instance cache error: {0}")]
|
||||
InstanceMetaCacheError(#[from] InstanceMetaCacheError),
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::model::{PackType, PackingContext, UserRelationship};
|
|||
use compact_str::CompactString;
|
||||
use either::Either;
|
||||
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::emoji::EmojiTag;
|
||||
use magnetar_calckey_model::note_model::{
|
||||
|
@ -197,7 +197,7 @@ impl NoteModel {
|
|||
note_data: &NoteData,
|
||||
) -> PackResult<PackNoteBase> {
|
||||
let Required(ref user) = UserModel
|
||||
.base_from_existing(ctx, ¬e_data.user)
|
||||
.base_from_existing(ctx, ¬e_data.user, None)
|
||||
.await?
|
||||
.user;
|
||||
|
||||
|
@ -378,7 +378,6 @@ impl NoteModel {
|
|||
async fn extract_poll(
|
||||
&self,
|
||||
ctx: &PackingContext,
|
||||
as_user: Option<&ck::user::Model>,
|
||||
note: &ck::note::Model,
|
||||
) -> PackResult<Option<PackPollBase>> {
|
||||
if !note.has_poll {
|
||||
|
@ -391,7 +390,7 @@ impl NoteModel {
|
|||
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(¬e.id, &u.id).await?),
|
||||
None => None,
|
||||
};
|
||||
|
@ -406,13 +405,12 @@ impl NoteModel {
|
|||
&self,
|
||||
ctx: &PackingContext,
|
||||
drive_model: &DriveModel,
|
||||
as_user: Option<&ck::user::Model>,
|
||||
note_data: &NoteData,
|
||||
) -> PackResult<PackNoteMaybeAttachments> {
|
||||
let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!(
|
||||
self.extract_base(ctx, note_data),
|
||||
self.extract_attachments(ctx, drive_model, ¬e_data.note),
|
||||
self.extract_poll(ctx, as_user, ¬e_data.note)
|
||||
self.extract_poll(ctx, ¬e_data.note)
|
||||
)?;
|
||||
|
||||
Ok(PackNoteMaybeAttachments::pack_from((
|
||||
|
@ -431,40 +429,17 @@ impl NoteModel {
|
|||
)))
|
||||
}
|
||||
|
||||
pub async fn fetch_single(
|
||||
async fn pack_full_single(
|
||||
&self,
|
||||
ctx: &PackingContext,
|
||||
as_user: Option<&ck::user::Model>,
|
||||
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(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);
|
||||
};
|
||||
|
||||
note: NoteData,
|
||||
) -> PackResult<PackNoteMaybeFull> {
|
||||
let drive_model = DriveModel;
|
||||
|
||||
let reply_target = async {
|
||||
match note.reply.as_ref() {
|
||||
Some(r) if self.with_context => self
|
||||
.pack_single_attachments(ctx, &drive_model, as_user, r)
|
||||
.pack_single_attachments(ctx, &drive_model, r)
|
||||
.await
|
||||
.map(Some),
|
||||
_ => Ok(None),
|
||||
|
@ -474,7 +449,7 @@ impl NoteModel {
|
|||
let renote_target = async {
|
||||
match note.renote.as_ref() {
|
||||
Some(r) if self.with_context => self
|
||||
.pack_single_attachments(ctx, &drive_model, as_user, r)
|
||||
.pack_single_attachments(ctx, &drive_model, r)
|
||||
.await
|
||||
.map(Some),
|
||||
_ => Ok(None),
|
||||
|
@ -491,7 +466,7 @@ impl NoteModel {
|
|||
reply_target_pack,
|
||||
renote_target_pack,
|
||||
) = try_join!(
|
||||
self.pack_single_attachments(ctx, &drive_model, as_user, ¬e),
|
||||
self.pack_single_attachments(ctx, &drive_model, ¬e),
|
||||
reply_target,
|
||||
renote_target
|
||||
)?;
|
||||
|
@ -506,12 +481,89 @@ impl NoteModel {
|
|||
)
|
||||
});
|
||||
|
||||
Ok(Some(PackNoteMaybeFull::pack_from((
|
||||
Ok(PackNoteMaybeFull::pack_from((
|
||||
id,
|
||||
note,
|
||||
user_context,
|
||||
attachment,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::emoji::EmojiModel;
|
||||
use crate::model::processing::{get_mm_token_emoji, PackResult};
|
||||
use crate::model::{PackType, PackingContext};
|
||||
use crate::model::processing::note::NoteModel;
|
||||
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_sdk::endpoints::user::{UserByIdReq, UserSelfReq};
|
||||
use magnetar_sdk::mmm::Token;
|
||||
use magnetar_sdk::types::drive::PackDriveFileBase;
|
||||
use magnetar_sdk::types::emoji::EmojiContext;
|
||||
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::{mmm, Packed, Required};
|
||||
use magnetar_sdk::{mmm, Optional, Packed, Required};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{join, try_join};
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProfileFieldRaw<'a> {
|
||||
name: &'a str,
|
||||
value: &'a str,
|
||||
}
|
||||
|
||||
pub struct UserModel;
|
||||
|
||||
impl UserModel {
|
||||
|
@ -21,6 +37,12 @@ impl UserModel {
|
|||
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(
|
||||
&self,
|
||||
ctx: &PackingContext,
|
||||
|
@ -56,15 +78,17 @@ impl UserModel {
|
|||
&self,
|
||||
ctx: &PackingContext,
|
||||
user: &ck::user::Model,
|
||||
hint_avatar_file: Option<&ck::drive_file::Model>,
|
||||
) -> PackResult<PackUserBase> {
|
||||
let drive_file_pack = DriveModel;
|
||||
let avatar = match &user.avatar_id {
|
||||
Some(av_id) => drive_file_pack.get_cached_base(ctx, av_id).await?,
|
||||
None => None,
|
||||
let avatar = match (hint_avatar_file, &user.avatar_id) {
|
||||
(Some(avatar_file), _) => Some(drive_file_pack.pack_existing(ctx, avatar_file)),
|
||||
(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 username_mm = self.tokenize_username(&user);
|
||||
let username_mm = self.tokenize_username(user);
|
||||
|
||||
let emoji_model = EmojiModel;
|
||||
let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&username_mm));
|
||||
|
@ -100,4 +124,305 @@ impl UserModel {
|
|||
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! |