unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at cache-folder-container 1083 lines 40 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 { InteractionControl, InteractionControlType, Privacy } from '../../models/post.js' 38import { getPostThreadPDSDirect, processSinglePost } from '../../atproto/utils/getAtProtoThread.js' 39import * as cheerio from 'cheerio' 40import { PostView, ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs.js' 41import { getAdminUser } from '../getAdminAndDeletedUser.js' 42import escapeHTML from 'escape-html' 43import { wait } from '../wait.js' 44import { canInteract } from '../baseQueryNew.js' 45import { getAllLocalUserIds } from '../cacheGetters/getAllLocalUserIds.js' 46 47const updateMediaDataQueue = new Queue('processRemoteMediaData', { 48 connection: completeEnvironment.bullmqConnection, 49 defaultJobOptions: { 50 removeOnComplete: true, 51 attempts: 3, 52 backoff: { 53 type: 'exponential', 54 delay: 1000 55 }, 56 removeOnFail: true 57 } 58}) 59 60async function getPostThreadRecursive( 61 user: any, 62 remotePostId: string | null, 63 remotePostObject?: any, 64 localPostToForceUpdate?: string, 65 options?: any 66) { 67 let detachedQuote = false 68 let detachedReply = false 69 let parent: Post | undefined | null 70 const replyControl: { 71 replyControl: InteractionControlType 72 likeControl: InteractionControlType 73 reblogControl: InteractionControlType 74 quoteControl: InteractionControlType 75 } = { 76 replyControl: InteractionControl.Anyone, 77 likeControl: InteractionControl.Anyone, 78 reblogControl: InteractionControl.Anyone, 79 quoteControl: InteractionControl.Anyone 80 } 81 const checkBluesky = completeEnvironment.enableBsky && !options?.forceNotBsky 82 if (remotePostId === null) return 83 84 const deletedUser = getDeletedUser() 85 try { 86 remotePostId.startsWith(`${completeEnvironment.frontendUrl}/fediverse/post/`) 87 } catch (error) { 88 logger.info({ 89 message: 'Error with url on post', 90 object: remotePostId, 91 stack: new Error().stack 92 }) 93 return 94 } 95 if (remotePostId.startsWith(`${completeEnvironment.frontendUrl}/fediverse/post/`)) { 96 // we are looking at a local post 97 const partToRemove = `${completeEnvironment.frontendUrl}/fediverse/post/` 98 const postId = remotePostId.substring(partToRemove.length) 99 return await Post.findOne({ 100 where: { 101 id: postId 102 } 103 }) 104 } 105 if (checkBluesky && remotePostId.startsWith('at://')) { 106 // Bluesky post. Likely coming from an import 107 const postInDatabase = await Post.findOne({ 108 where: { 109 bskyUri: remotePostId 110 } 111 }) 112 if (postInDatabase) { 113 return postInDatabase 114 } else if (!remotePostObject) { 115 const postId = await processSinglePost(remotePostId) 116 return await Post.findByPk(postId) 117 } 118 } 119 // fix bridgy duplicates. they are originaly bsky posts after all 120 // TODO This function has other path for this. it would be nice to clean it up 121 if ( 122 (checkBluesky && remotePostId.startsWith('https://bsky.brid.gy/')) || 123 remotePostId.startsWith('https://fed.brid.gy/r/') 124 ) { 125 // the post is a bsky one lol. 126 let uri = remotePostId.split('https://bsky.brid.gy/convert/ap/')[1] 127 if (remotePostId.startsWith('https://fed.brid.gy/r/')) { 128 const profileAndPost = remotePostId.split('/profile/')[1].split('/post/') 129 let bskyProfile = profileAndPost[0] 130 let bskyUri = profileAndPost[1] 131 uri = `at://${bskyProfile}/app.bsky.feed.post/${bskyUri}` 132 } 133 if (uri) { 134 const bskyVersionId = await processSinglePost(uri, false) 135 if (bskyVersionId) { 136 const bskyVersion = (await Post.findByPk(bskyVersionId)) as Post 137 if (!bskyVersion.remotePostId && !(await getAllLocalUserIds()).includes(bskyVersion.userId)) { 138 // we have the bsky post in the db, it is not from a local user 139 const localPostWithExistingremoteId = await Post.findOne({ 140 where: { 141 remotePostId: remotePostId 142 } 143 }) 144 if (localPostWithExistingremoteId && localPostWithExistingremoteId.id != bskyVersion.id) { 145 // OK TIME TO UPDATE WHO IS PARENT OF DESCENDENTS 146 await Post.update( 147 { 148 parentId: bskyVersion.id 149 }, 150 { 151 where: { 152 parentId: localPostWithExistingremoteId.id 153 } 154 } 155 ) 156 localPostWithExistingremoteId.remotePostId = null 157 localPostWithExistingremoteId.isDeleted = true 158 await localPostWithExistingremoteId.save() 159 } 160 bskyVersion.remotePostId = remotePostId 161 await bskyVersion.save() 162 } 163 return bskyVersion 164 } 165 } 166 } 167 const postInDatabase = await Post.findOne({ 168 where: { 169 remotePostId: remotePostId 170 } 171 }) 172 if (postInDatabase && !localPostToForceUpdate) { 173 if (postInDatabase.remotePostId) { 174 const parentPostPetition = await getPetitionSigned(user, postInDatabase.remotePostId) 175 if (parentPostPetition) { 176 await loadPoll(parentPostPetition, postInDatabase, user) 177 } 178 } 179 return postInDatabase 180 } else { 181 try { 182 const postPetition = remotePostObject ? remotePostObject : await getPetitionSigned(user, remotePostId) 183 if (postPetition && !localPostToForceUpdate) { 184 const remotePostInDatabase = await Post.findOne({ 185 where: { 186 remotePostId: postPetition.id 187 } 188 }) 189 if (remotePostInDatabase) { 190 if (remotePostInDatabase.remotePostId) { 191 const parentPostPetition = await getPetitionSigned(user, remotePostInDatabase.remotePostId) 192 if (parentPostPetition) { 193 await loadPoll(parentPostPetition, remotePostInDatabase, user) 194 } 195 } 196 return remotePostInDatabase 197 } 198 } 199 // peertube: what the fuck 200 let actorUrl = postPetition.attributedTo 201 if (Array.isArray(actorUrl)) { 202 actorUrl = actorUrl[0].id 203 } 204 const remoteUser = await getRemoteActor(actorUrl, user) 205 if (remoteUser) { 206 const remoteHost = (await FederatedHost.findByPk(remoteUser.federatedHostId as string)) as FederatedHost 207 const remoteUserServerBaned = remoteHost?.blocked ? remoteHost.blocked : false 208 // HACK: some implementations (GTS IM LOOKING AT YOU) may send a single element instead of an array 209 // I should had used a funciton instead of this dirty thing, BUT you see, its late. Im eepy 210 // also this code is CRITICAL. A failure here is a big problem. So this hack it is 211 postPetition.tag = !Array.isArray(postPetition.tag) 212 ? [postPetition.tag].filter((elem) => elem) 213 : postPetition.tag 214 const medias: any[] = [] 215 const fediTags: fediverseTag[] = [ 216 ...new Set<fediverseTag>( 217 postPetition.tag 218 ?.filter((elem: fediverseTag) => 219 [ 220 postPetition.tag.some((tag: fediverseTag) => tag.type == 'WafrnHashtag') ? 'WafrnHashtag' : 'Hashtag' 221 ].includes(elem.type) 222 ) 223 .map((elem: fediverseTag) => { 224 return { href: elem.href, type: elem.type, name: elem.name } 225 }) 226 ) 227 ] 228 const invisibleMentionsToRemove = postPetition.tag?.find((elem: fediverseTag) => elem.type === 'WafrnMentionsTextToHide') 229 let fediMentions: fediverseTag[] = postPetition.tag?.filter((elem: fediverseTag) => elem.type === 'Mention') 230 if (fediMentions == undefined) { 231 fediMentions = postPetition.to.map((elem: string) => { 232 return { href: elem } 233 }) 234 } 235 let federatedAsks: fediverseTag[] = postPetition.tag?.filter( 236 (elem: fediverseTag) => elem.type === 'AskQuestion' 237 ) 238 239 const fediEmojis: any[] = postPetition.tag?.filter((elem: fediverseTag) => elem.type === 'Emoji') 240 241 const privacy = getApObjectPrivacy(postPetition, remoteUser) 242 // part of getting the canreply stuff 243 if (postPetition.interactionPolicy) { 244 const publicList = 'https://www.w3.org/ns/activitystreams#Public' 245 const sameAsOpList = 'sameAsInitialPost' 246 // canAnnounce 247 if (postPetition.interactionPolicy.canAnnounce) { 248 const listCanAnnounce = (postPetition.interactionPolicy?.canAnnounce?.always || []).concat( 249 postPetition.interactionPolicy.canAnnounce.automaticApproval || [] 250 ) 251 replyControl.reblogControl = InteractionControl.MentionedUsersOnly 252 const followersCanReply = listCanAnnounce.includes(remoteUser.followersCollectionUrl) 253 const followingCanReply = listCanAnnounce.includes(remoteUser.followingCollectionUrl) 254 if (followersCanReply) { 255 replyControl.reblogControl = followingCanReply 256 ? InteractionControl.FollowersFollowingAndMentioned 257 : InteractionControl.FollowersAndMentioned 258 } else { 259 replyControl.reblogControl = followingCanReply 260 ? InteractionControl.FollowingAndMentioned 261 : replyControl.reblogControl 262 } 263 if (listCanAnnounce.includes(publicList)) { 264 replyControl.reblogControl = InteractionControl.Anyone 265 } 266 if (listCanAnnounce.includes(sameAsOpList)) { 267 replyControl.reblogControl = InteractionControl.SameAsOp 268 } 269 } 270 271 if (postPetition.interactionPolicy.canLike) { 272 const listCanLike = (postPetition.interactionPolicy.canLike.always || []).concat( 273 postPetition.interactionPolicy.canLike.automaticApproval || [] 274 ) 275 replyControl.likeControl = InteractionControl.MentionedUsersOnly 276 const followersCanReply = listCanLike.includes(remoteUser.followersCollectionUrl) 277 const followingCanReply = listCanLike.includes(remoteUser.followingCollectionUrl) 278 if (followersCanReply) { 279 replyControl.likeControl = followingCanReply 280 ? InteractionControl.FollowersFollowingAndMentioned 281 : InteractionControl.FollowersAndMentioned 282 } else { 283 replyControl.likeControl = followingCanReply 284 ? InteractionControl.FollowingAndMentioned 285 : replyControl.likeControl 286 } 287 if (listCanLike.includes(publicList)) { 288 replyControl.likeControl = InteractionControl.Anyone 289 } 290 if (listCanLike.includes(sameAsOpList)) { 291 replyControl.likeControl = InteractionControl.SameAsOp 292 } 293 } 294 295 if (postPetition.interactionPolicy.canReply) { 296 const listCanReply = (postPetition.interactionPolicy.canReply.always || []).concat( 297 postPetition.interactionPolicy.canReply.automaticApproval || [] 298 ) 299 replyControl.replyControl = InteractionControl.MentionedUsersOnly 300 const followersCanReply = listCanReply.includes(remoteUser.followersCollectionUrl) 301 const followingCanReply = listCanReply.includes(remoteUser.followingCollectionUrl) 302 if (followersCanReply) { 303 replyControl.replyControl = followingCanReply 304 ? InteractionControl.FollowersFollowingAndMentioned 305 : InteractionControl.FollowersAndMentioned 306 } else { 307 replyControl.replyControl = followingCanReply 308 ? InteractionControl.FollowingAndMentioned 309 : replyControl.replyControl 310 } 311 if (listCanReply.includes(publicList)) { 312 replyControl.replyControl = InteractionControl.Anyone 313 } 314 if (listCanReply.includes(sameAsOpList)) { 315 replyControl.replyControl = InteractionControl.SameAsOp 316 } 317 } 318 319 if (postPetition.interactionPolicy.canQuote) { 320 const listCanQuote = (postPetition.interactionPolicy.canQuote.always || []).concat( 321 postPetition.interactionPolicy.canQuote.automaticApproval || [] 322 ) 323 replyControl.quoteControl = InteractionControl.MentionedUsersOnly 324 const followerscanQuote = listCanQuote.includes(remoteUser.followersCollectionUrl) 325 const followingcanQuote = listCanQuote.includes(remoteUser.followingCollectionUrl) 326 if (followerscanQuote) { 327 replyControl.quoteControl = followingcanQuote 328 ? InteractionControl.FollowersFollowingAndMentioned 329 : InteractionControl.FollowersAndMentioned 330 } else { 331 replyControl.quoteControl = followingcanQuote 332 ? InteractionControl.FollowingAndMentioned 333 : replyControl.quoteControl 334 } 335 if (listCanQuote.includes(publicList)) { 336 replyControl.quoteControl = InteractionControl.Anyone 337 } 338 if (listCanQuote.includes(sameAsOpList)) { 339 replyControl.quoteControl = InteractionControl.SameAsOp 340 } 341 } 342 } 343 if (parent && parent.replyControl == InteractionControl.SameAsOp) { 344 replyControl.replyControl = InteractionControl.SameAsOp 345 } else if (parent) { 346 // we check if op has property forceDescendentsToUseSameInteractionControls 347 const opId = ( 348 parent.hierarchyLevel === 1 349 ? parent 350 : (( 351 await parent.getAncestors({ 352 where: { 353 hierarchyLevel: 1 354 } 355 }) 356 )[0] as Post) 357 ).remotePostId 358 const opPostPetition = await getPetitionSigned(user, parent.remotePostId as string) 359 if (opPostPetition && opPostPetition.forceDescendentsToUseSameInteractionControls == true) { 360 replyControl.replyControl = InteractionControl.SameAsOp 361 } 362 } 363 let postTextContent = `${postPetition.content ? postPetition.content : ''}` // Fix for bridgy giving this as undefined 364 if(invisibleMentionsToRemove && postTextContent.startsWith(invisibleMentionsToRemove.name)) { 365 postTextContent = postTextContent.substring(invisibleMentionsToRemove.name.length) 366 } 367 if (postPetition.type == 'Video') { 368 // peertube federation. We just add a link to the video, federating this is HELL 369 postTextContent = postTextContent + ` <a href="${postPetition.id}" target="_blank">${postPetition.id}</a>` 370 } 371 if (postPetition.tag && postPetition.tag.some((tag: fediverseTag) => tag.type === 'WafrnHashtag')) { 372 // Ok we have wafrn hashtags with us, we are probably talking with another wafrn! Crazy, I know 373 const dom = cheerio.load(postTextContent) 374 const tags = dom('a.hashtag').html('') 375 postTextContent = dom.html() 376 } 377 if ( 378 postPetition.attachment && 379 postPetition.attachment.length > 0 && 380 (!remoteUser.banned || options?.allowMediaFromBanned) 381 ) { 382 for await (const remoteFile of postPetition.attachment) { 383 if (remoteFile.type !== 'Link') { 384 const wafrnMedia = await Media.create({ 385 url: remoteFile.url, 386 NSFW: postPetition?.sensitive, 387 userId: remoteUserServerBaned || remoteUser.banned ? (await deletedUser)?.id : remoteUser.id, 388 description: remoteFile.name, 389 ipUpload: 'IMAGE_FROM_OTHER_FEDIVERSE_INSTANCE', 390 mediaOrder: postPetition.attachment.indexOf(remoteFile), // could be non consecutive but its ok 391 external: true, 392 mediaType: remoteFile.mediaType ? remoteFile.mediaType : '', 393 blurhash: remoteFile.blurhash ? remoteFile.blurhash : null, 394 height: remoteFile.height ? remoteFile.height : null, 395 width: remoteFile.width ? remoteFile.width : null 396 }) 397 if (!wafrnMedia.mediaType || (wafrnMedia.mediaType?.startsWith('image') && !wafrnMedia.width)) { 398 await updateMediaDataQueue.add(`updateMedia:${wafrnMedia.id}`, { 399 mediaId: wafrnMedia.id 400 }) 401 } 402 medias.push(wafrnMedia) 403 } else { 404 postTextContent = '' + postTextContent + `<a href="${remoteFile.href}" >${remoteFile.href}</a>` 405 } 406 } 407 } 408 const lemmyName = postPetition.name ? postPetition.name : '' 409 postTextContent = postTextContent ? postTextContent : `<p>${lemmyName}</p>` 410 let createdAt = new Date(postPetition.published) 411 if (createdAt.getTime() > new Date().getTime()) { 412 createdAt = new Date() 413 } 414 415 let bskyUri: string | undefined, bskyCid: string | undefined 416 let existingBskyPost: Post | undefined 417 // check if it's a bridgy post or a post from a wafrn by checking a valid FEP-fffd 418 if (postPetition.url && Array.isArray(postPetition.url)) { 419 const url = postPetition.url as Array<string | { type: string; href: string }> 420 const firstFffd = url.find((x) => typeof x !== 'string') 421 // check if it starts at at:// then its a bridged post, we do not touch it if it's not 422 if (checkBluesky && firstFffd && firstFffd.href.startsWith('at://')) { 423 // get it's bsky counterparts first, we need the cid 424 const postBskyVersionId = await processSinglePost(firstFffd.href) 425 const postBskyVersion = postBskyVersionId ? await Post.findByPk(postBskyVersionId) : undefined 426 if (postBskyVersion) { 427 bskyCid = postBskyVersion.bskyCid || undefined 428 bskyUri = postBskyVersion.bskyUri || undefined 429 const directPetition = await getPostThreadPDSDirect(bskyUri as string) 430 if (directPetition.value.fediverseId) { 431 // This is a wafrn post 432 // first we going to check if the post is already on db because this can break everything 433 const existingFedi = await Post.findOne({ 434 where: { 435 remotePostId: postPetition.id 436 } 437 }) 438 if (existingFedi && existingFedi.id != postBskyVersion.id) { 439 existingFedi.remotePostId = null 440 await Post.update( 441 { 442 parentId: postBskyVersion.id 443 }, 444 { 445 where: { 446 parentId: existingFedi.id 447 } 448 } 449 ) 450 await existingFedi.save() 451 } 452 if (!postBskyVersion.remotePostId) { 453 postBskyVersion.remotePostId = postPetition.id 454 await postBskyVersion.save() 455 } 456 if(!localPostToForceUpdate) { 457 return postBskyVersion 458 } 459 } else { 460 postBskyVersion.remotePostId = postPetition.id 461 const existingFedi = await Post.findOne({ 462 where: { 463 remotePostId: postPetition.id 464 } 465 }) 466 if (existingFedi && postBskyVersion.id != existingFedi.id && existingFedi.remotePostId) { 467 if (existingFedi.remotePostId.startsWith('https://bsky.brid.gy/')) { 468 // the real post is the bsky one 469 existingFedi.remotePostId = null 470 existingFedi.isDeleted = true 471 await Post.update( 472 { 473 parentId: postBskyVersion.id 474 }, 475 { 476 where: { 477 parentId: existingFedi.id 478 } 479 } 480 ) 481 await existingFedi.save() 482 postBskyVersion.remotePostId = existingFedi.remotePostId 483 await postBskyVersion.save() 484 return postBskyVersion 485 } else { 486 // the real post is fedi one 487 existingFedi.bskyCid = postBskyVersion.bskyCid 488 existingFedi.bskyUri = postBskyVersion.bskyUri 489 postBskyVersion.bskyCid = null 490 postBskyVersion.bskyUri = null 491 postBskyVersion.isDeleted = true 492 await postBskyVersion.save() 493 await Post.update( 494 { 495 parentId: existingFedi.id 496 }, 497 { 498 where: { 499 parentId: postBskyVersion.id 500 } 501 } 502 ) 503 await existingFedi.save() 504 return existingFedi 505 } 506 } 507 return postBskyVersion 508 } 509 } else { 510 if (!options.ignoreBridgyRepeat) { 511 const processSinglePostQueue = new Queue('processSinglePost', { 512 connection: completeEnvironment.bullmqConnection, 513 defaultJobOptions: { 514 removeOnComplete: true, 515 attempts: 6, 516 backoff: { 517 type: 'exponential', 518 delay: 2500 519 }, 520 removeOnFail: false 521 } 522 }) 523 processSinglePostQueue.add('processSinglePost', { post: firstFffd.href, forceUpdate: false }) 524 const processFediPostQueue = new Queue('processFediPostQueue', { 525 connection: completeEnvironment.bullmqConnection, 526 defaultJobOptions: { 527 removeOnComplete: true, 528 attempts: 6, 529 backoff: { 530 type: 'exponential', 531 delay: 2500 532 }, 533 removeOnFail: false 534 } 535 }) 536 processFediPostQueue.add('processSinglePost', { post: remotePostId }, { delay: 1000 }) 537 } 538 } 539 } 540 } 541 542 const postToCreate: any = { 543 content: '' + postTextContent, 544 content_warning: postPetition.summary 545 ? postPetition.summary 546 : remoteUser.NSFW 547 ? 'User is marked as NSFW by this instance staff. Possible NSFW without tagging' 548 : '', 549 createdAt: createdAt, 550 updatedAt: createdAt, 551 userId: remoteUserServerBaned || remoteUser.banned ? (await deletedUser)?.id : remoteUser.id, 552 remotePostId: postPetition.id, 553 privacy: privacy, 554 bskyUri: postPetition.blueskyUri, 555 displayUrl: Array.isArray(postPetition.url) ? postPetition.url[0] : postPetition.url, 556 bskyCid: postPetition.blueskyCid, 557 ...(bskyCid && bskyUri 558 ? { 559 bskyCid, 560 bskyUri 561 } 562 : {}), 563 ...replyControl 564 } 565 566 if (postPetition.name) { 567 postToCreate.title = postPetition.name 568 } 569 570 const mentionedUsersIds: string[] = [] 571 const quotes: any[] = [] 572 try { 573 if (!remoteUser.banned && !remoteUserServerBaned) { 574 for await (const mention of fediMentions) { 575 let mentionedUser 576 if (mention.href?.indexOf(completeEnvironment.frontendUrl) !== -1) { 577 const username = mention.href?.substring( 578 `${completeEnvironment.frontendUrl}/fediverse/blog/`.length 579 ) as string 580 mentionedUser = await User.findOne({ 581 where: sequelize.where(sequelize.fn('lower', sequelize.col('url')), username.toLowerCase()) 582 }) 583 } else { 584 mentionedUser = await getRemoteActor(mention.href, user) 585 } 586 if ( 587 mentionedUser?.id && 588 mentionedUser.id != (await deletedUser)?.id && 589 !mentionedUsersIds.includes(mentionedUser.id) 590 ) { 591 mentionedUsersIds.push(mentionedUser.id) 592 } 593 } 594 } 595 } catch (error) { 596 logger.info({ message: 'problem processing mentions', error }) 597 } 598 599 if (postPetition.inReplyTo && postPetition.id !== postPetition.inReplyTo) { 600 parent = await getPostThreadRecursive( 601 user, 602 postPetition.inReplyTo.id ? postPetition.inReplyTo.id : postPetition.inReplyTo 603 ) 604 postToCreate.parentId = parent?.id 605 } 606 607 const existingPost = localPostToForceUpdate ? await Post.findByPk(localPostToForceUpdate) : undefined 608 609 if (existingPost) { 610 existingPost.set(postToCreate) 611 await existingPost.save() 612 await loadPoll(postPetition, existingPost, user) 613 } 614 615 const newPost = existingPost ? existingPost : await Post.create(postToCreate) 616 try { 617 if (!remoteUser.banned && !remoteUserServerBaned && fediEmojis) { 618 processEmojis(newPost, fediEmojis) 619 } 620 } catch (error) { 621 logger.debug('Problem processing emojis') 622 } 623 newPost.setMedias(medias) 624 try { 625 if (postPetition.quote || postPetition.quoteUrl || postPetition.tag?.filter((elem: fediverseTag) => elem.type === 'BskyQuote')?.length) { 626 const urlQuote = postPetition.quoteUrl || postPetition.quote 627 const postToQuote = await getPostThreadRecursive(user, urlQuote) 628 if (postToQuote && postToQuote.privacy != Privacy.DirectMessage) { 629 quotes.push(postToQuote) 630 } 631 if (!postToQuote) { 632 postToCreate.content = postToCreate.content + `<p>RE: ${urlQuote}</p>` 633 } 634 const postsToQuotePromise: any[] = [] 635 if (completeEnvironment.enableBsky) { 636 postPetition.tag 637 ?.filter((elem: fediverseTag) => elem.type === 'BskyQuote') 638 .forEach((quote: fediverseTag) => { 639 640 postsToQuotePromise.push(processSinglePost(quote.href as string)) 641 postToCreate.content = postToCreate.content.replace(quote.name, '') 642 }) 643 } 644 postPetition.tag 645 ?.filter((elem: fediverseTag) => elem.type === 'Link') 646 .forEach((quote: fediverseTag) => { 647 postsToQuotePromise.push(getPostThreadRecursive(user, quote.href as string)) 648 postToCreate.content = postToCreate.content.replace(quote.name, '') 649 }) 650 const quotesToAdd = await Promise.allSettled(postsToQuotePromise) 651 const quotesThatWillGetAdded = quotesToAdd.filter( 652 (elem) => elem.status === 'fulfilled' && elem.value && elem.value.privacy !== 10 653 ) 654 quotesThatWillGetAdded.forEach((quot) => { 655 if (quot.status === 'fulfilled' && !quotes.map((q) => q.id).includes(quot.value.id)) { 656 quotes.push(quot.value) 657 } 658 }) 659 } 660 } catch (error) { 661 logger.info('Error processing quotes') 662 logger.debug(error) 663 } 664 newPost.setQuoted(quotes) 665 666 try { 667 if (federatedAsks && federatedAsks.length) { 668 await Ask.destroy({ 669 where: { 670 postId: newPost.id 671 } 672 }) 673 const askTag = federatedAsks[0] // only first ask sorryyy 674 if (askTag.actor && askTag.representation && askTag.name) { 675 const asker = askTag.actor != 'anonymous' ? await getRemoteActor(askTag.actor, user) : undefined 676 const askText = askTag.name 677 const htmlToRemove = askTag.representation 678 await Ask.create({ 679 postId: newPost.id, 680 userAsked: newPost.userId, 681 userAsker: asker?.id, 682 question: escapeHTML(askText) 683 }) 684 newPost.content = newPost.content.replace(htmlToRemove, '') 685 } 686 } 687 } catch (error) { 688 logger.info({ 689 message: `Error setting wafrn ask`, 690 error: error 691 }) 692 } 693 694 await newPost.save() 695 const postsBeingQuotedIds = quotes.map((elem) => elem.quotedPostId) 696 const postsQuoteds = await Post.findAll({ 697 where: { 698 id: { 699 [Op.in]: postsBeingQuotedIds 700 } 701 } 702 }) 703 detachedQuote = postsQuoteds.some( 704 async (elem) => !(await canInteract(elem.quoteControl, newPost.userId, elem.id)) 705 ) 706 await bulkCreateNotifications( 707 quotes.map((quote) => ({ 708 notificationType: 'QUOTE', 709 notifiedUserId: quote.userId, 710 userId: newPost.userId, 711 postId: newPost.id, 712 createdAt: new Date(newPost.createdAt), 713 detached: detachedQuote 714 })), 715 { 716 postContent: newPost.content, 717 userUrl: remoteUser.url 718 } 719 ) 720 try { 721 if (!remoteUser.banned && !remoteUserServerBaned) { 722 await addTagsToPost(newPost, fediTags) 723 } 724 } catch (error) { 725 logger.info('problem processing tags') 726 } 727 try { 728 await addAsksToPost(newPost, fediTags) 729 } catch (error) {} 730 if (mentionedUsersIds.length != 0) { 731 // check if detached 732 if (parent?.detached) { 733 detachedReply = true 734 } 735 if (!detachedReply && parent && (await getAllLocalUserIds()).includes(parent.userId)) { 736 detachedReply = !(await canInteract(parent.replyControl, newPost.userId, parent.id)) 737 } 738 if (detachedReply) { 739 newPost.detached = true 740 await newPost.save() 741 } 742 await processMentions(newPost, mentionedUsersIds, detachedReply) 743 } 744 await loadPoll(remotePostObject, newPost, user) 745 const postCleanContent = dompurify.sanitize(newPost.content, { ALLOWED_TAGS: [] }).trim() 746 const mentions = await newPost.getMentionPost() 747 if (postCleanContent.startsWith('!ask') && mentions.length === 1) { 748 let askContent = postCleanContent.split(`!ask @${mentions[0].url}`)[1] 749 if (askContent.startsWith('@' + completeEnvironment.instanceUrl)) { 750 askContent = askContent.split('@' + completeEnvironment.instanceUrl)[1] 751 } 752 await Ask.create({ 753 question: escapeHTML(askContent), 754 userAsker: newPost.userId, 755 userAsked: mentions[0].id, 756 answered: false, 757 apObject: JSON.stringify(postPetition) 758 }) 759 } 760 761 if (existingBskyPost) { 762 // very expensive updates! but only happens when bsky 763 // post is already on db but the fedi post is not 764 await EmojiReaction.update( 765 { 766 postId: newPost.id 767 }, 768 { 769 where: { 770 postId: existingBskyPost.id 771 } 772 } 773 ) 774 await Notification.update( 775 { 776 postId: newPost.id 777 }, 778 { 779 where: { 780 postId: existingBskyPost.id 781 } 782 } 783 ) 784 await PostReport.update( 785 { 786 postId: newPost.id 787 }, 788 { 789 where: { 790 postId: existingBskyPost.id 791 } 792 } 793 ) 794 try { 795 await PostAncestor.update( 796 { 797 postsId: newPost.id 798 }, 799 { 800 where: { 801 postsId: existingBskyPost.id 802 } 803 } 804 ) 805 } catch {} 806 await QuestionPoll.update( 807 { 808 postId: newPost.id 809 }, 810 { 811 where: { 812 postId: existingBskyPost.id 813 } 814 } 815 ) 816 await Quotes.update( 817 { 818 quoterPostId: newPost.id 819 }, 820 { 821 where: { 822 quoterPostId: existingBskyPost.id 823 } 824 } 825 ) 826 if ( 827 !(await Quotes.findOne({ 828 where: { 829 quotedPostId: newPost.id 830 } 831 })) 832 ) { 833 await Quotes.update( 834 { 835 quotedPostId: newPost.id 836 }, 837 { 838 where: { 839 quotedPostId: existingBskyPost.id 840 } 841 } 842 ) 843 } 844 await RemoteUserPostView.update( 845 { 846 postId: newPost.id 847 }, 848 { 849 where: { 850 postId: existingBskyPost.id 851 } 852 } 853 ) 854 await SilencedPost.update( 855 { 856 postId: newPost.id 857 }, 858 { 859 where: { 860 postId: existingBskyPost.id 861 } 862 } 863 ) 864 await SilencedPost.update( 865 { 866 postId: newPost.id 867 }, 868 { 869 where: { 870 postId: existingBskyPost.id 871 } 872 } 873 ) 874 await UserBitesPostRelation.update( 875 { 876 postId: newPost.id 877 }, 878 { 879 where: { 880 postId: existingBskyPost.id 881 } 882 } 883 ) 884 await UserBookmarkedPosts.update( 885 { 886 postId: newPost.id 887 }, 888 { 889 where: { 890 postId: existingBskyPost.id 891 } 892 } 893 ) 894 await UserLikesPostRelations.update( 895 { 896 postId: newPost.id 897 }, 898 { 899 where: { 900 postId: existingBskyPost.id 901 } 902 } 903 ) 904 await Post.update( 905 { 906 parentId: newPost.id 907 }, 908 { 909 where: { 910 parentId: existingBskyPost.id 911 } 912 } 913 ) 914 915 // now we delete the existing bsky post 916 await existingBskyPost.destroy() 917 918 // THEN we merge it 919 newPost.bskyCid = existingBskyPost.bskyCid 920 newPost.bskyUri = existingBskyPost.bskyUri 921 await newPost.save() 922 } 923 924 return newPost 925 } 926 } catch (error) { 927 logger.trace({ 928 message: 'error getting remote post', 929 url: remotePostId, 930 user: user.url, 931 problem: error 932 }) 933 return null 934 } 935 } 936} 937 938async function addAsksToPost(post: Post, tags: fediverseTag[]) { 939 const asks = tags.filter((elem) => elem.type === 'AskQuestion') 940 if (asks.length) { 941 const ask = asks[0] 942 const userAsker = await getRemoteActor(ask.actor as string, await getAdminUser()) 943 const textToRemove = ask.representation as string 944 const askText = ask.name 945 if (textToRemove) { 946 post.content = post.content.replace(textToRemove, '') 947 await Ask.create({ 948 answered: true, 949 postId: post.id, 950 userAsker: userAsker ? userAsker.id : undefined, 951 userAsked: post.userId 952 }) 953 await post.save() 954 } 955 } 956} 957 958async function addTagsToPost(post: any, originalTags: fediverseTag[]) { 959 let tags = [...originalTags] 960 const res = await post.setPostTags([]) 961 if (tags.some((elem) => elem.name == 'WafrnHashtag')) { 962 tags = tags.filter((elem) => elem.name == 'WafrnHashtag') 963 } 964 return await PostTag.bulkCreate( 965 tags 966 .filter((elem) => elem && post && elem.name && post.id) 967 .map((elem) => { 968 return { 969 tagName: elem?.name?.replace('#', ''), 970 postId: post.id 971 } 972 }) 973 ) 974} 975 976async function processMentions(post: any, userIds: string[], detached: boolean) { 977 await post.setMentionPost([]) 978 await Notification.destroy({ 979 where: { 980 notificationType: 'MENTION', 981 postId: post.id 982 } 983 }) 984 const blocks = await Blocks.findAll({ 985 where: { 986 blockerId: { 987 [Op.in]: userIds 988 }, 989 blockedId: post.userId 990 } 991 }) 992 const remoteUser = await User.findByPk(post.userId, { 993 attributes: ['url', 'federatedHostId'] 994 }) 995 const userServerBlocks = await ServerBlock.findAll({ 996 where: { 997 userBlockerId: { 998 [Op.in]: userIds 999 }, 1000 blockedServerId: remoteUser?.federatedHostId || '' 1001 } 1002 }) 1003 const blockerIds: string[] = blocks 1004 .map((block: any) => block.blockerId) 1005 .concat(userServerBlocks.map((elem: any) => elem.userBlockerId)) 1006 1007 await bulkCreateNotifications( 1008 userIds.map((mentionedUserId) => ({ 1009 notificationType: 'MENTION', 1010 notifiedUserId: mentionedUserId, 1011 userId: post.userId, 1012 postId: post.id, 1013 createdAt: new Date(post.createdAt), 1014 detached: detached 1015 })), 1016 { 1017 postContent: post.content, 1018 userUrl: remoteUser?.url 1019 } 1020 ) 1021 1022 return await PostMentionsUserRelation.bulkCreate( 1023 userIds 1024 .filter((elem) => !blockerIds.includes(elem)) 1025 .map((elem) => { 1026 return { 1027 postId: post.id, 1028 userId: elem 1029 } 1030 }), 1031 { 1032 ignoreDuplicates: true 1033 } 1034 ) 1035} 1036 1037async function processEmojis(post: any, fediEmojis: any[]) { 1038 let emojis: any[] = [] 1039 let res: any 1040 const emojiIds: string[] = Array.from(new Set(fediEmojis.map((emoji: any) => emoji.id))) 1041 const foundEmojis = await Emoji.findAll({ 1042 where: { 1043 id: { 1044 [Op.in]: emojiIds 1045 } 1046 } 1047 }) 1048 foundEmojis.forEach((emoji: any) => { 1049 const newData = fediEmojis.find((foundEmoji: any) => foundEmoji.id === emoji.id) 1050 if (newData && newData.icon?.url) { 1051 emoji.set({ 1052 url: newData.icon.url 1053 }) 1054 emoji.save() 1055 } else { 1056 logger.debug('issue with emoji') 1057 logger.debug(emoji) 1058 logger.debug(newData) 1059 } 1060 }) 1061 emojis = emojis.concat(foundEmojis) 1062 const notFoundEmojis = fediEmojis.filter((elem: any) => !foundEmojis.find((found: any) => found.id === elem.id)) 1063 if (fediEmojis && notFoundEmojis && notFoundEmojis.length > 0) { 1064 try { 1065 const newEmojis = notFoundEmojis.map((newEmoji: any) => { 1066 return { 1067 id: newEmoji.id ? newEmoji.id : newEmoji.name + newEmoji.icon?.url, 1068 name: newEmoji.name, 1069 external: true, 1070 url: newEmoji.icon?.url 1071 } 1072 }) 1073 emojis = emojis.concat(await Emoji.bulkCreate(newEmojis, { ignoreDuplicates: true })) 1074 } catch (error) { 1075 logger.debug('Error with emojis') 1076 logger.debug(error) 1077 } 1078 } 1079 1080 return await post.setEmojis(emojis) 1081} 1082 1083export { getPostThreadRecursive }