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