unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
1import { Op, QueryTypes } from 'sequelize'
2import {
3 Ask,
4 Blocks,
5 Emoji,
6 EmojiReaction,
7 FederatedHost,
8 Media,
9 Post,
10 PostEmojiRelations,
11 PostMentionsUserRelation,
12 PostTag,
13 QuestionPoll,
14 QuestionPollAnswer,
15 QuestionPollQuestion,
16 Quotes,
17 sequelize,
18 ServerBlock,
19 User,
20 UserBookmarkedPosts,
21 UserEmojiRelation,
22 UserLikesPostRelations,
23 UserOptions
24} from '../models/index.js'
25import getPosstGroupDetails from './getPostGroupDetails.js'
26import getFollowedsIds from './cacheGetters/getFollowedsIds.js'
27import { Queue } from 'bullmq'
28import { completeEnvironment } from './backendOptions.js'
29import { InteractionControl, InteractionControlType, Privacy } from '../models/post.js'
30import { getAllLocalUserIds } from './cacheGetters/getAllLocalUserIds.js'
31import { checkBskyLabelersNSFW } from './atproto/checkBskyLabelerNSFW.js'
32import { isAdult } from './isAdult.js'
33import { logger } from './logger.js'
34
35const updateMediaDataQueue = new Queue('processRemoteMediaData', {
36 connection: completeEnvironment.bullmqConnection,
37 defaultJobOptions: {
38 removeOnComplete: true,
39 attempts: 3,
40 backoff: {
41 type: 'exponential',
42 delay: 1000
43 },
44 removeOnFail: true
45 }
46})
47
48async function getQuotes(postIds: string[]): Promise<Quotes[]> {
49 return await Quotes.findAll({
50 where: {
51 quoterPostId: {
52 [Op.in]: postIds
53 }
54 }
55 })
56}
57
58async function getMedias(postIds: string[]) {
59 const medias = await Media.findAll({
60 attributes: [
61 'id',
62 'NSFW',
63 'description',
64 'url',
65 'external',
66 'mediaOrder',
67 'mediaType',
68 'postId',
69 'blurhash',
70 'width',
71 'height'
72 ],
73 where: {
74 postId: {
75 [Op.in]: postIds
76 }
77 }
78 })
79
80 let mediasToProcess = medias.filter(
81 (elem: any) => !elem.mediaType || (elem.mediaType?.startsWith('image') && !elem.width)
82 )
83 if (mediasToProcess && mediasToProcess.length > 0) {
84 updateMediaDataQueue.addBulk(
85 mediasToProcess.map((media: any) => {
86 return {
87 name: `getMediaData${media.id}`,
88 data: { mediaId: media.id }
89 }
90 })
91 )
92 }
93 return medias
94}
95async function getMentionedUserIds(
96 postIds: string[]
97): Promise<{ usersMentioned: string[]; postMentionRelation: any[] }> {
98 const mentions = await PostMentionsUserRelation.findAll({
99 attributes: ['userId', 'postId'],
100 where: {
101 postId: {
102 [Op.in]: postIds
103 }
104 }
105 })
106 const usersMentioned = mentions.map((elem: any) => elem.userId)
107 const postMentionRelation = mentions.map((elem: any) => {
108 return { userMentioned: elem.userId, post: elem.postId }
109 })
110 return { usersMentioned, postMentionRelation }
111}
112
113async function getTags(postIds: string[]) {
114 return await PostTag.findAll({
115 attributes: ['postId', 'tagName'],
116 where: {
117 postId: {
118 [Op.in]: postIds
119 }
120 }
121 })
122}
123
124async function getLikes(postIds: string[]) {
125 return await UserLikesPostRelations.findAll({
126 attributes: ['userId', 'postId'],
127 where: {
128 postId: {
129 [Op.in]: postIds
130 }
131 }
132 })
133}
134
135async function getBookmarks(postIds: string[], userId: string) {
136 return await UserBookmarkedPosts.findAll({
137 attributes: ['userId', 'postId'],
138 where: {
139 userId: userId,
140 postId: {
141 [Op.in]: postIds
142 }
143 }
144 })
145}
146
147async function getEmojis(input: { userIds: string[]; postIds: string[] }): Promise<{
148 userEmojiRelation: UserEmojiRelation[]
149 postEmojiRelation: PostEmojiRelations[]
150 postEmojiReactions: EmojiReaction[]
151 emojis: Emoji[]
152}> {
153 let postEmojisIdsPromise = PostEmojiRelations.findAll({
154 attributes: ['emojiId', 'postId'],
155 where: {
156 postId: {
157 [Op.in]: input.postIds
158 }
159 }
160 })
161
162 let postEmojiReactionsPromise = EmojiReaction.findAll({
163 attributes: ['emojiId', 'postId', 'userId', 'content'],
164 where: {
165 postId: {
166 [Op.in]: input.postIds
167 }
168 }
169 })
170
171 let userEmojiIdPromise = UserEmojiRelation.findAll({
172 attributes: ['emojiId', 'userId'],
173 where: {
174 userId: {
175 [Op.in]: input.userIds
176 }
177 }
178 })
179
180 await Promise.all([postEmojisIdsPromise, userEmojiIdPromise, postEmojiReactionsPromise])
181 let postEmojisIds = await postEmojisIdsPromise
182 let userEmojiId = await userEmojiIdPromise
183 let postEmojiReactions = await postEmojiReactionsPromise
184
185 const emojiIds: string[] = ([] as string[])
186 .concat(postEmojisIds.map((elem: any) => elem.emojiId))
187 .concat(userEmojiId.map((elem: any) => elem.emojiId))
188 .concat(postEmojiReactions.map((reaction: any) => reaction.emojiId))
189 return {
190 userEmojiRelation: userEmojiId,
191 postEmojiRelation: postEmojisIds,
192 postEmojiReactions: postEmojiReactions,
193 emojis: await Emoji.findAll({
194 attributes: ['id', 'url', 'external', 'name', 'uuid'],
195 where: {
196 id: {
197 [Op.in]: emojiIds
198 }
199 }
200 })
201 }
202}
203
204// TODO optimization: make more promise all and less await dothing await dothing
205async function getUnjointedPosts(postIdsInput: string[], posterId: string, doNotFullyHide = false) {
206 let user = await User.scope('full').findByPk(posterId)
207
208 // we need a list of all the userId we just got from the post
209 let userIds: string[] = []
210 let postIds: string[] = []
211 if (completeEnvironment.enableBsky) {
212 // DETECT BSKY NSFW
213 const bskyPosts = await Post.findAll({
214 where: {
215 id: {
216 [Op.in]: postIdsInput
217 },
218 userId: {
219 [Op.notIn]: await getAllLocalUserIds()
220 },
221 bskyUri: {
222 [Op.ne]: null
223 }
224 }
225 })
226 if (bskyPosts && bskyPosts.length) {
227 await checkBskyLabelersNSFW(bskyPosts.filter((elem) => !elem.content_warning && elem.bskyUri))
228 }
229 // END DETECT BSKY NSFW
230 }
231 const posts = await Post.findAll({
232 include: [
233 {
234 model: Post,
235 as: 'ancestors',
236 required: false,
237 where: {
238 isDeleted: {
239 [Op.ne]: true
240 }
241 }
242 }
243 ],
244 where: {
245 id: {
246 [Op.in]: postIdsInput
247 },
248 isDeleted: {
249 [Op.ne]: true
250 }
251 }
252 })
253 posts.forEach((post: any) => {
254 userIds.push(post.userId)
255 postIds.push(post.id)
256 post.ancestors?.forEach((ancestor: any) => {
257 userIds.push(ancestor.userId)
258 postIds.push(ancestor.id)
259 })
260 })
261 const quotes = await getQuotes(postIds)
262 const quotedPostsIds = quotes.map((quote) => quote.quotedPostId)
263 postIds = postIds.concat(quotedPostsIds)
264 const quotedPosts = await Post.findAll({
265 where: {
266 id: {
267 [Op.in]: quotedPostsIds
268 }
269 }
270 })
271 const asks = await Ask.findAll({
272 attributes: ['question', 'apObject', 'createdAt', 'updatedAt', 'postId', 'userAsked', 'userAsker'],
273 where: {
274 postId: {
275 [Op.in]: postIds
276 }
277 }
278 })
279
280 const rewootedPosts = await Post.findAll({
281 attributes: ['id', 'parentId'],
282 where: {
283 isReblog: true,
284 userId: posterId,
285 parentId: {
286 [Op.in]: postIds
287 }
288 }
289 })
290 const rewootIds = rewootedPosts.map((r: any) => r.id)
291
292 userIds = userIds
293 .concat(quotedPosts.map((q: any) => q.userId))
294 .concat(asks.map((elem: any) => elem.userAsked))
295 .concat(asks.map((elem: any) => elem.userAsker))
296 const emojis = getEmojis({
297 userIds,
298 postIds
299 })
300 const mentions = await getMentionedUserIds(postIds)
301 userIds = userIds.concat(mentions.usersMentioned)
302 userIds = userIds.concat((await emojis).postEmojiReactions.map((react: any) => react.userId))
303 const polls = QuestionPoll.findAll({
304 where: {
305 postId: {
306 [Op.in]: postIds
307 }
308 },
309 include: [
310 {
311 model: QuestionPollQuestion,
312 include: [
313 {
314 model: QuestionPollAnswer,
315 required: false,
316 where: {
317 userId: posterId
318 }
319 }
320 ]
321 }
322 ]
323 })
324
325 let medias = getMedias([...postIds, ...rewootIds])
326 let tags = getTags([...postIds, ...rewootIds])
327
328 const likes = await getLikes(postIds)
329 const bookmarks = await getBookmarks(postIds, posterId)
330 userIds = userIds.concat(likes.map((like: any) => like.userId))
331 const users = User.findAll({
332 attributes: ['url', 'avatar', 'id', 'name', 'remoteId', 'banned', 'bskyDid', 'federatedHostId', 'isBot'],
333 where: {
334 id: {
335 [Op.in]: userIds
336 }
337 },
338 raw: true
339 })
340 const fediAttachmentsDb = await UserOptions.findAll({
341 where: {
342 userId: {
343 [Op.in]: userIds
344 },
345 optionName: 'fediverse.public.attachment'
346 }
347 })
348 const usersMap: Map<string, User> = new Map()
349 const usersPronounsMap: Map<string, string | undefined> = new Map()
350 for (const att of fediAttachmentsDb) {
351 const fediAttachments: { name: string; value: string }[] = JSON.parse(att.optionValue)
352 const pronouns = fediAttachments.find((elem) => elem.name.toLowerCase() === 'pronouns')?.value
353 if (!pronouns) continue
354 usersPronounsMap.set(att.userId, pronouns)
355 }
356 for (const usr of await users) {
357 usersMap.set(usr.id, usr)
358 }
359 const postWithNotes = getPosstGroupDetails(posts)
360 await Promise.all([emojis, users, polls, medias, tags, postWithNotes])
361 const hostsIds = (await users).filter((elem) => elem.federatedHostId).map((elem) => elem.federatedHostId)
362 const blockedHosts = await FederatedHost.findAll({
363 where: {
364 id: {
365 [Op.in]: hostsIds as string[]
366 },
367 blocked: true
368 }
369 })
370 const blockedHostsIds = blockedHosts.map((elem) => elem.id)
371 let blockedUsersSet: Set<string> = new Set()
372 const blockedUsersQuery = await Blocks.findAll({
373 where: {
374 [Op.or]: [
375 {
376 blockerId: posterId
377 },
378 {
379 blockedId: posterId
380 }
381 ]
382 }
383 })
384 for (const block of blockedUsersQuery) {
385 blockedUsersSet.add(block.blockedId)
386 blockedUsersSet.add(block.blockerId)
387 }
388 blockedUsersSet.delete(posterId)
389 const bannedUserIds = (await users)
390 .filter((elem) => elem.banned || (elem.federatedHostId && blockedHostsIds.includes(elem.federatedHostId)))
391 .map((elem) => elem.id)
392 let usersFollowedByPoster: string[] | Promise<string[]> = getFollowedsIds(posterId)
393 let usersFollowingPoster: string[] | Promise<string[]> = getFollowedsIds(posterId, false, {
394 getFollowersInstead: true
395 })
396
397 await Promise.all([usersFollowedByPoster, usersFollowingPoster, tags, medias])
398 usersFollowedByPoster = await usersFollowedByPoster
399 usersFollowingPoster = await usersFollowingPoster
400 const tagsAwaited = await tags
401 const mediasAwaited = await medias
402
403 const invalidRewoots = [] as string[]
404 for (const id of rewootIds) {
405 const hasMedia = mediasAwaited.some((media: any) => media.postId === id)
406 const hasTags = tagsAwaited.some((tag: any) => tag.postId === id)
407 if (hasMedia || hasTags) {
408 invalidRewoots.push(id)
409 }
410 }
411
412 const finalRewootIds = rewootedPosts.filter((r: any) => !invalidRewoots.includes(r.id)).map((r: any) => r.parentId)
413 const blockedServers = (await ServerBlock.findAll({ where: { userBlockerId: posterId } })).map(
414 (elem) => elem.blockedServerId
415 )
416 const postsMentioningUser: string[] = mentions.postMentionRelation
417 .filter((mention: any) => mention.userMentioned === posterId)
418 .map((mention: any) => mention.post)
419 const allPosts = (await postWithNotes)
420 .concat((await postWithNotes).flatMap((elem: any) => elem.ancestors))
421 .concat(await quotedPosts)
422 .map((elem: any) => (elem.dataValues ? elem.dataValues : elem))
423 const postsToFullySend = allPosts.filter((post: any) => {
424 const postIsPostedByUser = post.userId === posterId
425 const isReblog =
426 post.content === '' &&
427 !tagsAwaited.some((tag: any) => tag.postId === post.id) &&
428 !mediasAwaited.some((media: any) => media.postId === post.id)
429 const validPrivacy = [Privacy.Public, Privacy.LocalOnly, Privacy.Unlisted, Privacy.LinkOnly].includes(post.privacy)
430 const userFollowsPoster = usersFollowedByPoster.includes(post.userId) && post.privacy === Privacy.FollowersOnly
431 const userIsMentioned = postsMentioningUser.includes(post.id)
432 const posterIsInBlockedServer = blockedServers.includes(usersMap.get(post.userId)?.federatedHostId as string)
433 return (
434 !bannedUserIds.includes(post.userId) &&
435 !posterIsInBlockedServer &&
436 (postIsPostedByUser || validPrivacy || userFollowsPoster || userIsMentioned || isReblog)
437 )
438 })
439 const postIdsToFullySend: string[] = postsToFullySend
440 .filter((elem) => !blockedUsersSet.has(elem.userId))
441 .map((post: any) => post.id)
442 const postsToSendFiltered = (await postWithNotes)
443 .map((post: any) => filterPost(post, postIdsToFullySend, doNotFullyHide))
444 .filter((elem: any) => !!elem)
445 let mediasToSend = (await medias).filter((elem: any) => {
446 return postIdsToFullySend.includes(elem.postId)
447 })
448 const tagsFiltered = (await tags).filter((tag: any) => postIdsToFullySend.includes(tag.postId))
449 const quotesFiltered = quotes.filter((quote: any) => postIdsToFullySend.includes(quote.quoterPostId))
450 const pollsFiltered = (await polls).filter((poll: any) => postIdsToFullySend.includes(poll.postId))
451 // we edit posts so we add the interactionPolicies
452 let postsToSend = postsToSendFiltered
453 .filter((elem) => !!elem)
454 .map(async (elem) => addPostCanInteract(posterId, elem, usersFollowingPoster, usersFollowedByPoster, mentions))
455
456 let finalPostsToSend = await Promise.all(postsToSend)
457 const userIsAdult = isAdult(user?.birthDate)
458
459 if (!userIsAdult && user?.role !== 10) {
460 finalPostsToSend = finalPostsToSend.filter((x) => {
461 const cwToFilter = (x.content_warning || '').toLowerCase()
462 return (
463 !cwToFilter.includes('nsfw') &&
464 !cwToFilter.includes('lewd') &&
465 !cwToFilter.includes('sexual') &&
466 !cwToFilter.includes('nudity') &&
467 !cwToFilter.includes('porn')
468 )
469 })
470 mediasToSend = mediasToSend.filter((x) => !x.NSFW)
471 }
472
473 return {
474 rewootIds: finalRewootIds.filter((elem) => !!elem),
475 posts: finalPostsToSend,
476 emojiRelations: await emojis,
477 mentions: mentions.postMentionRelation.filter((elem) => !!elem),
478 users: (await users)
479 .filter((elem) => !!elem)
480 .map((x) => {
481 const pronouns = usersPronounsMap.get(x.id)
482 return {
483 ...x,
484 ...(pronouns
485 ? {
486 pronouns
487 }
488 : {})
489 }
490 }),
491 polls: pollsFiltered.filter((elem) => !!elem),
492 medias: mediasToSend.filter((elem) => !!elem),
493 tags: tagsFiltered.filter((elem) => !!elem),
494 likes: likes.filter((elem) => !!elem),
495 bookmarks: bookmarks,
496 quotes: quotesFiltered.filter((elem) => !!elem),
497 quotedPosts: (await quotedPosts)
498 .map((elem: any) => filterPost(elem, postIdsToFullySend, doNotFullyHide))
499 .filter((elem) => !!elem),
500 asks: asks.filter((elem) => !!elem)
501 }
502}
503
504function filterPost(postToBeFilter: any, postIdsToFullySend: string[], donotHide = false): any {
505 let res = postToBeFilter
506 if (!postIdsToFullySend.includes(res.id)) {
507 res = undefined
508 }
509 if (res) {
510 const ancestorsLength = res.ancestors ? res.ancestors.length : 0
511 res.ancestors = res.ancestors
512 ? res.ancestors.map((elem: any) => filterPost(elem, postIdsToFullySend, donotHide)).filter((elem: any) => !!elem)
513 : []
514 res.ancestors = res.ancestors.filter((elem: any) => !(elem == undefined))
515 if (ancestorsLength != res.ancestors.length && !donotHide) {
516 res = undefined
517 }
518 }
519
520 return res
521}
522
523// we are gona do this for likes, quotes, replies and rewoots... and we may will this function too when user interacts with a post!
524async function canInteract(
525 level: InteractionControlType,
526 userId: string,
527 postId: string,
528 userFollowersInput?: string[],
529 userFollowingInput?: string[],
530 mentionsInput?: { usersMentioned: string[]; postMentionRelation: any[] }
531): Promise<boolean> {
532 if (level == InteractionControl.Anyone) {
533 return true
534 }
535 let usersFollowing = userFollowingInput ? userFollowingInput : getFollowedsIds(userId)
536 let userFollowers = userFollowersInput
537 ? userFollowersInput
538 : getFollowedsIds(userId, false, {
539 getFollowersInstead: true
540 })
541 let mentions = mentionsInput ? mentionsInput : getMentionedUserIds([postId])
542 let post: Promise<Post | null> | Post | null = Post.findByPk(postId)
543 await Promise.all([usersFollowing, userFollowers, mentions, post])
544 usersFollowing = await usersFollowing
545 userFollowers = await userFollowers
546 mentions = await mentions
547 post = await post
548 // TMP hack
549 let res = false
550 if (post) {
551 if (post.userId == userId) {
552 return true
553 }
554 // we order the switch cases by complexity (number of conditions)
555 switch (level) {
556 case InteractionControl.NoOne: {
557 // we already check if user is from poster himself. This is a special one for bsky
558 res = false
559 break
560 }
561 case InteractionControl.Followers: {
562 res = usersFollowing.includes(post.userId)
563 break
564 }
565 case InteractionControl.Following: {
566 // post creator follows you
567 res = userFollowers.includes(post.userId)
568 break
569 }
570 case InteractionControl.MentionedUsersOnly: {
571 // post creator follows you
572 res = mentions.postMentionRelation.some((elem) => elem.post == postId && elem.userMentioned == userId)
573 break
574 }
575 case InteractionControl.FollowersAndMentioned: {
576 // post creator follows you
577 res =
578 usersFollowing.includes(post.userId) ||
579 mentions.postMentionRelation.some((elem) => elem.post == postId && elem.userMentioned == userId)
580 break
581 }
582 case InteractionControl.FollowingAndMentioned: {
583 // post creator follows you
584 res =
585 userFollowers.includes(post.userId) ||
586 mentions.postMentionRelation.some((elem) => elem.post == postId && elem.userMentioned == userId)
587 break
588 }
589 case InteractionControl.FollowersAndFollowing: {
590 // include mentioned users
591 res = userFollowers.includes(post.userId) || usersFollowing.includes(post.userId)
592 break
593 }
594 case InteractionControl.FollowersFollowingAndMentioned: {
595 res =
596 userFollowers.includes(post.userId) ||
597 usersFollowing.includes(post.userId) ||
598 mentions.postMentionRelation.some((elem) => elem.post == postId && elem.userMentioned == userId)
599 break
600 }
601 case InteractionControl.SameAsOp: {
602 // special one for bsky too
603 // ok we need to check for the initial post and to the calculations with it.
604 // we look for op post
605 const parentsIds = (
606 await sequelize.query(`SELECT DISTINCT "ancestorId" FROM "postsancestors" where "postsId" = '${post.id}'`, {
607 type: QueryTypes.SELECT
608 })
609 ).map((elem: any) => elem.ancestorId as string)
610 const originalPost = await Post.findOne({
611 where: {
612 hierarchyLevel: 1,
613 id: {
614 [Op.in]: parentsIds
615 }
616 }
617 })
618 if (!originalPost || originalPost?.id === post.id) {
619 res = false
620 } else {
621 // this will only be used for REPLIES
622 res = await canInteract(
623 originalPost.replyControl,
624 userId,
625 originalPost.id,
626 userFollowersInput,
627 userFollowingInput,
628 mentionsInput
629 )
630 }
631 }
632 }
633 }
634
635 return !!res
636}
637
638async function addPostCanInteract(
639 userId: string,
640 postInput: any,
641 userFollowersInput?: string[],
642 userFollowingInput?: string[],
643 mentionsInput?: { usersMentioned: string[]; postMentionRelation: any[] }
644): Promise<
645 Post & {
646 canReply: boolean
647 canLike: boolean
648 canReblog: boolean
649 canQuote: boolean
650 }
651> {
652 let post: any = { ...postInput }
653 let canReply = canInteract(post.replyControl, userId, post.id, userFollowersInput, userFollowingInput, mentionsInput)
654 let canLike = canInteract(post.likeControl, userId, post.id, userFollowersInput, userFollowingInput, mentionsInput)
655 let canReblog = canInteract(
656 post.reblogControl,
657 userId,
658 post.id,
659 userFollowersInput,
660 userFollowingInput,
661 mentionsInput
662 )
663 let canQuote = canInteract(post.quoteControl, userId, post.id, userFollowersInput, userFollowingInput, mentionsInput)
664
665 await Promise.all([canReblog, canReply, canQuote, canLike])
666 post.canReply = await canReply
667 post.canLike = await canLike
668 post.canReblog = await canReblog
669 post.canQuote = await canQuote
670 if (post.ancestors) {
671 post.ancestors = await Promise.all(
672 post.ancestors.map((elem: Post) =>
673 addPostCanInteract(userId, elem.dataValues, userFollowersInput, userFollowingInput, mentionsInput)
674 )
675 )
676 }
677
678 return post
679}
680
681export {
682 getUnjointedPosts,
683 getMedias,
684 getQuotes,
685 getMentionedUserIds,
686 getTags,
687 getLikes,
688 getBookmarks,
689 getEmojis,
690 addPostCanInteract,
691 canInteract
692}