unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at testPDSNotExplode 336 lines 12 kB view raw
1import { Op } from 'sequelize' 2import { Media, Post, PostTag, sequelize, User } from '../../models/index.js' 3import { completeEnvironment } from '../backendOptions.js' 4import { fediverseTag } from '../../interfaces/fediverse/tags.js' 5import { activityPubObject } from '../../interfaces/fediverse/activityPubObject.js' 6import { emojiToAPTag } from './emojiToAPTag.js' 7import { getPostReplies } from './getPostReplies.js' 8import { getPostAndUserFromPostId } from '../cacheGetters/getPostAndUserFromPostId.js' 9import { logger } from '../logger.js' 10import { Privacy } from '../../models/post.js' 11import { redisCache } from '../redis.js' 12 13async function postToJSONLD(postId: string): Promise<activityPubObject | undefined> { 14 let resFromCacheString = await redisCache.get('postToJsonLD:' + postId) 15 if (resFromCacheString) { 16 return JSON.parse(resFromCacheString) as activityPubObject 17 } 18 const cacheData = await getPostAndUserFromPostId(postId) 19 const post = cacheData.data 20 const localUser = post.user 21 const userAsker = post.ask?.asker 22 const ask = post.ask 23 24 const stringMyFollowers = `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}/followers` 25 const dbMentions = post.mentionPost 26 let mentionedUsers: string[] = [] 27 28 if (dbMentions) { 29 mentionedUsers = dbMentions.filter((elem: any) => elem.remoteInbox).map((elem: any) => elem.remoteId) 30 } 31 let parentPostString = null 32 let quotedPostString = null 33 const conversationString = `${completeEnvironment.frontendUrl}/fediverse/conversation/${post.id}` 34 35 if (post.parentId) { 36 let dbPost = (await getPostAndUserFromPostId(post.parentId)).data 37 38 const ancestorIdsQuery = await sequelize.query( 39 `SELECT "ancestorId" FROM "postsancestors" where "postsId" = '${post.parentId}'` 40 ) 41 let ancestors: Post[] = [] 42 const ancestorIds: string[] = ancestorIdsQuery[0].map((elem: any) => elem.ancestorId) 43 if (ancestorIds.length > 0) { 44 ancestors = await Post.findAll({ 45 include: [ 46 { 47 model: User, 48 as: 'user', 49 attributes: ['url'] 50 } 51 ], 52 where: { 53 id: { 54 [Op.in]: ancestorIds 55 } 56 }, 57 order: [['createdAt', 'DESC']] 58 }) 59 if (post.bskyDid) { 60 // we do same check for all parents 61 62 const parentsUsers = ancestors.map((elem) => elem.user) 63 if (parentsUsers.some((elem) => elem.isBlueskyUser)) { 64 return undefined 65 } 66 } 67 } 68 for await (const ancestor of ancestors) { 69 if ( 70 dbPost && 71 dbPost.content === '' && 72 dbPost.hierarchyLevel !== 0 && 73 dbPost.postTags.length == 0 && 74 dbPost.medias.length == 0 && 75 dbPost.quoted.length == 0 && // fix this this is still dirty 76 dbPost.content_warning.length == 0 77 ) { 78 // TODO optimize this. 79 // yes this is still optimizable but we are no longer using a while that could infinite loop 80 // and also there are some checks in this function. so its ok ish 81 // but still 82 dbPost = (await getPostAndUserFromPostId(ancestor.id)).data 83 } else { 84 break 85 } 86 } 87 parentPostString = dbPost?.remotePostId 88 ? dbPost.remotePostId 89 : `${completeEnvironment.frontendUrl}/fediverse/post/${dbPost ? dbPost.id : post.parentId}` 90 } 91 const postMedias = await post.medias 92 let processedContent = post.content 93 const wafrnMediaRegex = 94 /\[wafrnmediaid="[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}"\]/gm 95 96 // we remove the wafrnmedia from the post for the outside world, as they get this on the attachments 97 processedContent = processedContent.replaceAll(wafrnMediaRegex, '') 98 if (ask) { 99 processedContent = `<p>${getUserName(userAsker)} <a href="${completeEnvironment.frontendUrl + '/fediverse/post/' + post.id 100 }">asked</a> </p> <blockquote>${ask.question}</blockquote> ${processedContent}` 101 } 102 const mentions: string[] = post.mentionPost.map((elem: any) => elem.id) 103 const fediMentions: fediverseTag[] = [] 104 const fediTags: fediverseTag[] = [] 105 let tagsAndQuotes = '<br>' 106 const quotedPosts = post.quoted 107 if (quotedPosts && quotedPosts.length > 0) { 108 const mainQuotedPost = quotedPosts[0] 109 quotedPostString = getPostUrlForQuote(mainQuotedPost) 110 quotedPosts.forEach((quotedPost: any) => { 111 const postUrl = getPostUrlForQuote(quotedPost) 112 tagsAndQuotes = tagsAndQuotes + `<br>RE: <a href="${postUrl}">${postUrl}</a><br>` 113 if (!postUrl.startsWith('https://bsky.app/')) { 114 fediTags.push({ 115 type: 'Link', 116 mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 117 name: `RE: ${postUrl}`, 118 href: postUrl 119 }) 120 } 121 }) 122 } 123 for await (const tag of post.postTags) { 124 const externalTagName = tag.tagName.replaceAll('"', "'").replaceAll(' ', '-') 125 const link = `${completeEnvironment.frontendUrl}/dashboard/search/${encodeURIComponent(tag.tagName)}` 126 tagsAndQuotes = `${tagsAndQuotes}<small><a class="hashtag" data-tag="post" href="${link}" rel="tag ugc">#${externalTagName}</a></small> ` 127 fediTags.push({ 128 type: 'Hashtag', 129 name: `#${externalTagName}`, 130 href: link 131 }) 132 fediTags.push({ 133 type: 'WafrnHashtag', 134 href: link, 135 name: tag.tagName.replaceAll('"', "'") 136 }) 137 } 138 for await (const userId of mentions) { 139 const user = 140 (await User.findOne({ where: { id: userId } })) || 141 ((await User.findOne({ where: { url: completeEnvironment.deletedUser } })) as User) 142 const url = user.fullHandle 143 const remoteId = user.fullFediverseUrl 144 if (remoteId) { 145 fediMentions.push({ 146 type: 'Mention', 147 name: url, 148 href: remoteId 149 }) 150 } 151 } 152 153 let contentWarning = false 154 postMedias.forEach((media: any) => { 155 if (media.NSFW) { 156 contentWarning = true 157 } 158 }) 159 160 const emojis = post.emojis 161 162 if (ask) { 163 fediTags.push({ 164 type: 'AskQuestion', 165 name: ask.question, 166 actor: userAsker 167 ? userAsker.remoteId 168 ? userAsker.remoteId 169 : completeEnvironment.frontendUrl + '/fediverse/blog/' + userAsker.url 170 : 'anonymous' 171 }) 172 } 173 174 const lineBreaksAtEndRegex = /\s*(<br\s*\/?>)+\s*$/g 175 176 const usersToSend = getToAndCC(post.privacy, mentionedUsers, stringMyFollowers) 177 const actorUrl = `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}` 178 let misskeyQuoteURL = quotedPostString 179 if (misskeyQuoteURL?.startsWith('https://bsky.app/')) { 180 misskeyQuoteURL = null 181 } 182 let postAsJSONLD: activityPubObject = { 183 '@context': [ 184 'https://www.w3.org/ns/activitystreams', 185 `${completeEnvironment.frontendUrl}/contexts/litepub-0.1.jsonld` 186 ], 187 id: `${completeEnvironment.frontendUrl}/fediverse/activity/post/${post.id}`, 188 type: 'Create', 189 actor: actorUrl, 190 published: new Date(post.createdAt).toISOString(), 191 to: usersToSend.to, 192 cc: usersToSend.cc, 193 object: { 194 id: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 195 actor: actorUrl, 196 type: 'Note', 197 summary: post.content_warning ? post.content_warning : '', 198 inReplyTo: parentPostString, 199 published: new Date(post.createdAt).toISOString(), 200 updated: new Date(post.updatedAt).toISOString(), 201 url: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 202 attributedTo: `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}`, 203 to: usersToSend.to, 204 cc: usersToSend.cc, 205 sensitive: !!post.content_warning || contentWarning, 206 atomUri: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 207 inReplyToAtomUri: parentPostString, 208 quote: misskeyQuoteURL, 209 quoteUrl: misskeyQuoteURL, 210 _misksey_quote: misskeyQuoteURL, 211 quoteUri: misskeyQuoteURL, 212 // conversation: conversationString, 213 content: (processedContent + tagsAndQuotes).replace(lineBreaksAtEndRegex, ''), 214 attachment: postMedias 215 ?.sort((a: Media, b: Media) => a.mediaOrder - b.mediaOrder) 216 .map((media: Media) => { 217 const extension = media.url.split('.')[media.url.split('.').length - 1].toLowerCase() 218 return { 219 type: 'Document', 220 mediaType: media.mediaType, 221 url: media.external ? media.url : completeEnvironment.mediaUrl + media.url, 222 sensitive: media.NSFW ? true : false, 223 name: media.description 224 } 225 }), 226 tag: fediMentions.concat(fediTags).concat(emojis.map((emoji: any) => emojiToAPTag(emoji))), 227 replies: { 228 id: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}/replies`, 229 type: 'Collection', 230 first: { 231 type: 'CollectionPage', 232 partOf: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}/replies`, 233 next: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}/replies?page=1`, 234 items: [] 235 } 236 } 237 } 238 } 239 const newObject: any = {} 240 const objKeys = Object.keys(postAsJSONLD.object) 241 objKeys.forEach((key) => { 242 if (postAsJSONLD.object[key]) { 243 newObject[key] = postAsJSONLD.object[key] 244 } 245 }) 246 postAsJSONLD.object = newObject 247 if ( 248 post.content === '' && 249 post.postTags.length === 0 && 250 post.medias.length === 0 && 251 post.quoted.length === 0 && 252 post.content_warning == 0 253 ) { 254 postAsJSONLD = { 255 '@context': 'https://www.w3.org/ns/activitystreams', 256 id: `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 257 type: 'Announce', 258 actor: `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}`, 259 published: new Date(post.createdAt).toISOString(), 260 to: 261 post.privacy / 1 === Privacy.DirectMessage 262 ? mentionedUsers 263 : post.privacy / 1 === Privacy.Public 264 ? ['https://www.w3.org/ns/activitystreams#Public'] 265 : [stringMyFollowers], 266 cc: [`${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}`, stringMyFollowers], 267 object: parentPostString 268 } 269 } 270 await redisCache.set('postToJsonLD:' + postId, JSON.stringify(postAsJSONLD), 'EX', 300) 271 return postAsJSONLD 272} 273 274function getToAndCC( 275 privacy: number, 276 mentionedUsers: string[], 277 stringMyFollowers: string 278): { to: string[]; cc: string[] } { 279 let to: string[] = [] 280 let cc: string[] = [] 281 switch (privacy) { 282 case 0: { 283 to = ['https://www.w3.org/ns/activitystreams#Public', stringMyFollowers, ...mentionedUsers] 284 cc = mentionedUsers 285 break 286 } 287 case 1: { 288 to = [stringMyFollowers, ...mentionedUsers] 289 cc = [] 290 break 291 } 292 case 3: { 293 to = [stringMyFollowers, ...mentionedUsers] 294 cc = ['https://www.w3.org/ns/activitystreams#Public'] 295 break 296 } 297 default: { 298 ; (to = mentionedUsers), (cc = []) 299 } 300 } 301 return { 302 to, 303 cc 304 } 305} 306 307// stolen I mean inspired by https://stackoverflow.com/questions/2970525/converting-a-string-with-spaces-into-camel-case 308function camelize(str: string): string { 309 return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { 310 if (+match === 0) return '' // or if (/\s+/.test(match)) for white spaces 311 return index === 0 ? match.toLowerCase() : match.toUpperCase() 312 }) 313} 314 315function getUserName(user?: { url: string }): string { 316 let res = user ? '@' + user.url + '@' + completeEnvironment.instanceUrl : 'anonymous' 317 if (user?.url.startsWith('@')) { 318 res = user.url 319 } 320 return res 321} 322 323function getPostUrlForQuote(post: any): string { 324 const isPostFromFedi = !!post.remotePostId 325 let res = `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}` 326 if (post.isRemoteBlueskyPost) { 327 const parts = post.bskyUri.split('/app.bsky.feed.post/') 328 const userDid = parts[0].split('at://')[1] 329 res = `https://bsky.app/profile/${userDid}/post/${parts[1]}` 330 } else if (isPostFromFedi) { 331 res = post.remotePostId 332 } 333 return res 334} 335 336export { postToJSONLD, getPostUrlForQuote }