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 "isBot"
365 ],
366 where: {
367 id: {
368 [Op.in]: userIds,
369 },
370 },
371 });
372 const usersMap: Map<string, User> = new Map();
373 for (const usr of await users) {
374 usersMap.set(usr.id, usr);
375 }
376 const postWithNotes = getPosstGroupDetails(posts);
377 await Promise.all([emojis, users, polls, medias, tags, postWithNotes]);
378 const hostsIds = (await users)
379 .filter((elem) => elem.federatedHostId)
380 .map((elem) => elem.federatedHostId);
381 const blockedHosts = await FederatedHost.findAll({
382 where: {
383 id: {
384 [Op.in]: hostsIds as string[],
385 },
386 blocked: true,
387 },
388 });
389 const blockedHostsIds = blockedHosts.map((elem) => elem.id);
390 let blockedUsersSet: Set<string> = new Set();
391 const blockedUsersQuery = await Blocks.findAll({
392 where: {
393 [Op.or]: [
394 {
395 blockerId: posterId,
396 },
397 {
398 blockedId: posterId,
399 },
400 ],
401 },
402 });
403 for (const block of blockedUsersQuery) {
404 blockedUsersSet.add(block.blockedId);
405 blockedUsersSet.add(block.blockerId);
406 }
407 blockedUsersSet.delete(posterId);
408 const bannedUserIds = (await users)
409 .filter(
410 (elem) =>
411 elem.banned ||
412 (elem.federatedHostId && blockedHostsIds.includes(elem.federatedHostId))
413 )
414 .map((elem) => elem.id);
415 let usersFollowedByPoster: string[] | Promise<string[]> =
416 getFollowedsIds(posterId);
417 let usersFollowingPoster: string[] | Promise<string[]> = getFollowedsIds(
418 posterId,
419 false,
420 {
421 getFollowersInstead: true,
422 }
423 );
424
425 await Promise.all([
426 usersFollowedByPoster,
427 usersFollowingPoster,
428 tags,
429 medias,
430 ]);
431 usersFollowedByPoster = await usersFollowedByPoster;
432 usersFollowingPoster = await usersFollowingPoster;
433 const tagsAwaited = await tags;
434 const mediasAwaited = await medias;
435
436 const invalidRewoots = [] as string[];
437 for (const id of rewootIds) {
438 const hasMedia = mediasAwaited.some((media: any) => media.postId === id);
439 const hasTags = tagsAwaited.some((tag: any) => tag.postId === id);
440 if (hasMedia || hasTags) {
441 invalidRewoots.push(id);
442 }
443 }
444
445 const finalRewootIds = rewootedPosts
446 .filter((r: any) => !invalidRewoots.includes(r.id))
447 .map((r: any) => r.parentId);
448 const blockedServers = (
449 await ServerBlock.findAll({ where: { userBlockerId: posterId } })
450 ).map((elem) => elem.blockedServerId);
451 const postsMentioningUser: string[] = mentions.postMentionRelation
452 .filter((mention: any) => mention.userMentioned === posterId)
453 .map((mention: any) => mention.post);
454 const allPosts = (await postWithNotes)
455 .concat((await postWithNotes).flatMap((elem: any) => elem.ancestors))
456 .concat(await quotedPosts)
457 .map((elem: any) => (elem.dataValues ? elem.dataValues : elem));
458 const postsToFullySend = allPosts.filter((post: any) => {
459 const postIsPostedByUser = post.userId === posterId;
460 const isReblog =
461 post.content === "" &&
462 !tagsAwaited.some((tag: any) => tag.postId === post.id) &&
463 !mediasAwaited.some((media: any) => media.postId === post.id);
464 const validPrivacy = [
465 Privacy.Public,
466 Privacy.LocalOnly,
467 Privacy.Unlisted,
468 Privacy.LinkOnly,
469 ].includes(post.privacy);
470 const userFollowsPoster =
471 usersFollowedByPoster.includes(post.userId) &&
472 post.privacy === Privacy.FollowersOnly;
473 const userIsMentioned = postsMentioningUser.includes(post.id);
474 const posterIsInBlockedServer = blockedServers.includes(
475 usersMap.get(post.userId)?.federatedHostId as string
476 );
477 return (
478 !bannedUserIds.includes(post.userId) &&
479 !posterIsInBlockedServer &&
480 (postIsPostedByUser ||
481 validPrivacy ||
482 userFollowsPoster ||
483 userIsMentioned ||
484 isReblog)
485 );
486 });
487 const postIdsToFullySend: string[] = postsToFullySend
488 .filter((elem) => !blockedUsersSet.has(elem.userId))
489 .map((post: any) => post.id);
490 const postsToSendFiltered = (await postWithNotes)
491 .map((post: any) => filterPost(post, postIdsToFullySend, doNotFullyHide))
492 .filter((elem: any) => !!elem);
493 let mediasToSend = (await medias).filter((elem: any) => {
494 return postIdsToFullySend.includes(elem.postId);
495 });
496 const tagsFiltered = (await tags).filter((tag: any) =>
497 postIdsToFullySend.includes(tag.postId)
498 );
499 const quotesFiltered = quotes.filter((quote: any) =>
500 postIdsToFullySend.includes(quote.quoterPostId)
501 );
502 const pollsFiltered = (await polls).filter((poll: any) =>
503 postIdsToFullySend.includes(poll.postId)
504 );
505 // we edit posts so we add the interactionPolicies
506 let postsToSend = postsToSendFiltered
507 .filter((elem) => !!elem)
508 .map(async (elem) =>
509 addPostCanInteract(
510 posterId,
511 elem,
512 usersFollowingPoster,
513 usersFollowedByPoster,
514 mentions
515 )
516 );
517
518 let finalPostsToSend = await Promise.all(postsToSend);
519 const userIsAdult = isAdult(user?.birthDate);
520
521 if (!userIsAdult && user?.role !== 10) {
522 finalPostsToSend = finalPostsToSend.filter((x) => {
523 const cwToFilter = (x.content_warning || "").toLowerCase();
524 return (
525 !cwToFilter.includes("nsfw") &&
526 !cwToFilter.includes("lewd") &&
527 !cwToFilter.includes("sexual") &&
528 !cwToFilter.includes("nudity") &&
529 !cwToFilter.includes("porn")
530 );
531 });
532 mediasToSend = mediasToSend.filter((x) => !x.NSFW);
533 }
534
535 return {
536 rewootIds: finalRewootIds.filter((elem) => !!elem),
537 posts: finalPostsToSend,
538 emojiRelations: await emojis,
539 mentions: mentions.postMentionRelation.filter((elem) => !!elem),
540 users: (await users).filter((elem) => !!elem),
541 polls: pollsFiltered.filter((elem) => !!elem),
542 medias: mediasToSend.filter((elem) => !!elem),
543 tags: tagsFiltered.filter((elem) => !!elem),
544 likes: likes.filter((elem) => !!elem),
545 bookmarks: bookmarks,
546 quotes: quotesFiltered.filter((elem) => !!elem),
547 quotedPosts: (await quotedPosts)
548 .map((elem: any) => filterPost(elem, postIdsToFullySend, doNotFullyHide))
549 .filter((elem) => !!elem),
550 asks: asks.filter((elem) => !!elem),
551 };
552}
553
554function filterPost(
555 postToBeFilter: any,
556 postIdsToFullySend: string[],
557 donotHide = false
558): any {
559 let res = postToBeFilter;
560 if (!postIdsToFullySend.includes(res.id)) {
561 res = undefined;
562 }
563 if (res) {
564 const ancestorsLength = res.ancestors ? res.ancestors.length : 0;
565 res.ancestors = res.ancestors
566 ? res.ancestors
567 .map((elem: any) => filterPost(elem, postIdsToFullySend, donotHide))
568 .filter((elem: any) => !!elem)
569 : [];
570 res.ancestors = res.ancestors.filter((elem: any) => !(elem == undefined));
571 if (ancestorsLength != res.ancestors.length && !donotHide) {
572 res = undefined;
573 }
574 }
575
576 return res;
577}
578
579// we are gona do this for likes, quotes, replies and rewoots... and we may will this function too when user interacts with a post!
580async function canInteract(
581 level: InteractionControlType,
582 userId: string,
583 postId: string,
584 userFollowersInput?: string[],
585 userFollowingInput?: string[],
586 mentionsInput?: { usersMentioned: string[]; postMentionRelation: any[] }
587): Promise<boolean> {
588 if (level == InteractionControl.Anyone) {
589 return true;
590 }
591 let usersFollowing = userFollowingInput
592 ? userFollowingInput
593 : getFollowedsIds(userId);
594 let userFollowers = userFollowersInput
595 ? userFollowersInput
596 : getFollowedsIds(userId, false, {
597 getFollowersInstead: true,
598 });
599 let mentions = mentionsInput ? mentionsInput : getMentionedUserIds([postId]);
600 let post: Promise<Post | null> | Post | null = Post.findByPk(postId);
601 await Promise.all([usersFollowing, userFollowers, mentions, post]);
602 usersFollowing = await usersFollowing;
603 userFollowers = await userFollowers;
604 mentions = await mentions;
605 post = await post;
606 // TMP hack
607 let res = false;
608 if (post) {
609 if (post.userId == userId) {
610 return true;
611 }
612 switch (level) {
613 case InteractionControl.Anyone: {
614 res = false;
615 break;
616 }
617 case InteractionControl.Followers: {
618 res = usersFollowing.includes(post.userId);
619 break;
620 }
621 case InteractionControl.Following: {
622 // post creator follows you
623 res = userFollowers.includes(post.userId);
624 break;
625 }
626 case InteractionControl.FollowersAndMentioned: {
627 // post creator follows you
628 res =
629 usersFollowing.includes(post.userId) ||
630 mentions.postMentionRelation.find(
631 (elem) => elem.postId == postId && elem.userId == userId
632 );
633 break;
634 }
635 case InteractionControl.FollowingAndMentioned: {
636 // post creator follows you
637 res =
638 userFollowers.includes(post.userId) ||
639 mentions.postMentionRelation.find(
640 (elem) => elem.postId == postId && elem.userId == userId
641 );
642 break;
643 }
644 case InteractionControl.FollowersFollowersAndMentioned: {
645 res =
646 userFollowers.includes(post.userId) ||
647 userFollowingInput?.includes(post.userId) ||
648 mentions.postMentionRelation.find(
649 (elem) => elem.postId == postId && elem.userId == userId
650 );
651 break;
652 }
653 case InteractionControl.MentionedUsersOnly: {
654 // post creator follows you
655 res = mentions.postMentionRelation.find(
656 (elem) => elem.postId == postId && elem.userId == userId
657 );
658 break;
659 }
660 case InteractionControl.NoOne: {
661 // we already check if user is from poster himself. This is a special one for bsky
662 res = false;
663 break;
664 }
665 case InteractionControl.SameAsOp: {
666 // special one for bsky too
667 // ok we need to check for the initial post and to the calculations with it.
668 // we look for op post
669 const parentsIds = (
670 await sequelize.query(
671 `SELECT DISTINCT "ancestorId" FROM "postsancestors" where "postsId" = '${post.id}'`,
672 {
673 type: QueryTypes.SELECT,
674 }
675 )
676 ).map((elem: any) => elem.ancestorId as string);
677 const originalPost = await Post.findOne({
678 where: {
679 hierarchyLevel: 1,
680 id: {
681 [Op.in]: parentsIds,
682 },
683 },
684 });
685 if (!originalPost || originalPost?.id === post.id) {
686 return res;
687 } else {
688 // this will only be used for REPLIES
689 res = await canInteract(
690 originalPost.replyControl,
691 userId,
692 originalPost.id,
693 userFollowersInput,
694 userFollowingInput,
695 mentionsInput
696 );
697 }
698 }
699 }
700 }
701
702 return res;
703}
704
705async function addPostCanInteract(
706 userId: string,
707 postInput: any,
708 userFollowersInput?: string[],
709 userFollowingInput?: string[],
710 mentionsInput?: { usersMentioned: string[]; postMentionRelation: any[] }
711): Promise<
712 Post & {
713 canReply: boolean;
714 canLike: boolean;
715 canReblog: boolean;
716 canQuote: boolean;
717 }
718> {
719 let post: any = { ...postInput };
720 let canReply = canInteract(
721 post.replyControl,
722 userId,
723 post.id,
724 userFollowersInput,
725 userFollowingInput,
726 mentionsInput
727 );
728 let canLike = canInteract(
729 post.likeControl,
730 userId,
731 post.id,
732 userFollowersInput,
733 userFollowingInput,
734 mentionsInput
735 );
736 let canReblog = canInteract(
737 post.reblogControl,
738 userId,
739 post.id,
740 userFollowersInput,
741 userFollowingInput,
742 mentionsInput
743 );
744 let canQuote = canInteract(
745 post.quoteControl,
746 userId,
747 post.id,
748 userFollowersInput,
749 userFollowingInput,
750 mentionsInput
751 );
752
753 await Promise.all([canReblog, canReply, canQuote, canLike]);
754 post.canReply = await canReply;
755 post.canLike = await canLike;
756 post.canReblog = await canReblog;
757 post.canQuote = await canQuote;
758 if (post.ancestors) {
759 post.ancestors = await Promise.all(
760 post.ancestors.map((elem: Post) =>
761 addPostCanInteract(
762 userId,
763 elem.dataValues,
764 userFollowersInput,
765 userFollowingInput,
766 mentionsInput
767 )
768 )
769 );
770 }
771
772 return post;
773}
774
775export {
776 getUnjointedPosts,
777 getMedias,
778 getQuotes,
779 getMentionedUserIds,
780 getTags,
781 getLikes,
782 getBookmarks,
783 getEmojis,
784 addPostCanInteract,
785};