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