unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at virtualscroll 831 lines 26 kB view raw
1import { Op } from "sequelize"; 2import { 3 Blocks, 4 Emoji, 5 FederatedHost, 6 Media, 7 Post, 8 PostMentionsUserRelation, 9 ServerBlock, 10 PostTag, 11 User, 12 sequelize, 13 Ask, 14 Notification, 15 EmojiReaction, 16 PostAncestor, 17 PostReport, 18 QuestionPoll, 19 Quotes, 20 RemoteUserPostView, 21 SilencedPost, 22 UserBitesPostRelation, 23 UserBookmarkedPosts, 24 UserLikesPostRelations, 25} from "../../models/index.js"; 26import { completeEnvironment } from "../backendOptions.js"; 27import { logger } from "../logger.js"; 28import { getRemoteActor } from "./getRemoteActor.js"; 29import { getPetitionSigned } from "./getPetitionSigned.js"; 30import { fediverseTag } from "../../interfaces/fediverse/tags.js"; 31import { loadPoll } from "./loadPollFromPost.js"; 32import { getApObjectPrivacy } from "./getPrivacy.js"; 33import dompurify from "isomorphic-dompurify"; 34import { Queue } from "bullmq"; 35import { bulkCreateNotifications } from "../pushNotifications.js"; 36import { getDeletedUser } from "../cacheGetters/getDeletedUser.js"; 37import { Privacy } from "../../models/post.js"; 38import { 39 getAtProtoThread, 40 getPostThreadSafe, 41 processSinglePost, 42} from "../../atproto/utils/getAtProtoThread.js"; 43import * as cheerio from "cheerio"; 44import { 45 PostView, 46 ThreadViewPost, 47} from "@atproto/api/dist/client/types/app/bsky/feed/defs.js"; 48 49const updateMediaDataQueue = new Queue("processRemoteMediaData", { 50 connection: completeEnvironment.bullmqConnection, 51 defaultJobOptions: { 52 removeOnComplete: true, 53 attempts: 3, 54 backoff: { 55 type: "exponential", 56 delay: 1000, 57 }, 58 removeOnFail: true, 59 }, 60}); 61 62async function getPostThreadRecursive( 63 user: any, 64 remotePostId: string | null, 65 remotePostObject?: any, 66 localPostToForceUpdate?: string, 67 options?: any 68) { 69 if (remotePostId === null) return; 70 71 const deletedUser = getDeletedUser(); 72 try { 73 remotePostId.startsWith( 74 `${completeEnvironment.frontendUrl}/fediverse/post/` 75 ); 76 } catch (error) { 77 logger.info({ 78 message: "Error with url on post", 79 object: remotePostId, 80 stack: new Error().stack, 81 }); 82 return; 83 } 84 if ( 85 remotePostId.startsWith( 86 `${completeEnvironment.frontendUrl}/fediverse/post/` 87 ) 88 ) { 89 // we are looking at a local post 90 const partToRemove = `${completeEnvironment.frontendUrl}/fediverse/post/`; 91 const postId = remotePostId.substring(partToRemove.length); 92 return await Post.findOne({ 93 where: { 94 id: postId, 95 }, 96 }); 97 } 98 if (completeEnvironment.enableBsky && remotePostId.startsWith("at://")) { 99 // Bluesky post. Likely coming from an import 100 const postInDatabase = await Post.findOne({ 101 where: { 102 bskyUri: remotePostId, 103 }, 104 }); 105 if (postInDatabase) { 106 return postInDatabase; 107 } else if (!remotePostObject) { 108 const postId = await getAtProtoThread(remotePostId); 109 return await Post.findByPk(postId); 110 } 111 } 112 const postInDatabase = await Post.findOne({ 113 where: { 114 remotePostId: remotePostId, 115 }, 116 }); 117 if (postInDatabase && !localPostToForceUpdate) { 118 if (postInDatabase.remotePostId) { 119 const parentPostPetition = await getPetitionSigned( 120 user, 121 postInDatabase.remotePostId 122 ); 123 if (parentPostPetition) { 124 await loadPoll(parentPostPetition, postInDatabase, user); 125 } 126 } 127 return postInDatabase; 128 } else { 129 try { 130 const postPetition = remotePostObject 131 ? remotePostObject 132 : await getPetitionSigned(user, remotePostId); 133 if (postPetition && !localPostToForceUpdate) { 134 const remotePostInDatabase = await Post.findOne({ 135 where: { 136 remotePostId: postPetition.id, 137 }, 138 }); 139 if (remotePostInDatabase) { 140 if (remotePostInDatabase.remotePostId) { 141 const parentPostPetition = await getPetitionSigned( 142 user, 143 remotePostInDatabase.remotePostId 144 ); 145 if (parentPostPetition) { 146 await loadPoll(parentPostPetition, remotePostInDatabase, user); 147 } 148 } 149 return remotePostInDatabase; 150 } 151 } 152 // peertube: what the fuck 153 let actorUrl = postPetition.attributedTo; 154 if (Array.isArray(actorUrl)) { 155 actorUrl = actorUrl[0].id; 156 } 157 const remoteUser = await getRemoteActor(actorUrl, user); 158 if (remoteUser) { 159 const remoteHost = (await FederatedHost.findByPk( 160 remoteUser.federatedHostId as string 161 )) as FederatedHost; 162 const remoteUserServerBaned = remoteHost?.blocked 163 ? remoteHost.blocked 164 : false; 165 // HACK: some implementations (GTS IM LOOKING AT YOU) may send a single element instead of an array 166 // I should had used a funciton instead of this dirty thing, BUT you see, its late. Im eepy 167 // also this code is CRITICAL. A failure here is a big problem. So this hack it is 168 postPetition.tag = !Array.isArray(postPetition.tag) 169 ? [postPetition.tag].filter((elem) => elem) 170 : postPetition.tag; 171 const medias: any[] = []; 172 const fediTags: fediverseTag[] = [ 173 ...new Set<fediverseTag>( 174 postPetition.tag 175 ?.filter((elem: fediverseTag) => 176 [ 177 postPetition.tag.some( 178 (tag: fediverseTag) => tag.type == "WafrnHashtag" 179 ) 180 ? "WafrnHashtag" 181 : "Hashtag", 182 ].includes(elem.type) 183 ) 184 .map((elem: fediverseTag) => { 185 return { href: elem.href, type: elem.type, name: elem.name }; 186 }) 187 ), 188 ]; 189 let fediMentions: fediverseTag[] = postPetition.tag?.filter( 190 (elem: fediverseTag) => elem.type === "Mention" 191 ); 192 if (fediMentions == undefined) { 193 fediMentions = postPetition.to.map((elem: string) => { 194 return { href: elem }; 195 }); 196 } 197 const fediEmojis: any[] = postPetition.tag?.filter( 198 (elem: fediverseTag) => elem.type === "Emoji" 199 ); 200 201 const privacy = getApObjectPrivacy(postPetition, remoteUser); 202 203 let postTextContent = `${ 204 postPetition.content ? postPetition.content : "" 205 }`; // Fix for bridgy giving this as undefined 206 if (postPetition.type == "Video") { 207 // peertube federation. We just add a link to the video, federating this is HELL 208 postTextContent = 209 postTextContent + 210 ` <a href="${postPetition.id}" target="_blank">${postPetition.id}</a>`; 211 } 212 if ( 213 postPetition.tag && 214 postPetition.tag.some( 215 (tag: fediverseTag) => tag.type === "WafrnHashtag" 216 ) 217 ) { 218 // Ok we have wafrn hashtags with us, we are probably talking with another wafrn! Crazy, I know 219 const dom = cheerio.load(postTextContent); 220 const tags = dom("a.hashtag").html(""); 221 postTextContent = dom.html(); 222 } 223 if ( 224 postPetition.attachment && 225 postPetition.attachment.length > 0 && 226 (!remoteUser.banned || options?.allowMediaFromBanned) 227 ) { 228 for await (const remoteFile of postPetition.attachment) { 229 if (remoteFile.type !== "Link") { 230 const wafrnMedia = await Media.create({ 231 url: remoteFile.url, 232 NSFW: postPetition?.sensitive, 233 userId: 234 remoteUserServerBaned || remoteUser.banned 235 ? ( 236 await deletedUser 237 )?.id 238 : remoteUser.id, 239 description: remoteFile.name, 240 ipUpload: "IMAGE_FROM_OTHER_FEDIVERSE_INSTANCE", 241 mediaOrder: postPetition.attachment.indexOf(remoteFile), // could be non consecutive but its ok 242 external: true, 243 mediaType: remoteFile.mediaType ? remoteFile.mediaType : "", 244 blurhash: remoteFile.blurhash ? remoteFile.blurhash : null, 245 height: remoteFile.height ? remoteFile.height : null, 246 width: remoteFile.width ? remoteFile.width : null, 247 }); 248 if ( 249 !wafrnMedia.mediaType || 250 (wafrnMedia.mediaType?.startsWith("image") && !wafrnMedia.width) 251 ) { 252 await updateMediaDataQueue.add(`updateMedia:${wafrnMedia.id}`, { 253 mediaId: wafrnMedia.id, 254 }); 255 } 256 medias.push(wafrnMedia); 257 } else { 258 postTextContent = 259 "" + 260 postTextContent + 261 `<a href="${remoteFile.href}" >${remoteFile.href}</a>`; 262 } 263 } 264 } 265 const lemmyName = postPetition.name ? postPetition.name : ""; 266 postTextContent = postTextContent 267 ? postTextContent 268 : `<p>${lemmyName}</p>`; 269 let createdAt = new Date(postPetition.published); 270 if (createdAt.getTime() > new Date().getTime()) { 271 createdAt = new Date(); 272 } 273 274 let bskyUri: string | undefined, bskyCid: string | undefined; 275 let existingBskyPost: Post | undefined; 276 // check if it's a bridgy post or a post from a wafrn by checking a valid FEP-fffd 277 if (postPetition.url && Array.isArray(postPetition.url)) { 278 const url = postPetition.url as Array< 279 string | { type: string; href: string } 280 >; 281 const firstFffd = url.find((x) => typeof x !== "string"); 282 // check if it starts at at:// then its a bridged post, we do not touch it if it's not 283 if (firstFffd && firstFffd.href.startsWith("at://")) { 284 // get it's bsky counterparts first, we need the cid 285 const thread = await getPostThreadSafe({ 286 uri: firstFffd.href, 287 }); 288 if (thread && thread.success) { 289 try { 290 const threadView = thread.data.thread as ThreadViewPost; 291 bskyCid = threadView.post.cid; 292 bskyUri = threadView.post.uri; 293 // check if it cames from wafrn 294 if ( 295 !( 296 threadView.post.record as { 297 fediverseId: string | undefined; 298 } 299 ).fediverseId 300 ) { 301 // this is a bridgy fed post, assume main post is on bsky, use bsky user 302 const postId = await processSinglePost(threadView.post); 303 if (postId) { 304 const post = await Post.findByPk(postId); 305 if (post) { 306 post.remotePostId = postPetition.id; 307 await post.save(); 308 return post; 309 } 310 } 311 } else { 312 // now this is a wafrn post, where we do a thing little bit different 313 // first we going to check if the post is already on db because this can break everything 314 let existingPost = await Post.findOne({ 315 where: { 316 bskyCid: threadView.post.cid, 317 remotePostId: null, 318 }, 319 }); 320 if (existingPost) { 321 existingBskyPost = existingPost; 322 // do not attempt to merge it right now, this will crash backend 323 bskyCid = undefined; 324 bskyUri = undefined; 325 } 326 } 327 } catch {} 328 } 329 } 330 } 331 332 const postToCreate: any = { 333 content: "" + postTextContent, 334 content_warning: postPetition.summary 335 ? postPetition.summary 336 : remoteUser.NSFW 337 ? "User is marked as NSFW by this instance staff. Possible NSFW without tagging" 338 : "", 339 createdAt: createdAt, 340 updatedAt: createdAt, 341 userId: 342 remoteUserServerBaned || remoteUser.banned 343 ? (await deletedUser)?.id 344 : remoteUser.id, 345 remotePostId: postPetition.id, 346 privacy: privacy, 347 bskyUri: postPetition.blueskyUri, 348 bskyCid: postPetition.blueskyCid, 349 ...(bskyCid && bskyUri 350 ? { 351 bskyCid, 352 bskyUri, 353 } 354 : {}), 355 }; 356 357 if (postPetition.name) { 358 postToCreate.title = postPetition.name; 359 } 360 361 const mentionedUsersIds: string[] = []; 362 const quotes: any[] = []; 363 try { 364 if (!remoteUser.banned && !remoteUserServerBaned) { 365 for await (const mention of fediMentions) { 366 let mentionedUser; 367 if ( 368 mention.href?.indexOf(completeEnvironment.frontendUrl) !== -1 369 ) { 370 const username = mention.href?.substring( 371 `${completeEnvironment.frontendUrl}/fediverse/blog/`.length 372 ) as string; 373 mentionedUser = await User.findOne({ 374 where: sequelize.where( 375 sequelize.fn("lower", sequelize.col("url")), 376 username.toLowerCase() 377 ), 378 }); 379 } else { 380 mentionedUser = await getRemoteActor(mention.href, user); 381 } 382 if ( 383 mentionedUser?.id && 384 mentionedUser.id != (await deletedUser)?.id && 385 !mentionedUsersIds.includes(mentionedUser.id) 386 ) { 387 mentionedUsersIds.push(mentionedUser.id); 388 } 389 } 390 } 391 } catch (error) { 392 logger.info({ message: "problem processing mentions", error }); 393 } 394 395 if ( 396 postPetition.inReplyTo && 397 postPetition.id !== postPetition.inReplyTo 398 ) { 399 const parent = await getPostThreadRecursive( 400 user, 401 postPetition.inReplyTo.id 402 ? postPetition.inReplyTo.id 403 : postPetition.inReplyTo 404 ); 405 postToCreate.parentId = parent?.id; 406 } 407 408 const existingPost = localPostToForceUpdate 409 ? await Post.findByPk(localPostToForceUpdate) 410 : undefined; 411 412 if (existingPost) { 413 existingPost.set(postToCreate); 414 await existingPost.save(); 415 await loadPoll(postPetition, existingPost, user); 416 } 417 418 const newPost = existingPost 419 ? existingPost 420 : await Post.create(postToCreate); 421 try { 422 if (!remoteUser.banned && !remoteUserServerBaned && fediEmojis) { 423 processEmojis(newPost, fediEmojis); 424 } 425 } catch (error) { 426 logger.debug("Problem processing emojis"); 427 } 428 newPost.setMedias(medias); 429 try { 430 if (postPetition.quote || postPetition.quoteUrl) { 431 const urlQuote = postPetition.quoteUrl || postPetition.quote; 432 const postToQuote = await getPostThreadRecursive(user, urlQuote); 433 if (postToQuote && postToQuote.privacy != Privacy.DirectMessage) { 434 quotes.push(postToQuote); 435 } 436 if (!postToQuote) { 437 postToCreate.content = 438 postToCreate.content + `<p>RE: ${urlQuote}</p>`; 439 } 440 const postsToQuotePromise: any[] = []; 441 postPetition.tag 442 ?.filter((elem: fediverseTag) => elem.type === "Link") 443 .forEach((quote: fediverseTag) => { 444 postsToQuotePromise.push( 445 getPostThreadRecursive(user, quote.href as string) 446 ); 447 postToCreate.content = postToCreate.content.replace( 448 quote.name, 449 "" 450 ); 451 }); 452 const quotesToAdd = await Promise.allSettled(postsToQuotePromise); 453 const quotesThatWillGetAdded = quotesToAdd.filter( 454 (elem) => 455 elem.status === "fulfilled" && 456 elem.value && 457 elem.value.privacy !== 10 458 ); 459 quotesThatWillGetAdded.forEach((quot) => { 460 if ( 461 quot.status === "fulfilled" && 462 !quotes.map((q) => q.id).includes(quot.value.id) 463 ) { 464 quotes.push(quot.value); 465 } 466 }); 467 } 468 } catch (error) { 469 logger.info("Error processing quotes"); 470 logger.debug(error); 471 } 472 newPost.setQuoted(quotes); 473 474 await newPost.save(); 475 476 await bulkCreateNotifications( 477 quotes.map((quote) => ({ 478 notificationType: "QUOTE", 479 notifiedUserId: quote.userId, 480 userId: newPost.userId, 481 postId: newPost.id, 482 createdAt: new Date(newPost.createdAt), 483 })), 484 { 485 postContent: newPost.content, 486 userUrl: remoteUser.url, 487 } 488 ); 489 try { 490 if (!remoteUser.banned && !remoteUserServerBaned) { 491 await addTagsToPost(newPost, fediTags); 492 } 493 } catch (error) { 494 logger.info("problem processing tags"); 495 } 496 if (mentionedUsersIds.length != 0) { 497 await processMentions(newPost, mentionedUsersIds); 498 } 499 await loadPoll(remotePostObject, newPost, user); 500 const postCleanContent = dompurify 501 .sanitize(newPost.content, { ALLOWED_TAGS: [] }) 502 .trim(); 503 const mentions = await newPost.getMentionPost(); 504 if (postCleanContent.startsWith("!ask") && mentions.length === 1) { 505 let askContent = postCleanContent.split( 506 `!ask @${mentions[0].url}` 507 )[1]; 508 if (askContent.startsWith("@" + completeEnvironment.instanceUrl)) { 509 askContent = askContent.split( 510 "@" + completeEnvironment.instanceUrl 511 )[1]; 512 } 513 await Ask.create({ 514 question: askContent, 515 userAsker: newPost.userId, 516 userAsked: mentions[0].id, 517 answered: false, 518 apObject: JSON.stringify(postPetition), 519 }); 520 } 521 522 if (existingBskyPost) { 523 // very expensive updates! but only happens when bsky 524 // post is already on db but the fedi post is not 525 await EmojiReaction.update( 526 { 527 postId: newPost.id, 528 }, 529 { 530 where: { 531 postId: existingBskyPost.id, 532 }, 533 } 534 ); 535 await Notification.update( 536 { 537 postId: newPost.id, 538 }, 539 { 540 where: { 541 postId: existingBskyPost.id, 542 }, 543 } 544 ); 545 await PostReport.update( 546 { 547 postId: newPost.id, 548 }, 549 { 550 where: { 551 postId: existingBskyPost.id, 552 }, 553 } 554 ); 555 try { 556 await PostAncestor.update( 557 { 558 postsId: newPost.id, 559 }, 560 { 561 where: { 562 postsId: existingBskyPost.id, 563 }, 564 } 565 ); 566 } catch {} 567 await QuestionPoll.update( 568 { 569 postId: newPost.id, 570 }, 571 { 572 where: { 573 postId: existingBskyPost.id, 574 }, 575 } 576 ); 577 await Quotes.update( 578 { 579 quoterPostId: newPost.id, 580 }, 581 { 582 where: { 583 quoterPostId: existingBskyPost.id, 584 }, 585 } 586 ); 587 if ( 588 !(await Quotes.findOne({ 589 where: { 590 quotedPostId: newPost.id, 591 }, 592 })) 593 ) { 594 await Quotes.update( 595 { 596 quotedPostId: newPost.id, 597 }, 598 { 599 where: { 600 quotedPostId: existingBskyPost.id, 601 }, 602 } 603 ); 604 } 605 await RemoteUserPostView.update( 606 { 607 postId: newPost.id, 608 }, 609 { 610 where: { 611 postId: existingBskyPost.id, 612 }, 613 } 614 ); 615 await SilencedPost.update( 616 { 617 postId: newPost.id, 618 }, 619 { 620 where: { 621 postId: existingBskyPost.id, 622 }, 623 } 624 ); 625 await SilencedPost.update( 626 { 627 postId: newPost.id, 628 }, 629 { 630 where: { 631 postId: existingBskyPost.id, 632 }, 633 } 634 ); 635 await UserBitesPostRelation.update( 636 { 637 postId: newPost.id, 638 }, 639 { 640 where: { 641 postId: existingBskyPost.id, 642 }, 643 } 644 ); 645 await UserBookmarkedPosts.update( 646 { 647 postId: newPost.id, 648 }, 649 { 650 where: { 651 postId: existingBskyPost.id, 652 }, 653 } 654 ); 655 await UserLikesPostRelations.update( 656 { 657 postId: newPost.id, 658 }, 659 { 660 where: { 661 postId: existingBskyPost.id, 662 }, 663 } 664 ); 665 await Post.update( 666 { 667 parentId: newPost.id, 668 }, 669 { 670 where: { 671 parentId: existingBskyPost.id, 672 }, 673 } 674 ); 675 676 // now we delete the existing bsky post 677 await existingBskyPost.destroy(); 678 679 // THEN we merge it 680 newPost.bskyCid = existingBskyPost.bskyCid; 681 newPost.bskyUri = existingBskyPost.bskyUri; 682 await newPost.save(); 683 } 684 685 return newPost; 686 } 687 } catch (error) { 688 logger.trace({ 689 message: "error getting remote post", 690 url: remotePostId, 691 user: user.url, 692 problem: error, 693 }); 694 return null; 695 } 696 } 697} 698 699async function addTagsToPost(post: any, originalTags: fediverseTag[]) { 700 let tags = [...originalTags]; 701 const res = await post.setPostTags([]); 702 if (tags.some((elem) => elem.name == "WafrnHashtag")) { 703 tags = tags.filter((elem) => elem.name == "WafrnHashtag"); 704 } 705 return await PostTag.bulkCreate( 706 tags 707 .filter((elem) => elem && post && elem.name && post.id) 708 .map((elem) => { 709 return { 710 tagName: elem?.name?.replace("#", ""), 711 postId: post.id, 712 }; 713 }) 714 ); 715} 716 717async function processMentions(post: any, userIds: string[]) { 718 await post.setMentionPost([]); 719 await Notification.destroy({ 720 where: { 721 notificationType: "MENTION", 722 postId: post.id, 723 }, 724 }); 725 const blocks = await Blocks.findAll({ 726 where: { 727 blockerId: { 728 [Op.in]: userIds, 729 }, 730 blockedId: post.userId, 731 }, 732 }); 733 const remoteUser = await User.findByPk(post.userId, { 734 attributes: ["url", "federatedHostId"], 735 }); 736 const userServerBlocks = await ServerBlock.findAll({ 737 where: { 738 userBlockerId: { 739 [Op.in]: userIds, 740 }, 741 blockedServerId: remoteUser?.federatedHostId || "", 742 }, 743 }); 744 const blockerIds: string[] = blocks 745 .map((block: any) => block.blockerId) 746 .concat(userServerBlocks.map((elem: any) => elem.userBlockerId)); 747 748 await bulkCreateNotifications( 749 userIds.map((mentionedUserId) => ({ 750 notificationType: "MENTION", 751 notifiedUserId: mentionedUserId, 752 userId: post.userId, 753 postId: post.id, 754 createdAt: new Date(post.createdAt), 755 })), 756 { 757 postContent: post.content, 758 userUrl: remoteUser?.url, 759 } 760 ); 761 762 return await PostMentionsUserRelation.bulkCreate( 763 userIds 764 .filter((elem) => !blockerIds.includes(elem)) 765 .map((elem) => { 766 return { 767 postId: post.id, 768 userId: elem, 769 }; 770 }), 771 { 772 ignoreDuplicates: true, 773 } 774 ); 775} 776 777async function processEmojis(post: any, fediEmojis: any[]) { 778 let emojis: any[] = []; 779 let res: any; 780 const emojiIds: string[] = Array.from( 781 new Set(fediEmojis.map((emoji: any) => emoji.id)) 782 ); 783 const foundEmojis = await Emoji.findAll({ 784 where: { 785 id: { 786 [Op.in]: emojiIds, 787 }, 788 }, 789 }); 790 foundEmojis.forEach((emoji: any) => { 791 const newData = fediEmojis.find( 792 (foundEmoji: any) => foundEmoji.id === emoji.id 793 ); 794 if (newData && newData.icon?.url) { 795 emoji.set({ 796 url: newData.icon.url, 797 }); 798 emoji.save(); 799 } else { 800 logger.debug("issue with emoji"); 801 logger.debug(emoji); 802 logger.debug(newData); 803 } 804 }); 805 emojis = emojis.concat(foundEmojis); 806 const notFoundEmojis = fediEmojis.filter( 807 (elem: any) => !foundEmojis.find((found: any) => found.id === elem.id) 808 ); 809 if (fediEmojis && notFoundEmojis && notFoundEmojis.length > 0) { 810 try { 811 const newEmojis = notFoundEmojis.map((newEmoji: any) => { 812 return { 813 id: newEmoji.id ? newEmoji.id : newEmoji.name + newEmoji.icon?.url, 814 name: newEmoji.name, 815 external: true, 816 url: newEmoji.icon?.url, 817 }; 818 }); 819 emojis = emojis.concat( 820 await Emoji.bulkCreate(newEmojis, { ignoreDuplicates: true }) 821 ); 822 } catch (error) { 823 logger.debug("Error with emojis"); 824 logger.debug(error); 825 } 826 } 827 828 return await post.setEmojis(emojis); 829} 830 831export { getPostThreadRecursive };