logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git

entity_normalizer.service.js (16083B)


  1. import escape from 'escape-html'
  2. import parseLinkHeader from 'parse-link-header'
  3. import { isStatusNotification } from '../notification_utils/notification_utils.js'
  4. import punycode from 'punycode.js'
  5. /** NOTICE! **
  6. * Do not initialize UI-generated data here.
  7. * It will override existing data.
  8. *
  9. * i.e. user.pinnedStatusIds was set to [] here
  10. * UI code would update it with data but upon next user fetch
  11. * it would be reverted back to []
  12. */
  13. const qvitterStatusType = (status) => {
  14. if (status.is_post_verb) {
  15. return 'status'
  16. }
  17. if (status.retweeted_status) {
  18. return 'retweet'
  19. }
  20. if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
  21. (typeof status.text === 'string' && status.text.match(/favorited/))) {
  22. return 'favorite'
  23. }
  24. if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
  25. return 'deletion'
  26. }
  27. if (status.text.match(/started following/) || status.activity_type === 'follow') {
  28. return 'follow'
  29. }
  30. return 'unknown'
  31. }
  32. export const parseUser = (data) => {
  33. const output = {}
  34. const masto = Object.prototype.hasOwnProperty.call(data, 'acct')
  35. // case for users in "mentions" property for statuses in MastoAPI
  36. const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
  37. output.inLists = null
  38. output.id = String(data.id)
  39. output._original = data // used for server-side settings
  40. if (masto) {
  41. output.screen_name = data.acct
  42. output.fqn = data.fqn
  43. output.statusnet_profile_url = data.url
  44. // There's nothing else to get
  45. if (mastoShort) {
  46. return output
  47. }
  48. output.emoji = data.emojis
  49. output.name = escape(data.display_name)
  50. output.name_html = output.name
  51. output.name_unescaped = data.display_name
  52. output.description = data.note
  53. // TODO cleanup this shit, output.description is overriden with source data
  54. output.description_html = data.note
  55. output.fields = data.fields
  56. output.fields_html = data.fields.map(field => {
  57. return {
  58. name: escape(field.name),
  59. value: field.value
  60. }
  61. })
  62. output.fields_text = data.fields.map(field => {
  63. return {
  64. name: unescape(field.name.replace(/<[^>]*>/g, '')),
  65. value: unescape(field.value.replace(/<[^>]*>/g, ''))
  66. }
  67. })
  68. // Utilize avatar_static for gif avatars?
  69. output.profile_image_url = data.avatar
  70. output.profile_image_url_original = data.avatar
  71. // Same, utilize header_static?
  72. output.cover_photo = data.header
  73. output.friends_count = data.following_count
  74. output.bot = data.bot
  75. if (data.pleroma) {
  76. if (data.pleroma.settings_store) {
  77. output.storage = data.pleroma.settings_store['pleroma-fe']
  78. }
  79. const relationship = data.pleroma.relationship
  80. output.background_image = data.pleroma.background_image
  81. output.favicon = data.pleroma.favicon
  82. output.token = data.pleroma.chat_token
  83. if (relationship) {
  84. output.relationship = relationship
  85. }
  86. output.allow_following_move = data.pleroma.allow_following_move
  87. output.hide_favorites = data.pleroma.hide_favorites
  88. output.hide_follows = data.pleroma.hide_follows
  89. output.hide_followers = data.pleroma.hide_followers
  90. output.hide_follows_count = data.pleroma.hide_follows_count
  91. output.hide_followers_count = data.pleroma.hide_followers_count
  92. output.rights = {
  93. moderator: data.pleroma.is_moderator,
  94. admin: data.pleroma.is_admin
  95. }
  96. // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
  97. if (output.rights.admin) {
  98. output.role = 'admin'
  99. } else if (output.rights.moderator) {
  100. output.role = 'moderator'
  101. } else {
  102. output.role = 'member'
  103. }
  104. output.birthday = data.pleroma.birthday
  105. if (data.pleroma.privileges) {
  106. output.privileges = data.pleroma.privileges
  107. } else if (data.pleroma.is_admin) {
  108. output.privileges = [
  109. 'users_read',
  110. 'users_manage_invites',
  111. 'users_manage_activation_state',
  112. 'users_manage_tags',
  113. 'users_manage_credentials',
  114. 'users_delete',
  115. 'messages_read',
  116. 'messages_delete',
  117. 'instances_delete',
  118. 'reports_manage_reports',
  119. 'moderation_log_read',
  120. 'announcements_manage_announcements',
  121. 'emoji_manage_emoji',
  122. 'statistics_read'
  123. ]
  124. } else if (data.pleroma.is_moderator) {
  125. output.privileges = [
  126. 'messages_delete',
  127. 'reports_manage_reports'
  128. ]
  129. } else {
  130. output.privileges = []
  131. }
  132. }
  133. if (data.source) {
  134. output.description = data.source.note
  135. output.default_scope = data.source.privacy
  136. output.fields = data.source.fields
  137. if (data.source.pleroma) {
  138. output.no_rich_text = data.source.pleroma.no_rich_text
  139. output.show_role = data.source.pleroma.show_role
  140. output.discoverable = data.source.pleroma.discoverable
  141. output.show_birthday = data.pleroma.show_birthday
  142. output.actor_type = data.source.pleroma.actor_type
  143. }
  144. }
  145. // TODO: handle is_local
  146. output.is_local = !output.screen_name.includes('@')
  147. } else {
  148. output.screen_name = data.screen_name
  149. output.name = data.name
  150. output.name_html = data.name_html
  151. output.description = data.description
  152. output.description_html = data.description_html
  153. output.profile_image_url = data.profile_image_url
  154. output.profile_image_url_original = data.profile_image_url_original
  155. output.cover_photo = data.cover_photo
  156. output.friends_count = data.friends_count
  157. // output.bot = ??? missing
  158. output.statusnet_profile_url = data.statusnet_profile_url
  159. output.is_local = data.is_local
  160. output.role = data.role
  161. output.show_role = data.show_role
  162. if (data.rights) {
  163. output.rights = {
  164. moderator: data.rights.delete_others_notice,
  165. admin: data.rights.admin
  166. }
  167. }
  168. output.no_rich_text = data.no_rich_text
  169. output.default_scope = data.default_scope
  170. output.hide_follows = data.hide_follows
  171. output.hide_followers = data.hide_followers
  172. output.hide_follows_count = data.hide_follows_count
  173. output.hide_followers_count = data.hide_followers_count
  174. output.background_image = data.background_image
  175. // Websocket token
  176. output.token = data.token
  177. // Convert relationsip data to expected format
  178. output.relationship = {
  179. muting: data.muted,
  180. blocking: data.statusnet_blocking,
  181. followed_by: data.follows_you,
  182. following: data.following
  183. }
  184. }
  185. output.created_at = new Date(data.created_at)
  186. output.locked = data.locked
  187. output.followers_count = data.followers_count
  188. output.statuses_count = data.statuses_count
  189. if (data.pleroma) {
  190. output.follow_request_count = data.pleroma.follow_request_count
  191. output.tags = data.pleroma.tags
  192. // deactivated was changed to is_active in Pleroma 2.3.0
  193. // so check if is_active is present
  194. output.deactivated = typeof data.pleroma.is_active !== 'undefined'
  195. ? !data.pleroma.is_active // new backend
  196. : data.pleroma.deactivated // old backend
  197. output.notification_settings = data.pleroma.notification_settings
  198. output.unread_chat_count = data.pleroma.unread_chat_count
  199. }
  200. output.tags = output.tags || []
  201. output.rights = output.rights || {}
  202. output.notification_settings = output.notification_settings || {}
  203. // Convert punycode to unicode for UI
  204. output.screen_name_ui = output.screen_name
  205. if (output.screen_name && output.screen_name.includes('@')) {
  206. const parts = output.screen_name.split('@')
  207. const unicodeDomain = punycode.toUnicode(parts[1])
  208. if (unicodeDomain !== parts[1]) {
  209. // Add some identifier so users can potentially spot spoofing attempts:
  210. // lain.com and xn--lin-6cd.com would appear identical otherwise.
  211. output.screen_name_ui_contains_non_ascii = true
  212. output.screen_name_ui = [parts[0], unicodeDomain].join('@')
  213. } else {
  214. output.screen_name_ui_contains_non_ascii = false
  215. }
  216. }
  217. return output
  218. }
  219. export const parseAttachment = (data) => {
  220. const output = {}
  221. const masto = !Object.prototype.hasOwnProperty.call(data, 'oembed')
  222. if (masto) {
  223. // Not exactly same...
  224. output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
  225. output.meta = data.meta // not present in BE yet
  226. output.id = data.id
  227. } else {
  228. output.mimetype = data.mimetype
  229. // output.meta = ??? missing
  230. }
  231. output.url = data.url
  232. output.large_thumb_url = data.preview_url
  233. output.description = data.description
  234. return output
  235. }
  236. export const parseSource = (data) => {
  237. const output = {}
  238. output.text = data.text
  239. output.spoiler_text = data.spoiler_text
  240. output.content_type = data.content_type
  241. return output
  242. }
  243. export const parseStatus = (data) => {
  244. const output = {}
  245. const masto = Object.prototype.hasOwnProperty.call(data, 'account')
  246. if (masto) {
  247. output.favorited = data.favourited
  248. output.fave_num = data.favourites_count
  249. output.repeated = data.reblogged
  250. output.repeat_num = data.reblogs_count
  251. output.bookmarked = data.bookmarked
  252. output.type = data.reblog ? 'retweet' : 'status'
  253. output.nsfw = data.sensitive
  254. output.raw_html = data.content
  255. output.emojis = data.emojis
  256. output.tags = data.tags
  257. output.edited_at = data.edited_at
  258. if (data.pleroma) {
  259. const { pleroma } = data
  260. output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
  261. output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
  262. output.statusnet_conversation_id = data.pleroma.conversation_id
  263. output.is_local = pleroma.local
  264. output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
  265. output.thread_muted = pleroma.thread_muted
  266. output.emoji_reactions = pleroma.emoji_reactions
  267. output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
  268. output.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined
  269. output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined)
  270. output.quote_url = pleroma.quote_url
  271. output.quote_visible = pleroma.quote_visible
  272. } else {
  273. output.text = data.content
  274. output.summary = data.spoiler_text
  275. }
  276. output.in_reply_to_status_id = data.in_reply_to_id
  277. output.in_reply_to_user_id = data.in_reply_to_account_id
  278. output.replies_count = data.replies_count
  279. if (output.type === 'retweet') {
  280. output.retweeted_status = parseStatus(data.reblog)
  281. }
  282. output.summary_raw_html = escape(data.spoiler_text)
  283. output.external_url = data.url
  284. output.poll = data.poll
  285. if (output.poll) {
  286. output.poll.options = (output.poll.options || []).map(field => ({
  287. ...field,
  288. title_html: escape(field.title)
  289. }))
  290. }
  291. output.pinned = data.pinned
  292. output.muted = data.muted
  293. } else {
  294. output.favorited = data.favorited
  295. output.fave_num = data.fave_num
  296. output.repeated = data.repeated
  297. output.repeat_num = data.repeat_num
  298. // catchall, temporary
  299. // Object.assign(output, data)
  300. output.type = qvitterStatusType(data)
  301. if (data.nsfw === undefined) {
  302. output.nsfw = isNsfw(data)
  303. if (data.retweeted_status) {
  304. output.nsfw = data.retweeted_status.nsfw
  305. }
  306. } else {
  307. output.nsfw = data.nsfw
  308. }
  309. output.raw_html = data.statusnet_html
  310. output.text = data.text
  311. output.in_reply_to_status_id = data.in_reply_to_status_id
  312. output.in_reply_to_user_id = data.in_reply_to_user_id
  313. output.in_reply_to_screen_name = data.in_reply_to_screen_name
  314. output.statusnet_conversation_id = data.statusnet_conversation_id
  315. if (output.type === 'retweet') {
  316. output.retweeted_status = parseStatus(data.retweeted_status)
  317. }
  318. output.summary = data.summary
  319. output.summary_html = data.summary_html
  320. output.external_url = data.external_url
  321. output.is_local = data.is_local
  322. }
  323. output.id = String(data.id)
  324. output.visibility = data.visibility
  325. output.card = data.card
  326. output.created_at = new Date(data.created_at)
  327. // Converting to string, the right way.
  328. output.in_reply_to_status_id = output.in_reply_to_status_id
  329. ? String(output.in_reply_to_status_id)
  330. : null
  331. output.in_reply_to_user_id = output.in_reply_to_user_id
  332. ? String(output.in_reply_to_user_id)
  333. : null
  334. output.user = parseUser(masto ? data.account : data.user)
  335. output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
  336. output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
  337. .map(parseAttachment)
  338. const retweetedStatus = masto ? data.reblog : data.retweeted_status
  339. if (retweetedStatus) {
  340. output.retweeted_status = parseStatus(retweetedStatus)
  341. }
  342. output.favoritedBy = []
  343. output.rebloggedBy = []
  344. if (Object.prototype.hasOwnProperty.call(data, 'originalStatus')) {
  345. Object.assign(output, data.originalStatus)
  346. }
  347. return output
  348. }
  349. export const parseNotification = (data) => {
  350. const mastoDict = {
  351. favourite: 'like',
  352. reblog: 'repeat'
  353. }
  354. const masto = !Object.prototype.hasOwnProperty.call(data, 'ntype')
  355. const output = {}
  356. if (masto) {
  357. output.type = mastoDict[data.type] || data.type
  358. output.seen = data.pleroma.is_seen
  359. output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
  360. output.target = output.type !== 'move'
  361. ? null
  362. : parseUser(data.target)
  363. output.from_profile = parseUser(data.account)
  364. output.emoji = data.emoji
  365. output.emoji_url = data.emoji_url
  366. if (data.report) {
  367. output.report = data.report
  368. output.report.content = data.report.content
  369. output.report.acct = parseUser(data.report.account)
  370. output.report.actor = parseUser(data.report.actor)
  371. output.report.statuses = data.report.statuses.map(parseStatus)
  372. }
  373. } else {
  374. const parsedNotice = parseStatus(data.notice)
  375. output.type = data.ntype
  376. output.seen = Boolean(data.is_seen)
  377. output.status = output.type === 'like'
  378. ? parseStatus(data.notice.favorited_status)
  379. : parsedNotice
  380. output.action = parsedNotice
  381. output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
  382. }
  383. output.created_at = new Date(data.created_at)
  384. output.id = parseInt(data.id)
  385. return output
  386. }
  387. const isNsfw = (status) => {
  388. const nsfwRegex = /#nsfw/i
  389. return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
  390. }
  391. export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
  392. const flakeId = opts.flakeId
  393. const parsedLinkHeader = parseLinkHeader(linkHeader)
  394. if (!parsedLinkHeader) return
  395. const maxId = parsedLinkHeader.next.max_id
  396. const minId = parsedLinkHeader.prev.min_id
  397. return {
  398. maxId: flakeId ? maxId : parseInt(maxId, 10),
  399. minId: flakeId ? minId : parseInt(minId, 10)
  400. }
  401. }
  402. export const parseChat = (chat) => {
  403. const output = {}
  404. output.id = chat.id
  405. output.account = parseUser(chat.account)
  406. output.unread = chat.unread
  407. output.lastMessage = parseChatMessage(chat.last_message)
  408. output.updated_at = new Date(chat.updated_at)
  409. return output
  410. }
  411. export const parseChatMessage = (message) => {
  412. if (!message) { return }
  413. if (message.isNormalized) { return message }
  414. const output = message
  415. output.id = message.id
  416. output.created_at = new Date(message.created_at)
  417. output.chat_id = message.chat_id
  418. output.emojis = message.emojis
  419. output.content = message.content
  420. if (message.attachment) {
  421. output.attachments = [parseAttachment(message.attachment)]
  422. } else {
  423. output.attachments = []
  424. }
  425. output.pending = !!message.pending
  426. output.error = false
  427. output.idempotency_key = message.idempotency_key
  428. output.isNormalized = true
  429. return output
  430. }