2024-05-29 02:59:49 +02:00
import { mentionValidator } from "@/api" ;
import { sanitizeHtml , sanitizeHtmlInline } from "@/sanitization" ;
2024-04-25 05:40:27 +02:00
import markdownItTaskLists from "@hackmd/markdown-it-task-lists" ;
2024-08-26 19:06:49 +02:00
import type { ContentFormat } from "@versia/federation/types" ;
2024-04-13 14:20:12 +02:00
import {
type InferSelectModel ,
and ,
eq ,
inArray ,
isNull ,
or ,
sql ,
} from "drizzle-orm" ;
2024-04-07 07:30:49 +02:00
import linkifyHtml from "linkify-html" ;
2024-04-14 05:49:32 +02:00
import {
anyOf ,
charIn ,
createRegExp ,
digit ,
exactly ,
global ,
letter ,
2024-04-14 12:36:25 +02:00
} from "magic-regexp" ;
2024-04-25 05:40:27 +02:00
import MarkdownIt from "markdown-it" ;
import markdownItContainer from "markdown-it-container" ;
import markdownItTocDoneRight from "markdown-it-toc-done-right" ;
2024-05-29 02:59:49 +02:00
import { db } from "~/drizzle/db" ;
2024-06-06 09:04:52 +02:00
import {
type Attachments ,
Instances ,
type Notes ,
Users ,
} from "~/drizzle/schema" ;
2024-08-19 14:43:54 +02:00
import { config } from "~/packages/config-manager/index" ;
2024-07-26 18:51:39 +02:00
import type { EmojiWithInstance } from "~/packages/database-interface/emoji" ;
2024-05-29 02:59:49 +02:00
import { User } from "~/packages/database-interface/user" ;
2024-06-13 04:26:43 +02:00
import type { Application } from "./application" ;
2024-04-11 13:39:07 +02:00
import {
2024-04-17 06:09:21 +02:00
type UserWithInstance ,
2024-04-13 14:20:12 +02:00
type UserWithRelations ,
transformOutputToUserWithRelations ,
2024-04-11 13:39:07 +02:00
userExtrasTemplate ,
2024-04-13 14:20:12 +02:00
userRelations ,
2024-06-13 04:26:43 +02:00
} from "./user" ;
2024-04-11 13:39:07 +02:00
2024-04-17 08:36:01 +02:00
export type Status = InferSelectModel < typeof Notes > ;
2024-04-11 13:39:07 +02:00
export type StatusWithRelations = Status & {
author : UserWithRelations ;
2024-04-17 06:09:21 +02:00
mentions : UserWithInstance [ ] ;
2024-04-17 08:36:01 +02:00
attachments : InferSelectModel < typeof Attachments > [ ] ;
2024-04-11 13:39:07 +02:00
reblog : StatusWithoutRecursiveRelations | null ;
emojis : EmojiWithInstance [ ] ;
2024-04-17 08:36:01 +02:00
reply : Status | null ;
quote : Status | null ;
2024-04-17 06:09:21 +02:00
application : Application | null ;
2024-04-11 13:39:07 +02:00
reblogCount : number ;
likeCount : number ;
replyCount : number ;
2024-05-09 01:19:53 +02:00
pinned : boolean ;
reblogged : boolean ;
muted : boolean ;
liked : boolean ;
2024-04-11 13:39:07 +02:00
} ;
2023-09-12 22:48:10 +02:00
2024-04-11 13:39:07 +02:00
export type StatusWithoutRecursiveRelations = Omit <
StatusWithRelations ,
2024-04-17 08:36:01 +02:00
"reply" | "quote" | "reblog"
2023-11-27 01:56:16 +01:00
> ;
2023-10-23 07:39:42 +02:00
2023-11-11 03:36:06 +01:00
/ * *
2024-04-17 06:09:21 +02:00
* Wrapper against the Status object to make it easier to work with
* @param query
* @returns
2023-11-11 03:36:06 +01:00
* /
2024-04-17 06:09:21 +02:00
export const findManyNotes = async (
2024-04-17 08:36:01 +02:00
query : Parameters < typeof db.query.Notes.findMany > [ 0 ] ,
2024-05-09 01:19:53 +02:00
userId? : string ,
2024-04-11 13:39:07 +02:00
) : Promise < StatusWithRelations [ ] > = > {
2024-04-17 08:36:01 +02:00
const output = await db . query . Notes . findMany ( {
2024-04-11 13:39:07 +02:00
. . . query ,
with : {
. . . query ? . with ,
2024-05-08 23:51:47 +02:00
attachments : true ,
2024-04-11 14:12:16 +02:00
emojis : {
with : {
emoji : {
with : {
instance : true ,
} ,
} ,
} ,
} ,
2024-04-11 13:39:07 +02:00
author : {
with : {
. . . userRelations ,
} ,
2024-04-17 08:36:01 +02:00
extras : userExtrasTemplate ( "Notes_author" ) ,
2024-04-11 13:39:07 +02:00
} ,
mentions : {
with : {
user : {
2024-04-17 06:09:21 +02:00
with : {
instance : true ,
} ,
2024-04-11 13:39:07 +02:00
} ,
} ,
} ,
reblog : {
with : {
attachments : true ,
emojis : {
with : {
emoji : {
with : {
instance : true ,
} ,
} ,
} ,
} ,
likes : true ,
application : true ,
mentions : {
with : {
user : {
2024-04-12 01:55:58 +02:00
with : userRelations ,
extras : userExtrasTemplate (
2024-04-17 08:36:01 +02:00
"Notes_reblog_mentions_user" ,
2024-04-12 01:55:58 +02:00
) ,
2024-04-11 13:39:07 +02:00
} ,
} ,
} ,
author : {
with : {
. . . userRelations ,
} ,
2024-04-17 08:36:01 +02:00
extras : userExtrasTemplate ( "Notes_reblog_author" ) ,
2024-04-11 13:39:07 +02:00
} ,
} ,
2024-04-17 06:09:21 +02:00
extras : {
2024-05-08 23:51:47 +02:00
reblogCount :
sql ` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes_reblog".id) ` . as (
"reblog_count" ,
) ,
likeCount :
sql ` (SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id) ` . as (
"like_count" ,
) ,
replyCount :
sql ` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes_reblog".id) ` . as (
"reply_count" ,
) ,
2024-05-09 01:19:53 +02:00
pinned : userId
? sql ` EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${ userId } ) ` . as (
"pinned" ,
)
: sql ` false ` . as ( "pinned" ) ,
reblogged : userId
? sql ` EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${ userId } AND "Notes"."reblogId" = "Notes_reblog".id) ` . as (
"reblogged" ,
)
: sql ` false ` . as ( "reblogged" ) ,
muted : userId
? sql ` EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${ userId } AND "Relationships"."subjectId" = "Notes_reblog"."authorId" AND "Relationships"."muting" = true) ` . as (
"muted" ,
)
: sql ` false ` . as ( "muted" ) ,
liked : userId
? sql ` EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id AND "Likes"."likerId" = ${ userId } ) ` . as (
"liked" ,
)
: sql ` false ` . as ( "liked" ) ,
2024-04-11 13:39:07 +02:00
} ,
} ,
2024-04-17 08:36:01 +02:00
reply : true ,
quote : true ,
2024-04-11 13:39:07 +02:00
} ,
extras : {
2024-05-08 23:51:47 +02:00
reblogCount :
sql ` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes".id) ` . as (
"reblog_count" ,
) ,
likeCount :
sql ` (SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes".id) ` . as (
"like_count" ,
) ,
replyCount :
sql ` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes".id) ` . as (
"reply_count" ,
) ,
2024-05-09 01:19:53 +02:00
pinned : userId
? sql ` EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${ userId } ) ` . as (
"pinned" ,
)
: sql ` false ` . as ( "pinned" ) ,
reblogged : userId
? sql ` EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${ userId } AND "Notes"."reblogId" = "Notes".id) ` . as (
"reblogged" ,
)
: sql ` false ` . as ( "reblogged" ) ,
muted : userId
? sql ` EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${ userId } AND "Relationships"."subjectId" = "Notes"."authorId" AND "Relationships"."muting" = true) ` . as (
"muted" ,
)
: sql ` false ` . as ( "muted" ) ,
liked : userId
? sql ` EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes".id AND "Likes"."likerId" = ${ userId } ) ` . as (
"liked" ,
)
: sql ` false ` . as ( "liked" ) ,
2024-04-11 13:39:07 +02:00
. . . query ? . extras ,
} ,
} ) ;
return output . map ( ( post ) = > ( {
. . . post ,
author : transformOutputToUserWithRelations ( post . author ) ,
2024-04-17 06:09:21 +02:00
mentions : post.mentions.map ( ( mention ) = > ( {
. . . mention . user ,
2024-04-17 08:36:01 +02:00
endpoints : mention.user.endpoints ,
2024-04-17 06:09:21 +02:00
} ) ) ,
2024-04-17 08:36:01 +02:00
emojis : ( post . emojis ? ? [ ] ) . map ( ( emoji ) = > emoji . emoji ) ,
2024-04-11 13:39:07 +02:00
reblog : post.reblog && {
. . . post . reblog ,
author : transformOutputToUserWithRelations ( post . reblog . author ) ,
2024-04-17 06:09:21 +02:00
mentions : post.reblog.mentions.map ( ( mention ) = > ( {
. . . mention . user ,
2024-04-17 08:36:01 +02:00
endpoints : mention.user.endpoints ,
2024-04-17 06:09:21 +02:00
} ) ) ,
2024-04-17 08:36:01 +02:00
emojis : ( post . reblog . emojis ? ? [ ] ) . map ( ( emoji ) = > emoji . emoji ) ,
2024-04-17 06:09:21 +02:00
reblogCount : Number ( post . reblog . reblogCount ) ,
likeCount : Number ( post . reblog . likeCount ) ,
replyCount : Number ( post . reblog . replyCount ) ,
2024-05-09 01:19:53 +02:00
pinned : Boolean ( post . reblog . pinned ) ,
reblogged : Boolean ( post . reblog . reblogged ) ,
muted : Boolean ( post . reblog . muted ) ,
liked : Boolean ( post . reblog . liked ) ,
2024-04-11 13:39:07 +02:00
} ,
reblogCount : Number ( post . reblogCount ) ,
likeCount : Number ( post . likeCount ) ,
replyCount : Number ( post . replyCount ) ,
2024-05-09 01:19:53 +02:00
pinned : Boolean ( post . pinned ) ,
reblogged : Boolean ( post . reblogged ) ,
muted : Boolean ( post . muted ) ,
liked : Boolean ( post . liked ) ,
2024-04-11 13:39:07 +02:00
} ) ) ;
} ;
2024-04-10 04:05:02 +02:00
/ * *
* Get people mentioned in the content ( match @username or @username @domain . com mentions )
* @param text The text to parse mentions from .
* @returns An array of users mentioned in the text .
* /
2024-06-30 10:24:10 +02:00
export const parseTextMentions = async (
text : string ,
author : User ,
) : Promise < User [ ] > = > {
2024-05-13 04:27:40 +02:00
const mentionedPeople = [ . . . text . matchAll ( mentionValidator ) ] ? ? [ ] ;
2024-06-13 04:26:43 +02:00
if ( mentionedPeople . length === 0 ) {
return [ ] ;
}
2024-04-11 13:39:07 +02:00
2024-04-14 05:49:32 +02:00
const baseUrlHost = new URL ( config . http . base_url ) . host ;
2024-04-12 03:52:09 +02:00
2024-04-14 05:49:32 +02:00
const isLocal = ( host? : string ) = > host === baseUrlHost || ! host ;
2024-04-12 03:52:09 +02:00
2024-04-14 05:49:32 +02:00
const foundUsers = await db
. select ( {
2024-04-17 08:36:01 +02:00
id : Users.id ,
username : Users.username ,
baseUrl : Instances.baseUrl ,
2024-04-14 05:49:32 +02:00
} )
2024-04-17 08:36:01 +02:00
. from ( Users )
. leftJoin ( Instances , eq ( Users . instanceId , Instances . id ) )
2024-04-14 05:49:32 +02:00
. where (
or (
. . . mentionedPeople . map ( ( person ) = >
and (
2024-04-17 08:36:01 +02:00
eq ( Users . username , person ? . [ 1 ] ? ? "" ) ,
2024-04-14 05:49:32 +02:00
isLocal ( person ? . [ 2 ] )
2024-04-17 08:36:01 +02:00
? isNull ( Users . instanceId )
: eq ( Instances . baseUrl , person ? . [ 2 ] ? ? "" ) ,
2024-04-14 05:49:32 +02:00
) ,
) ,
) ,
) ;
2024-04-12 03:52:09 +02:00
2024-04-14 05:49:32 +02:00
const notFoundRemoteUsers = mentionedPeople . filter (
( person ) = >
2024-06-13 04:26:43 +02:00
! (
isLocal ( person ? . [ 2 ] ) ||
foundUsers . find (
( user ) = >
user . username === person ? . [ 1 ] &&
user . baseUrl === person ? . [ 2 ] ,
)
2024-04-14 05:49:32 +02:00
) ,
) ;
2024-04-12 03:52:09 +02:00
const finalList =
2024-04-14 05:49:32 +02:00
foundUsers . length > 0
2024-04-25 05:40:27 +02:00
? await User . manyFromSql (
inArray (
Users . id ,
foundUsers . map ( ( u ) = > u . id ) ,
) ,
)
2024-04-12 03:52:09 +02:00
: [ ] ;
2024-04-11 00:47:02 +02:00
// Attempt to resolve mentions that were not found
2024-04-14 05:49:32 +02:00
for ( const person of notFoundRemoteUsers ) {
2024-07-26 18:51:39 +02:00
const manager = await author . getFederationRequester ( ) ;
2024-06-30 10:24:10 +02:00
2024-07-26 18:07:11 +02:00
const uri = await User . webFinger (
manager ,
person ? . [ 1 ] ? ? "" ,
person ? . [ 2 ] ? ? "" ,
) ;
2024-06-30 10:24:10 +02:00
const user = await User . resolve ( uri ) ;
2024-04-11 00:47:02 +02:00
if ( user ) {
2024-04-11 13:39:07 +02:00
finalList . push ( user ) ;
2024-04-11 00:47:02 +02:00
}
}
2024-04-11 13:39:07 +02:00
return finalList ;
2024-04-10 04:05:02 +02:00
} ;
2024-06-13 04:26:43 +02:00
export const replaceTextMentions = ( text : string , mentions : User [ ] ) = > {
2024-04-10 11:33:21 +02:00
let finalText = text ;
for ( const mention of mentions ) {
2024-06-13 02:45:07 +02:00
const user = mention . data ;
2024-04-10 11:33:21 +02:00
// Replace @username and @username@domain
2024-04-25 05:40:27 +02:00
if ( user . instance ) {
2024-04-10 11:33:21 +02:00
finalText = finalText . replace (
2024-04-14 05:49:32 +02:00
createRegExp (
2024-04-25 05:40:27 +02:00
exactly ( ` @ ${ user . username } @ ${ user . instance . baseUrl } ` ) ,
2024-04-14 05:49:32 +02:00
[ global ] ,
) ,
2024-04-25 05:40:27 +02:00
` <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href=" ${ mention . getUri ( ) } ">@ ${
user . username
} @ $ { user . instance . baseUrl } < / a > ` ,
2024-04-10 11:33:21 +02:00
) ;
} else {
finalText = finalText . replace (
2024-04-12 03:52:09 +02:00
// Only replace @username if it doesn't have another @ right after
2024-04-14 05:49:32 +02:00
createRegExp (
2024-04-25 05:40:27 +02:00
exactly ( ` @ ${ user . username } ` )
2024-04-14 05:49:32 +02:00
. notBefore ( anyOf ( letter , digit , charIn ( "@" ) ) )
. notAfter ( anyOf ( letter , digit , charIn ( "@" ) ) ) ,
[ global ] ,
) ,
2024-04-25 05:40:27 +02:00
` <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href=" ${ mention . getUri ( ) } ">@ ${
user . username
} < / a > ` ,
2024-04-12 03:52:09 +02:00
) ;
finalText = finalText . replace (
2024-04-14 05:49:32 +02:00
createRegExp (
exactly (
2024-04-25 05:40:27 +02:00
` @ ${ user . username } @ ${
2024-04-14 05:49:32 +02:00
new URL ( config . http . base_url ) . host
} ` ,
) ,
[ global ] ,
) ,
2024-04-25 05:40:27 +02:00
` <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href=" ${ mention . getUri ( ) } ">@ ${
user . username
} < / a > ` ,
2024-04-10 11:33:21 +02:00
) ;
}
}
return finalText ;
} ;
2024-04-14 05:49:32 +02:00
export const contentToHtml = async (
2024-06-20 01:21:02 +02:00
content : ContentFormat ,
2024-04-25 05:40:27 +02:00
mentions : User [ ] = [ ] ,
2024-05-12 03:27:28 +02:00
inline = false ,
2024-04-14 05:49:32 +02:00
) : Promise < string > = > {
2024-04-10 04:05:02 +02:00
let htmlContent : string ;
2024-05-12 03:27:28 +02:00
const sanitizer = inline ? sanitizeHtmlInline : sanitizeHtml ;
2024-04-10 04:05:02 +02:00
if ( content [ "text/html" ] ) {
2024-05-12 03:27:28 +02:00
htmlContent = await sanitizer ( content [ "text/html" ] . content ) ;
2024-04-10 04:05:02 +02:00
} else if ( content [ "text/markdown" ] ) {
2024-05-12 03:27:28 +02:00
htmlContent = await sanitizer (
2024-04-22 23:02:09 +02:00
await markdownParse ( content [ "text/markdown" ] . content ) ,
2024-04-10 04:05:02 +02:00
) ;
2024-04-22 23:02:09 +02:00
} else if ( content [ "text/plain" ] ? . content ) {
2024-04-10 04:05:02 +02:00
// Split by newline and add <p> tags
2024-05-12 03:27:28 +02:00
htmlContent = ( await sanitizer ( content [ "text/plain" ] . content ) )
2024-04-10 04:05:02 +02:00
. split ( "\n" )
. map ( ( line ) = > ` <p> ${ line } </p> ` )
. join ( "\n" ) ;
} else {
htmlContent = "" ;
}
2024-04-10 11:33:21 +02:00
// Replace mentions text
htmlContent = await replaceTextMentions ( htmlContent , mentions ? ? [ ] ) ;
2024-04-12 03:52:09 +02:00
// Linkify
htmlContent = linkifyHtml ( htmlContent , {
defaultProtocol : "https" ,
validate : {
email : ( ) = > false ,
} ,
target : "_blank" ,
rel : "nofollow noopener noreferrer" ,
} ) ;
2024-04-14 05:49:32 +02:00
return htmlContent ;
} ;
2024-04-22 23:02:09 +02:00
export const markdownParse = async ( content : string ) = > {
return ( await getMarkdownRenderer ( ) ) . render ( content ) ;
} ;
2024-06-13 04:26:43 +02:00
export const getMarkdownRenderer = ( ) = > {
2024-04-22 23:02:09 +02:00
const renderer = MarkdownIt ( {
html : true ,
linkify : true ,
} ) ;
renderer . use ( markdownItTocDoneRight , {
containerClass : "toc" ,
level : [ 1 , 2 , 3 , 4 ] ,
listType : "ul" ,
listClass : "toc-list" ,
itemClass : "toc-item" ,
linkClass : "toc-link" ,
} ) ;
renderer . use ( markdownItTaskLists ) ;
renderer . use ( markdownItContainer ) ;
return renderer ;
} ;