unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at virtualscroll 784 lines 21 kB view raw
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};