magnetar/src/model/processing/note.rs

260 lines
8.7 KiB
Rust

use crate::model::data::id::BaseId;
use crate::model::data::note::NoteBaseSource;
use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::user::UserModel;
use crate::model::processing::{get_mm_token_emoji, PackResult};
use crate::model::{PackType, PackingContext, UserRelationship};
use compact_str::CompactString;
use either::Either;
use magnetar_calckey_model::ck::sea_orm_active_enums::NoteVisibilityEnum;
use magnetar_calckey_model::note_model::{NoteResolveOptions, NoteVisibilityFilterFactory};
use magnetar_calckey_model::sea_orm::prelude::Expr;
use magnetar_calckey_model::sea_orm::sea_query::{Alias, IntoIden, PgFunc, Query, SimpleExpr};
use magnetar_calckey_model::sea_orm::{ActiveEnum, ColumnTrait, IntoSimpleExpr};
use magnetar_calckey_model::{ck, CalckeyDbError};
use magnetar_sdk::mmm::Token;
use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::note::{NoteBase, PackNoteMaybeFull};
use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Packed, Required};
#[derive(Debug, Clone)]
pub struct NoteVisibilityFilterSimple(Option<String>);
impl NoteVisibilityFilterFactory for NoteVisibilityFilterSimple {
fn with_note_and_user_tables(&self, note_tbl: Option<Alias>) -> SimpleExpr {
let note_tbl_name =
note_tbl.map_or_else(|| ck::note::Entity.into_iden(), |a| a.into_iden());
let note_visibility = Expr::col((note_tbl_name.clone(), ck::note::Column::Visibility));
let note_mentions = Expr::col((note_tbl_name.clone(), ck::note::Column::Mentions));
let note_reply_user_id = Expr::col((note_tbl_name.clone(), ck::note::Column::ReplyUserId));
let note_visible_user_ids =
Expr::col((note_tbl_name.clone(), ck::note::Column::VisibleUserIds));
let note_user_id = Expr::col((note_tbl_name, ck::note::Column::UserId));
let is_public = note_visibility
.clone()
.eq(NoteVisibilityEnum::Public.as_enum())
.or(note_visibility
.clone()
.eq(NoteVisibilityEnum::Home.as_enum()));
let Some(user_id_str) = &self.0 else {
return is_public;
};
let self_user_id = SimpleExpr::Constant(user_id_str.into());
let is_self = note_user_id.clone().eq(self_user_id.clone());
let is_visible_specified = {
let either_specified_or_followers = note_visibility
.clone()
.eq(NoteVisibilityEnum::Specified.as_enum())
.or(note_visibility
.clone()
.eq(NoteVisibilityEnum::Followers.as_enum()))
.into_simple_expr();
let mentioned_or_specified = self_user_id
.clone()
.eq(PgFunc::any(note_mentions.into_simple_expr()))
.or(self_user_id.eq(PgFunc::any(note_visible_user_ids)));
either_specified_or_followers.and(mentioned_or_specified)
};
let is_visible_followers = {
note_visibility
.eq(NoteVisibilityEnum::Followers.as_enum())
.and(
note_user_id.in_subquery(
Query::select()
.column(ck::following::Column::FolloweeId)
.from(ck::following::Entity)
.cond_where(ck::following::Column::FollowerId.eq(user_id_str))
.to_owned(),
),
)
.or(note_reply_user_id.eq(user_id_str))
};
is_self
.or(is_public)
.or(is_visible_followers)
.or(is_visible_specified)
}
}
pub struct NoteVisibilityFilterModel;
impl NoteVisibilityFilterModel {
pub async fn is_note_visible(
&self,
ctx: &PackingContext,
user: Option<&ck::user::Model>,
note: &ck::note::Model,
) -> Result<bool, CalckeyDbError> {
if user.is_some_and(|user| user.id == note.user_id) {
return Ok(true);
}
if matches!(
note.visibility,
NoteVisibilityEnum::Public | NoteVisibilityEnum::Home
) {
return Ok(true);
}
if matches!(
note.visibility,
NoteVisibilityEnum::Followers | NoteVisibilityEnum::Specified
) {
let Some(user) = user else {
return Ok(false);
};
if note.mentions.contains(&user.id) || note.visible_user_ids.contains(&user.id) {
return Ok(true);
}
if matches!(note.visibility, NoteVisibilityEnum::Specified) {
return Ok(false);
}
let following = ctx
.is_relationship_between(
Either::Right(user),
Either::Left(&note.user_id),
UserRelationship::Follow,
)
.await?;
// The second condition generally will not happen in the API,
// however it allows some AP processing, with activities
// between two foreign objects
return Ok(following || user.host.is_some() && note.user_host.is_some());
}
return Ok(false);
}
pub fn new_note_visibility_filter(&self, user: Option<&str>) -> NoteVisibilityFilterSimple {
NoteVisibilityFilterSimple(user.map(str::to_string))
}
}
struct SpeechTransformNyan;
impl SpeechTransformNyan {
fn new() -> Self {
SpeechTransformNyan
}
fn transform(&self, text: &mut CompactString) {
// TODO
}
}
pub struct NoteModel;
impl NoteModel {
pub fn tokenize_note_text(&self, note: &ck::note::Model) -> Option<Token> {
note.text
.as_deref()
.map(|text| mmm::Context::default().parse_full(text))
}
pub fn tokenize_note_cw(&self, note: &ck::note::Model) -> Option<Token> {
note.cw
.as_deref()
.map(|text| mmm::Context::default().parse_ui(text))
}
pub async fn fetch_single(
&self,
ctx: &PackingContext,
as_user: Option<&ck::user::Model>,
id: &str,
show_context: bool,
attachments: bool,
) -> 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: show_context,
with_reply_target: show_context,
with_renote_target: show_context,
})
.await?
else {
return Ok(None);
};
let Required(ref user) = UserModel.base_from_existing(ctx, &note.user).await?.user;
let cw_tok = self.tokenize_note_cw(&note.note);
let mut text_tok = self.tokenize_note_text(&note.note);
let mut emoji_extracted = Vec::new();
if let Some(cw_tok) = &cw_tok {
emoji_extracted.extend_from_slice(&get_mm_token_emoji(cw_tok));
}
if let Some(text_tok) = &mut text_tok {
emoji_extracted.extend_from_slice(&get_mm_token_emoji(text_tok));
if note.user.is_cat && note.user.speak_as_cat {
let transformer = SpeechTransformNyan::new();
text_tok.walk_speech_transform(&|text| transformer.transform(text));
}
}
let emoji_model = EmojiModel;
let shortcodes = emoji_model.deduplicate_emoji(ctx, emoji_extracted);
let emojis = emoji_model
.fetch_many_emojis(ctx, &shortcodes, note.user.host.as_deref())
.await?;
let emoji_context = &EmojiContext(emojis);
// TODO: Polls, reactions, attachments, ...
let note_base = NoteBase::extract(
ctx,
NoteBaseSource {
note: &note.note,
cw_mm: cw_tok
.as_ref()
.map(mmm::to_xml_string)
.and_then(Result::ok)
.map(MmXml)
.as_ref(),
text_mm: text_tok
.as_ref()
.map(mmm::to_xml_string)
.and_then(Result::ok)
.map(MmXml)
.as_ref(),
reactions: &vec![],
user,
emoji_context,
},
);
Ok(Some(PackNoteMaybeFull::pack_from((
Required(Id::from(note.note.id.clone())),
Required(note_base),
None,
None,
))))
}
}