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