forum.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
  5. import {
  6. bytesToString,
  7. deserializeMetadata,
  8. genericEventFields,
  9. getWorker,
  10. inconsistentState,
  11. perpareString,
  12. } from './common'
  13. import {
  14. CategoryCreatedEvent,
  15. CategoryStatusActive,
  16. CategoryArchivalStatusUpdatedEvent,
  17. ForumCategory,
  18. Worker,
  19. CategoryStatusArchived,
  20. CategoryDeletedEvent,
  21. CategoryStatusRemoved,
  22. ThreadCreatedEvent,
  23. ForumThread,
  24. Membership,
  25. ThreadStatusActive,
  26. ForumPoll,
  27. ForumPollAlternative,
  28. ThreadModeratedEvent,
  29. ThreadStatusModerated,
  30. ThreadMetadataUpdatedEvent,
  31. ThreadDeletedEvent,
  32. ThreadStatusLocked,
  33. ThreadStatusRemoved,
  34. ThreadMovedEvent,
  35. ForumPost,
  36. PostStatusActive,
  37. PostOriginThreadInitial,
  38. VoteOnPollEvent,
  39. PostAddedEvent,
  40. PostStatusLocked,
  41. PostOriginThreadReply,
  42. CategoryStickyThreadUpdateEvent,
  43. CategoryMembershipOfModeratorUpdatedEvent,
  44. PostModeratedEvent,
  45. PostStatusModerated,
  46. ForumPostReaction,
  47. PostReaction,
  48. PostReactedEvent,
  49. PostReactionResult,
  50. PostReactionResultCancel,
  51. PostReactionResultValid,
  52. PostReactionResultInvalid,
  53. PostTextUpdatedEvent,
  54. PostDeletedEvent,
  55. PostStatusRemoved,
  56. ForumThreadTag,
  57. } from 'query-node/dist/model'
  58. import { Forum } from './generated/types'
  59. import { PostReactionId, PrivilegedActor } from '@joystream/types/augment/all'
  60. import {
  61. ForumPostMetadata,
  62. ForumPostReaction as SupportedPostReactions,
  63. ForumThreadMetadata,
  64. } from '@joystream/metadata-protobuf'
  65. import { isSet } from '@joystream/metadata-protobuf/utils'
  66. import { MAX_TAGS_PER_FORUM_THREAD } from '@joystream/metadata-protobuf/consts'
  67. import { Not, In } from 'typeorm'
  68. import { Bytes } from '@polkadot/types'
  69. import _ from 'lodash'
  70. async function getCategory(store: DatabaseManager, categoryId: string, relations?: string[]): Promise<ForumCategory> {
  71. const category = await store.get(ForumCategory, { where: { id: categoryId }, relations })
  72. if (!category) {
  73. throw new Error(`Forum category not found by id: ${categoryId}`)
  74. }
  75. return category
  76. }
  77. async function getThread(store: DatabaseManager, threadId: string): Promise<ForumThread> {
  78. const thread = await store.get(ForumThread, { where: { id: threadId } })
  79. if (!thread) {
  80. throw new Error(`Forum thread not found by id: ${threadId.toString()}`)
  81. }
  82. return thread
  83. }
  84. async function getPost(store: DatabaseManager, postId: string, relations?: 'thread'[]): Promise<ForumPost> {
  85. const post = await store.get(ForumPost, { where: { id: postId }, relations })
  86. if (!post) {
  87. throw new Error(`Forum post not found by id: ${postId.toString()}`)
  88. }
  89. return post
  90. }
  91. async function getPollAlternative(store: DatabaseManager, threadId: string, index: number) {
  92. const poll = await store.get(ForumPoll, { where: { thread: { id: threadId } }, relations: ['pollAlternatives'] })
  93. if (!poll) {
  94. throw new Error(`Forum poll not found by threadId: ${threadId.toString()}`)
  95. }
  96. const pollAlternative = poll.pollAlternatives?.find((alt) => alt.index === index)
  97. if (!pollAlternative) {
  98. throw new Error(`Froum poll alternative not found by index ${index} in thread ${threadId.toString()}`)
  99. }
  100. return pollAlternative
  101. }
  102. async function getActorWorker(store: DatabaseManager, actor: PrivilegedActor): Promise<Worker> {
  103. const worker = await store.get(Worker, {
  104. where: {
  105. group: { id: 'forumWorkingGroup' },
  106. ...(actor.isLead ? { isLead: true } : { runtimeId: actor.asModerator.toNumber() }),
  107. },
  108. relations: ['group'],
  109. })
  110. if (!worker) {
  111. throw new Error(`Corresponding worker not found by forum PrivielagedActor: ${JSON.stringify(actor.toHuman())}`)
  112. }
  113. return worker
  114. }
  115. function normalizeForumTagLabel(label: string): string {
  116. // Optionally: normalize to lowercase & ASCII only?
  117. return perpareString(label)
  118. }
  119. function parseThreadMetadata(metaBytes: Bytes) {
  120. const meta = deserializeMetadata(ForumThreadMetadata, metaBytes)
  121. return {
  122. title: meta ? meta.title : bytesToString(metaBytes),
  123. tags:
  124. meta && isSet(meta.tags)
  125. ? _.uniq(meta.tags.slice(0, MAX_TAGS_PER_FORUM_THREAD).map((label) => normalizeForumTagLabel(label))).filter(
  126. (v) => v // Filter out empty strings
  127. )
  128. : undefined,
  129. }
  130. }
  131. async function prepareThreadTagsToSet(
  132. { event, store }: StoreContext & EventContext,
  133. labels: string[]
  134. ): Promise<ForumThreadTag[]> {
  135. const eventTime = new Date(event.blockTimestamp)
  136. return Promise.all(
  137. labels.map(async (label) => {
  138. const forumTag =
  139. (await store.get(ForumThreadTag, { where: { id: label } })) ||
  140. new ForumThreadTag({
  141. id: label,
  142. createdAt: eventTime,
  143. visibleThreadsCount: 0,
  144. })
  145. forumTag.updatedAt = eventTime
  146. ++forumTag.visibleThreadsCount
  147. await store.save<ForumThreadTag>(forumTag)
  148. return forumTag
  149. })
  150. )
  151. }
  152. async function unsetThreadTags({ event, store }: StoreContext & EventContext, tags: ForumThreadTag[]): Promise<void> {
  153. const eventTime = new Date(event.blockTimestamp)
  154. await Promise.all(
  155. tags.map(async (forumTag) => {
  156. --forumTag.visibleThreadsCount
  157. if (forumTag.visibleThreadsCount < 0) {
  158. inconsistentState('Trying to update forumTag.visibleThreadsCount to a number below 0!')
  159. }
  160. forumTag.updatedAt = eventTime
  161. await store.save<ForumThreadTag>(forumTag)
  162. })
  163. )
  164. }
  165. // Get standarized PostReactionResult by PostReactionId
  166. function parseReaction(reactionId: PostReactionId): typeof PostReactionResult {
  167. switch (reactionId.toNumber()) {
  168. case SupportedPostReactions.Reaction.CANCEL: {
  169. return new PostReactionResultCancel()
  170. }
  171. case SupportedPostReactions.Reaction.LIKE: {
  172. const result = new PostReactionResultValid()
  173. result.reaction = PostReaction.LIKE
  174. result.reactionId = reactionId.toNumber()
  175. return result
  176. }
  177. default: {
  178. console.warn(`Invalid post reaction id: ${reactionId.toString()}`)
  179. const result = new PostReactionResultInvalid()
  180. result.reactionId = reactionId.toNumber()
  181. return result
  182. }
  183. }
  184. }
  185. export async function forum_CategoryCreated({ event, store }: EventContext & StoreContext): Promise<void> {
  186. const [categoryId, parentCategoryId, titleBytes, descriptionBytes] = new Forum.CategoryCreatedEvent(event).params
  187. const eventTime = new Date(event.blockTimestamp)
  188. const category = new ForumCategory({
  189. id: categoryId.toString(),
  190. createdAt: eventTime,
  191. updatedAt: eventTime,
  192. title: bytesToString(titleBytes),
  193. description: bytesToString(descriptionBytes),
  194. status: new CategoryStatusActive(),
  195. parent: parentCategoryId.isSome ? new ForumCategory({ id: parentCategoryId.unwrap().toString() }) : undefined,
  196. })
  197. await store.save<ForumCategory>(category)
  198. const categoryCreatedEvent = new CategoryCreatedEvent({
  199. ...genericEventFields(event),
  200. category,
  201. })
  202. await store.save<CategoryCreatedEvent>(categoryCreatedEvent)
  203. }
  204. export async function forum_CategoryArchivalStatusUpdated({
  205. event,
  206. store,
  207. }: EventContext & StoreContext): Promise<void> {
  208. const [categoryId, newArchivalStatus, privilegedActor] = new Forum.CategoryArchivalStatusUpdatedEvent(event).params
  209. const eventTime = new Date(event.blockTimestamp)
  210. const category = await getCategory(store, categoryId.toString())
  211. const actorWorker = await getActorWorker(store, privilegedActor)
  212. const categoryArchivalStatusUpdatedEvent = new CategoryArchivalStatusUpdatedEvent({
  213. ...genericEventFields(event),
  214. category,
  215. newArchivalStatus: newArchivalStatus.valueOf(),
  216. actor: actorWorker,
  217. })
  218. await store.save<CategoryArchivalStatusUpdatedEvent>(categoryArchivalStatusUpdatedEvent)
  219. if (newArchivalStatus.valueOf()) {
  220. const status = new CategoryStatusArchived()
  221. status.categoryArchivalStatusUpdatedEventId = categoryArchivalStatusUpdatedEvent.id
  222. category.status = status
  223. } else {
  224. category.status = new CategoryStatusActive()
  225. }
  226. category.updatedAt = eventTime
  227. await store.save<ForumCategory>(category)
  228. }
  229. export async function forum_CategoryDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
  230. const [categoryId, privilegedActor] = new Forum.CategoryDeletedEvent(event).params
  231. const eventTime = new Date(event.blockTimestamp)
  232. const category = await getCategory(store, categoryId.toString())
  233. const actorWorker = await getActorWorker(store, privilegedActor)
  234. const categoryDeletedEvent = new CategoryDeletedEvent({
  235. ...genericEventFields(event),
  236. category,
  237. actor: actorWorker,
  238. })
  239. await store.save<CategoryDeletedEvent>(categoryDeletedEvent)
  240. const newStatus = new CategoryStatusRemoved()
  241. newStatus.categoryDeletedEventId = categoryDeletedEvent.id
  242. category.updatedAt = eventTime
  243. category.status = newStatus
  244. await store.save<ForumCategory>(category)
  245. }
  246. export async function forum_ThreadCreated(ctx: EventContext & StoreContext): Promise<void> {
  247. const { event, store } = ctx
  248. const [
  249. categoryId,
  250. threadId,
  251. postId,
  252. memberId,
  253. threadMetaBytes,
  254. postTextBytes,
  255. pollInput,
  256. ] = new Forum.ThreadCreatedEvent(event).params
  257. const eventTime = new Date(event.blockTimestamp)
  258. const author = new Membership({ id: memberId.toString() })
  259. const { title, tags } = parseThreadMetadata(threadMetaBytes)
  260. const thread = new ForumThread({
  261. createdAt: eventTime,
  262. updatedAt: eventTime,
  263. id: threadId.toString(),
  264. author,
  265. category: new ForumCategory({ id: categoryId.toString() }),
  266. title: title || '',
  267. isSticky: false,
  268. status: new ThreadStatusActive(),
  269. visiblePostsCount: 1,
  270. tags: tags ? await prepareThreadTagsToSet(ctx, tags) : [],
  271. })
  272. await store.save<ForumThread>(thread)
  273. if (pollInput.isSome) {
  274. const threadPoll = new ForumPoll({
  275. createdAt: eventTime,
  276. updatedAt: eventTime,
  277. description: bytesToString(pollInput.unwrap().description),
  278. endTime: new Date(pollInput.unwrap().end_time.toNumber()),
  279. thread,
  280. })
  281. await store.save<ForumPoll>(threadPoll)
  282. await Promise.all(
  283. pollInput.unwrap().poll_alternatives.map(async (alt, index) => {
  284. const alternative = new ForumPollAlternative({
  285. createdAt: eventTime,
  286. updatedAt: eventTime,
  287. poll: threadPoll,
  288. text: bytesToString(alt.alternative_text),
  289. index,
  290. })
  291. await store.save<ForumPollAlternative>(alternative)
  292. })
  293. )
  294. }
  295. const threadCreatedEvent = new ThreadCreatedEvent({
  296. ...genericEventFields(event),
  297. thread,
  298. title: title || '',
  299. text: bytesToString(postTextBytes),
  300. })
  301. await store.save<ThreadCreatedEvent>(threadCreatedEvent)
  302. const postOrigin = new PostOriginThreadInitial()
  303. postOrigin.threadCreatedEventId = threadCreatedEvent.id
  304. const initialPost = new ForumPost({
  305. id: postId.toString(),
  306. createdAt: eventTime,
  307. updatedAt: eventTime,
  308. author,
  309. thread,
  310. text: bytesToString(postTextBytes),
  311. status: new PostStatusActive(),
  312. origin: postOrigin,
  313. })
  314. await store.save<ForumPost>(initialPost)
  315. }
  316. export async function forum_ThreadModerated(ctx: EventContext & StoreContext): Promise<void> {
  317. const { event, store } = ctx
  318. const [threadId, rationaleBytes, privilegedActor] = new Forum.ThreadModeratedEvent(event).params
  319. const eventTime = new Date(event.blockTimestamp)
  320. const actorWorker = await getActorWorker(store, privilegedActor)
  321. const thread = await getThread(store, threadId.toString())
  322. const threadModeratedEvent = new ThreadModeratedEvent({
  323. ...genericEventFields(event),
  324. actor: actorWorker,
  325. thread,
  326. rationale: bytesToString(rationaleBytes),
  327. })
  328. await store.save<ThreadModeratedEvent>(threadModeratedEvent)
  329. const newStatus = new ThreadStatusModerated()
  330. newStatus.threadModeratedEventId = threadModeratedEvent.id
  331. thread.updatedAt = eventTime
  332. thread.status = newStatus
  333. thread.visiblePostsCount = 0
  334. await unsetThreadTags(ctx, thread.tags || [])
  335. await store.save<ForumThread>(thread)
  336. }
  337. export async function forum_ThreadMetadataUpdated(ctx: EventContext & StoreContext): Promise<void> {
  338. const { event, store } = ctx
  339. const [threadId, , , newMetadataBytes] = new Forum.ThreadMetadataUpdatedEvent(event).params
  340. const eventTime = new Date(event.blockTimestamp)
  341. const thread = await getThread(store, threadId.toString())
  342. const { title: newTitle, tags: newTagIds } = parseThreadMetadata(newMetadataBytes)
  343. // Only update tags if set
  344. if (isSet(newTagIds)) {
  345. const currentTagIds = (thread.tags || []).map((t) => t.id)
  346. const tagIdsToSet = _.difference(newTagIds, currentTagIds)
  347. const tagIdsToUnset = _.difference(currentTagIds, newTagIds)
  348. const newTags = await prepareThreadTagsToSet(ctx, tagIdsToSet)
  349. await unsetThreadTags(
  350. ctx,
  351. (thread.tags || []).filter((t) => tagIdsToUnset.includes(t.id))
  352. )
  353. thread.tags = newTags
  354. }
  355. if (isSet(newTitle)) {
  356. thread.title = newTitle
  357. }
  358. thread.updatedAt = eventTime
  359. await store.save<ForumThread>(thread)
  360. const threadMetadataUpdatedEvent = new ThreadMetadataUpdatedEvent({
  361. ...genericEventFields(event),
  362. thread,
  363. newTitle: newTitle || undefined,
  364. })
  365. await store.save<ThreadMetadataUpdatedEvent>(threadMetadataUpdatedEvent)
  366. }
  367. export async function forum_ThreadDeleted(ctx: EventContext & StoreContext): Promise<void> {
  368. const { event, store } = ctx
  369. const [threadId, , , hide] = new Forum.ThreadDeletedEvent(event).params
  370. const eventTime = new Date(event.blockTimestamp)
  371. const thread = await getThread(store, threadId.toString())
  372. const threadDeletedEvent = new ThreadDeletedEvent({
  373. ...genericEventFields(event),
  374. thread,
  375. })
  376. await store.save<ThreadDeletedEvent>(threadDeletedEvent)
  377. const status = hide.valueOf() ? new ThreadStatusRemoved() : new ThreadStatusLocked()
  378. status.threadDeletedEventId = threadDeletedEvent.id
  379. thread.status = status
  380. thread.updatedAt = eventTime
  381. if (hide.valueOf()) {
  382. thread.visiblePostsCount = 0
  383. await unsetThreadTags(ctx, thread.tags || [])
  384. }
  385. await store.save<ForumThread>(thread)
  386. }
  387. export async function forum_ThreadMoved({ event, store }: EventContext & StoreContext): Promise<void> {
  388. const [threadId, newCategoryId, privilegedActor, oldCategoryId] = new Forum.ThreadMovedEvent(event).params
  389. const eventTime = new Date(event.blockTimestamp)
  390. const thread = await getThread(store, threadId.toString())
  391. const actorWorker = await getActorWorker(store, privilegedActor)
  392. const threadMovedEvent = new ThreadMovedEvent({
  393. ...genericEventFields(event),
  394. thread,
  395. oldCategory: new ForumCategory({ id: oldCategoryId.toString() }),
  396. newCategory: new ForumCategory({ id: newCategoryId.toString() }),
  397. actor: actorWorker,
  398. })
  399. await store.save<ThreadMovedEvent>(threadMovedEvent)
  400. thread.updatedAt = eventTime
  401. thread.category = new ForumCategory({ id: newCategoryId.toString() })
  402. await store.save<ForumThread>(thread)
  403. }
  404. export async function forum_VoteOnPoll({ event, store }: EventContext & StoreContext): Promise<void> {
  405. const [threadId, alternativeIndex, forumUserId] = new Forum.VoteOnPollEvent(event).params
  406. const pollAlternative = await getPollAlternative(store, threadId.toString(), alternativeIndex.toNumber())
  407. const votingMember = new Membership({ id: forumUserId.toString() })
  408. const voteOnPollEvent = new VoteOnPollEvent({
  409. ...genericEventFields(event),
  410. pollAlternative,
  411. votingMember,
  412. })
  413. await store.save<VoteOnPollEvent>(voteOnPollEvent)
  414. }
  415. export async function forum_PostAdded({ event, store }: EventContext & StoreContext): Promise<void> {
  416. const [postId, forumUserId, , threadId, metadataBytes, isEditable] = new Forum.PostAddedEvent(event).params
  417. const eventTime = new Date(event.blockTimestamp)
  418. const thread = await getThread(store, threadId.toString())
  419. const metadata = deserializeMetadata(ForumPostMetadata, metadataBytes)
  420. const postText = metadata ? metadata.text || '' : bytesToString(metadataBytes)
  421. const repliesToPost =
  422. typeof metadata?.repliesTo === 'number' &&
  423. (await store.get(ForumPost, { where: { id: metadata.repliesTo.toString() } }))
  424. const postStatus = isEditable.valueOf() ? new PostStatusActive() : new PostStatusLocked()
  425. const postOrigin = new PostOriginThreadReply()
  426. const post = new ForumPost({
  427. id: postId.toString(),
  428. createdAt: eventTime,
  429. updatedAt: eventTime,
  430. text: postText,
  431. thread,
  432. status: postStatus,
  433. author: new Membership({ id: forumUserId.toString() }),
  434. origin: postOrigin,
  435. repliesTo: repliesToPost || undefined,
  436. })
  437. await store.save<ForumPost>(post)
  438. const postAddedEvent = new PostAddedEvent({
  439. ...genericEventFields(event),
  440. post,
  441. isEditable: isEditable.valueOf(),
  442. text: postText,
  443. })
  444. await store.save<PostAddedEvent>(postAddedEvent)
  445. // Update the other side of cross-relationship
  446. postOrigin.postAddedEventId = postAddedEvent.id
  447. await store.save<ForumPost>(post)
  448. ++thread.visiblePostsCount
  449. thread.updatedAt = eventTime
  450. await store.save<ForumThread>(thread)
  451. }
  452. export async function forum_CategoryStickyThreadUpdate({ event, store }: EventContext & StoreContext): Promise<void> {
  453. const [categoryId, newStickyThreadsIdsVec, privilegedActor] = new Forum.CategoryStickyThreadUpdateEvent(event).params
  454. const eventTime = new Date(event.blockTimestamp)
  455. const actorWorker = await getActorWorker(store, privilegedActor)
  456. const newStickyThreadsIds = newStickyThreadsIdsVec.map((id) => id.toString())
  457. const threadsToSetSticky = await store.getMany(ForumThread, {
  458. where: { category: { id: categoryId.toString() }, id: In(newStickyThreadsIds) },
  459. })
  460. const threadsToUnsetSticky = await store.getMany(ForumThread, {
  461. where: { category: { id: categoryId.toString() }, isSticky: true, id: Not(In(newStickyThreadsIds)) },
  462. })
  463. const setStickyUpdates = (threadsToSetSticky || []).map(async (t) => {
  464. t.updatedAt = eventTime
  465. t.isSticky = true
  466. await store.save<ForumThread>(t)
  467. })
  468. const unsetStickyUpdates = (threadsToUnsetSticky || []).map(async (t) => {
  469. t.updatedAt = eventTime
  470. t.isSticky = false
  471. await store.save<ForumThread>(t)
  472. })
  473. await Promise.all(setStickyUpdates.concat(unsetStickyUpdates))
  474. const categoryStickyThreadUpdateEvent = new CategoryStickyThreadUpdateEvent({
  475. ...genericEventFields(event),
  476. actor: actorWorker,
  477. category: new ForumCategory({ id: categoryId.toString() }),
  478. newStickyThreads: threadsToSetSticky,
  479. })
  480. await store.save<CategoryStickyThreadUpdateEvent>(categoryStickyThreadUpdateEvent)
  481. }
  482. export async function forum_CategoryMembershipOfModeratorUpdated({
  483. store,
  484. event,
  485. }: EventContext & StoreContext): Promise<void> {
  486. const [moderatorId, categoryId, canModerate] = new Forum.CategoryMembershipOfModeratorUpdatedEvent(event).params
  487. const eventTime = new Date(event.blockTimestamp)
  488. const moderator = await getWorker(store, 'forumWorkingGroup', moderatorId.toNumber())
  489. const category = await getCategory(store, categoryId.toString(), ['moderators'])
  490. if (canModerate.valueOf()) {
  491. category.moderators.push(moderator)
  492. category.updatedAt = eventTime
  493. await store.save<ForumCategory>(category)
  494. } else {
  495. category.moderators.splice(category.moderators.map((m) => m.id).indexOf(moderator.id), 1)
  496. category.updatedAt = eventTime
  497. await store.save<ForumCategory>(category)
  498. }
  499. const categoryMembershipOfModeratorUpdatedEvent = new CategoryMembershipOfModeratorUpdatedEvent({
  500. ...genericEventFields(event),
  501. category,
  502. moderator,
  503. newCanModerateValue: canModerate.valueOf(),
  504. })
  505. await store.save<CategoryMembershipOfModeratorUpdatedEvent>(categoryMembershipOfModeratorUpdatedEvent)
  506. }
  507. export async function forum_PostModerated({ event, store }: EventContext & StoreContext): Promise<void> {
  508. const [postId, rationaleBytes, privilegedActor] = new Forum.PostModeratedEvent(event).params
  509. const eventTime = new Date(event.blockTimestamp)
  510. const actorWorker = await getActorWorker(store, privilegedActor)
  511. const post = await getPost(store, postId.toString(), ['thread'])
  512. const postModeratedEvent = new PostModeratedEvent({
  513. ...genericEventFields(event),
  514. actor: actorWorker,
  515. post,
  516. rationale: bytesToString(rationaleBytes),
  517. })
  518. await store.save<PostModeratedEvent>(postModeratedEvent)
  519. const newStatus = new PostStatusModerated()
  520. newStatus.postModeratedEventId = postModeratedEvent.id
  521. post.updatedAt = eventTime
  522. post.status = newStatus
  523. await store.save<ForumPost>(post)
  524. const { thread } = post
  525. --thread.visiblePostsCount
  526. thread.updatedAt = eventTime
  527. await store.save<ForumThread>(thread)
  528. }
  529. export async function forum_PostReacted({ event, store }: EventContext & StoreContext): Promise<void> {
  530. const [userId, postId, reactionId] = new Forum.PostReactedEvent(event).params
  531. const eventTime = new Date(event.blockTimestamp)
  532. const reactionResult = parseReaction(reactionId)
  533. const postReactedEvent = new PostReactedEvent({
  534. ...genericEventFields(event),
  535. post: new ForumPost({ id: postId.toString() }),
  536. reactingMember: new Membership({ id: userId.toString() }),
  537. reactionResult,
  538. })
  539. await store.save<PostReactedEvent>(postReactedEvent)
  540. const existingUserPostReaction = await store.get(ForumPostReaction, {
  541. where: { post: { id: postId.toString() }, member: { id: userId.toString() } },
  542. })
  543. if (reactionResult.isTypeOf === 'PostReactionResultValid') {
  544. const { reaction } = reactionResult as PostReactionResultValid
  545. if (existingUserPostReaction) {
  546. existingUserPostReaction.updatedAt = eventTime
  547. existingUserPostReaction.reaction = reaction
  548. await store.save<ForumPostReaction>(existingUserPostReaction)
  549. } else {
  550. const newUserPostReaction = new ForumPostReaction({
  551. createdAt: eventTime,
  552. updatedAt: eventTime,
  553. post: new ForumPost({ id: postId.toString() }),
  554. member: new Membership({ id: userId.toString() }),
  555. reaction,
  556. })
  557. await store.save<ForumPostReaction>(newUserPostReaction)
  558. }
  559. } else if (existingUserPostReaction) {
  560. await store.remove<ForumPostReaction>(existingUserPostReaction)
  561. }
  562. }
  563. export async function forum_PostTextUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
  564. const [postId, , , , newTextBytes] = new Forum.PostTextUpdatedEvent(event).params
  565. const eventTime = new Date(event.blockTimestamp)
  566. const post = await getPost(store, postId.toString())
  567. const postTextUpdatedEvent = new PostTextUpdatedEvent({
  568. ...genericEventFields(event),
  569. post,
  570. newText: bytesToString(newTextBytes),
  571. })
  572. await store.save<PostTextUpdatedEvent>(postTextUpdatedEvent)
  573. post.updatedAt = eventTime
  574. post.text = bytesToString(newTextBytes)
  575. await store.save<ForumPost>(post)
  576. }
  577. export async function forum_PostDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
  578. // FIXME: Custom posts BTreeMap fix (because of invalid BTreeMap json encoding/decoding)
  579. // See: https://github.com/polkadot-js/api/pull/3789
  580. event.params[2].value = new Map(
  581. Object.entries(event.params[2].value).map(([key, val]) => [JSON.parse(key), val])
  582. ) as any
  583. const [rationaleBytes, userId, postsData] = new Forum.PostDeletedEvent(event).params
  584. const eventTime = new Date(event.blockTimestamp)
  585. const postDeletedEvent = new PostDeletedEvent({
  586. ...genericEventFields(event),
  587. actor: new Membership({ id: userId.toString() }),
  588. rationale: bytesToString(rationaleBytes),
  589. })
  590. await store.save<PostDeletedEvent>(postDeletedEvent)
  591. await Promise.all(
  592. Array.from(postsData.entries()).map(async ([[, , postId], hideFlag]) => {
  593. const post = await getPost(store, postId.toString(), ['thread'])
  594. const newStatus = hideFlag.valueOf() ? new PostStatusRemoved() : new PostStatusLocked()
  595. newStatus.postDeletedEventId = postDeletedEvent.id
  596. post.updatedAt = eventTime
  597. post.status = newStatus
  598. post.deletedInEvent = postDeletedEvent
  599. await store.save<ForumPost>(post)
  600. if (hideFlag.valueOf()) {
  601. const { thread } = post
  602. --thread.visiblePostsCount
  603. thread.updatedAt = eventTime
  604. await store.save<ForumThread>(thread)
  605. }
  606. })
  607. )
  608. }