unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
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 }