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 { 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 };