unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 785 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 "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};