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