forum.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
  5. import { bytesToString, deserializeMetadata, genericEventFields, getWorker } from './common'
  6. import {
  7. CategoryCreatedEvent,
  8. CategoryStatusActive,
  9. CategoryArchivalStatusUpdatedEvent,
  10. ForumCategory,
  11. Worker,
  12. CategoryStatusArchived,
  13. CategoryDeletedEvent,
  14. CategoryStatusRemoved,
  15. ThreadCreatedEvent,
  16. ForumThread,
  17. Membership,
  18. ThreadStatusActive,
  19. ForumPoll,
  20. ForumPollAlternative,
  21. ThreadModeratedEvent,
  22. ThreadStatusModerated,
  23. ThreadTitleUpdatedEvent,
  24. ThreadDeletedEvent,
  25. ThreadStatusLocked,
  26. ThreadStatusRemoved,
  27. ThreadMovedEvent,
  28. ForumPost,
  29. PostStatusActive,
  30. PostOriginThreadInitial,
  31. VoteOnPollEvent,
  32. PostAddedEvent,
  33. PostStatusLocked,
  34. PostOriginThreadReply,
  35. CategoryStickyThreadUpdateEvent,
  36. CategoryMembershipOfModeratorUpdatedEvent,
  37. PostModeratedEvent,
  38. PostStatusModerated,
  39. ForumPostReaction,
  40. PostReaction,
  41. PostReactedEvent,
  42. PostReactionResult,
  43. PostReactionResultCancel,
  44. PostReactionResultValid,
  45. PostReactionResultInvalid,
  46. PostTextUpdatedEvent,
  47. PostDeletedEvent,
  48. PostStatusRemoved,
  49. } from 'query-node/dist/model'
  50. import { Forum } from './generated/types'
  51. import { PostReactionId, PrivilegedActor } from '@joystream/types/augment/all'
  52. import { ForumPostMetadata, ForumPostReaction as SupportedPostReactions } from '@joystream/metadata-protobuf'
  53. import { Not, In } from 'typeorm'
  54. async function getCategory(store: DatabaseManager, categoryId: string, relations?: string[]): Promise<ForumCategory> {
  55. const category = await store.get(ForumCategory, { where: { id: categoryId }, relations })
  56. if (!category) {
  57. throw new Error(`Forum category not found by id: ${categoryId}`)
  58. }
  59. return category
  60. }
  61. async function getThread(store: DatabaseManager, threadId: string): Promise<ForumThread> {
  62. const thread = await store.get(ForumThread, { where: { id: threadId } })
  63. if (!thread) {
  64. throw new Error(`Forum thread not found by id: ${threadId.toString()}`)
  65. }
  66. return thread
  67. }
  68. async function getPost(store: DatabaseManager, postId: string): Promise<ForumPost> {
  69. const post = await store.get(ForumPost, { where: { id: postId } })
  70. if (!post) {
  71. throw new Error(`Forum post not found by id: ${postId.toString()}`)
  72. }
  73. return post
  74. }
  75. async function getPollAlternative(store: DatabaseManager, threadId: string, index: number) {
  76. const poll = await store.get(ForumPoll, { where: { thread: { id: threadId } }, relations: ['pollAlternatives'] })
  77. if (!poll) {
  78. throw new Error(`Forum poll not found by threadId: ${threadId.toString()}`)
  79. }
  80. const pollAlternative = poll.pollAlternatives?.find((alt) => alt.index === index)
  81. if (!pollAlternative) {
  82. throw new Error(`Froum poll alternative not found by index ${index} in thread ${threadId.toString()}`)
  83. }
  84. return pollAlternative
  85. }
  86. async function getActorWorker(store: DatabaseManager, actor: PrivilegedActor): Promise<Worker> {
  87. const worker = await store.get(Worker, {
  88. where: {
  89. group: { id: 'forumWorkingGroup' },
  90. ...(actor.isLead ? { isLead: true } : { runtimeId: actor.asModerator.toNumber() }),
  91. },
  92. relations: ['group'],
  93. })
  94. if (!worker) {
  95. throw new Error(`Corresponding worker not found by forum PrivielagedActor: ${JSON.stringify(actor.toHuman())}`)
  96. }
  97. return worker
  98. }
  99. // Get standarized PostReactionResult by PostReactionId
  100. function parseReaction(reactionId: PostReactionId): typeof PostReactionResult {
  101. switch (reactionId.toNumber()) {
  102. case SupportedPostReactions.Reaction.CANCEL: {
  103. return new PostReactionResultCancel()
  104. }
  105. case SupportedPostReactions.Reaction.LIKE: {
  106. const result = new PostReactionResultValid()
  107. result.reaction = PostReaction.LIKE
  108. result.reactionId = reactionId.toNumber()
  109. return result
  110. }
  111. default: {
  112. console.warn(`Invalid post reaction id: ${reactionId.toString()}`)
  113. const result = new PostReactionResultInvalid()
  114. result.reactionId = reactionId.toNumber()
  115. return result
  116. }
  117. }
  118. }
  119. export async function forum_CategoryCreated({ event, store }: EventContext & StoreContext): Promise<void> {
  120. const [categoryId, parentCategoryId, titleBytes, descriptionBytes] = new Forum.CategoryCreatedEvent(event).params
  121. const eventTime = new Date(event.blockTimestamp)
  122. const category = new ForumCategory({
  123. id: categoryId.toString(),
  124. createdAt: eventTime,
  125. updatedAt: eventTime,
  126. title: bytesToString(titleBytes),
  127. description: bytesToString(descriptionBytes),
  128. status: new CategoryStatusActive(),
  129. parent: parentCategoryId.isSome ? new ForumCategory({ id: parentCategoryId.unwrap().toString() }) : undefined,
  130. })
  131. await store.save<ForumCategory>(category)
  132. const categoryCreatedEvent = new CategoryCreatedEvent({
  133. ...genericEventFields(event),
  134. category,
  135. })
  136. await store.save<CategoryCreatedEvent>(categoryCreatedEvent)
  137. }
  138. export async function forum_CategoryArchivalStatusUpdated({
  139. event,
  140. store,
  141. }: EventContext & StoreContext): Promise<void> {
  142. const [categoryId, newArchivalStatus, privilegedActor] = new Forum.CategoryArchivalStatusUpdatedEvent(event).params
  143. const eventTime = new Date(event.blockTimestamp)
  144. const category = await getCategory(store, categoryId.toString())
  145. const actorWorker = await getActorWorker(store, privilegedActor)
  146. const categoryArchivalStatusUpdatedEvent = new CategoryArchivalStatusUpdatedEvent({
  147. ...genericEventFields(event),
  148. category,
  149. newArchivalStatus: newArchivalStatus.valueOf(),
  150. actor: actorWorker,
  151. })
  152. await store.save<CategoryArchivalStatusUpdatedEvent>(categoryArchivalStatusUpdatedEvent)
  153. if (newArchivalStatus.valueOf()) {
  154. const status = new CategoryStatusArchived()
  155. status.categoryArchivalStatusUpdatedEventId = categoryArchivalStatusUpdatedEvent.id
  156. category.status = status
  157. } else {
  158. category.status = new CategoryStatusActive()
  159. }
  160. category.updatedAt = eventTime
  161. await store.save<ForumCategory>(category)
  162. }
  163. export async function forum_CategoryDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
  164. const [categoryId, privilegedActor] = new Forum.CategoryDeletedEvent(event).params
  165. const eventTime = new Date(event.blockTimestamp)
  166. const category = await getCategory(store, categoryId.toString())
  167. const actorWorker = await getActorWorker(store, privilegedActor)
  168. const categoryDeletedEvent = new CategoryDeletedEvent({
  169. ...genericEventFields(event),
  170. category,
  171. actor: actorWorker,
  172. })
  173. await store.save<CategoryDeletedEvent>(categoryDeletedEvent)
  174. const newStatus = new CategoryStatusRemoved()
  175. newStatus.categoryDeletedEventId = categoryDeletedEvent.id
  176. category.updatedAt = eventTime
  177. category.status = newStatus
  178. await store.save<ForumCategory>(category)
  179. }
  180. export async function forum_ThreadCreated({ event, store }: EventContext & StoreContext): Promise<void> {
  181. const { forumUserId, categoryId, title, text, poll } = new Forum.CreateThreadCall(event).args
  182. const [threadId] = new Forum.ThreadCreatedEvent(event).params
  183. const eventTime = new Date(event.blockTimestamp)
  184. const author = new Membership({ id: forumUserId.toString() })
  185. const thread = new ForumThread({
  186. createdAt: eventTime,
  187. updatedAt: eventTime,
  188. id: threadId.toString(),
  189. author,
  190. category: new ForumCategory({ id: categoryId.toString() }),
  191. title: bytesToString(title),
  192. isSticky: false,
  193. status: new ThreadStatusActive(),
  194. })
  195. await store.save<ForumThread>(thread)
  196. if (poll.isSome) {
  197. const threadPoll = new ForumPoll({
  198. createdAt: eventTime,
  199. updatedAt: eventTime,
  200. description: bytesToString(poll.unwrap().description_hash), // FIXME: This should be raw description!
  201. endTime: new Date(poll.unwrap().end_time.toNumber()),
  202. thread,
  203. })
  204. await store.save<ForumPoll>(threadPoll)
  205. await Promise.all(
  206. poll.unwrap().poll_alternatives.map(async (alt, index) => {
  207. const alternative = new ForumPollAlternative({
  208. createdAt: eventTime,
  209. updatedAt: eventTime,
  210. poll: threadPoll,
  211. text: bytesToString(alt.alternative_text_hash), // FIXME: This should be raw text!
  212. index,
  213. })
  214. await store.save<ForumPollAlternative>(alternative)
  215. })
  216. )
  217. }
  218. const threadCreatedEvent = new ThreadCreatedEvent({
  219. ...genericEventFields(event),
  220. thread,
  221. title: bytesToString(title),
  222. text: bytesToString(text),
  223. })
  224. await store.save<ThreadCreatedEvent>(threadCreatedEvent)
  225. const postOrigin = new PostOriginThreadInitial()
  226. postOrigin.threadCreatedEventId = threadCreatedEvent.id
  227. const initialPost = new ForumPost({
  228. // FIXME: The postId is unknown
  229. createdAt: eventTime,
  230. updatedAt: eventTime,
  231. author,
  232. thread,
  233. text: bytesToString(text),
  234. status: new PostStatusActive(),
  235. origin: postOrigin,
  236. })
  237. await store.save<ForumPost>(initialPost)
  238. }
  239. export async function forum_ThreadModerated({ event, store }: EventContext & StoreContext): Promise<void> {
  240. const [threadId, rationaleBytes, privilegedActor] = new Forum.ThreadModeratedEvent(event).params
  241. const eventTime = new Date(event.blockTimestamp)
  242. const actorWorker = await getActorWorker(store, privilegedActor)
  243. const thread = await getThread(store, threadId.toString())
  244. const threadModeratedEvent = new ThreadModeratedEvent({
  245. ...genericEventFields(event),
  246. actor: actorWorker,
  247. thread,
  248. rationale: bytesToString(rationaleBytes),
  249. })
  250. await store.save<ThreadModeratedEvent>(threadModeratedEvent)
  251. const newStatus = new ThreadStatusModerated()
  252. newStatus.threadModeratedEventId = threadModeratedEvent.id
  253. thread.updatedAt = eventTime
  254. thread.status = newStatus
  255. await store.save<ForumThread>(thread)
  256. }
  257. export async function forum_ThreadTitleUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
  258. const [threadId, , , newTitleBytes] = new Forum.ThreadTitleUpdatedEvent(event).params
  259. const eventTime = new Date(event.blockTimestamp)
  260. const thread = await getThread(store, threadId.toString())
  261. const threadTitleUpdatedEvent = new ThreadTitleUpdatedEvent({
  262. ...genericEventFields(event),
  263. thread,
  264. newTitle: bytesToString(newTitleBytes),
  265. })
  266. await store.save<ThreadTitleUpdatedEvent>(threadTitleUpdatedEvent)
  267. thread.updatedAt = eventTime
  268. thread.title = bytesToString(newTitleBytes)
  269. await store.save<ForumThread>(thread)
  270. }
  271. export async function forum_ThreadDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
  272. const [threadId, , , hide] = new Forum.ThreadDeletedEvent(event).params
  273. const eventTime = new Date(event.blockTimestamp)
  274. const thread = await getThread(store, threadId.toString())
  275. const threadDeletedEvent = new ThreadDeletedEvent({
  276. ...genericEventFields(event),
  277. thread,
  278. })
  279. await store.save<ThreadDeletedEvent>(threadDeletedEvent)
  280. const status = hide.valueOf() ? new ThreadStatusRemoved() : new ThreadStatusLocked()
  281. status.threadDeletedEventId = threadDeletedEvent.id
  282. thread.status = status
  283. thread.updatedAt = eventTime
  284. await store.save<ForumThread>(thread)
  285. }
  286. export async function forum_ThreadMoved({ event, store }: EventContext & StoreContext): Promise<void> {
  287. const [threadId, newCategoryId, privilegedActor, oldCategoryId] = new Forum.ThreadMovedEvent(event).params
  288. const eventTime = new Date(event.blockTimestamp)
  289. const thread = await getThread(store, threadId.toString())
  290. const actorWorker = await getActorWorker(store, privilegedActor)
  291. const threadMovedEvent = new ThreadMovedEvent({
  292. ...genericEventFields(event),
  293. thread,
  294. oldCategory: new ForumCategory({ id: oldCategoryId.toString() }),
  295. newCategory: new ForumCategory({ id: newCategoryId.toString() }),
  296. actor: actorWorker,
  297. })
  298. await store.save<ThreadMovedEvent>(threadMovedEvent)
  299. thread.updatedAt = eventTime
  300. thread.category = new ForumCategory({ id: newCategoryId.toString() })
  301. await store.save<ForumThread>(thread)
  302. }
  303. export async function forum_VoteOnPoll({ event, store }: EventContext & StoreContext): Promise<void> {
  304. const [threadId, alternativeIndex, forumUserId] = new Forum.VoteOnPollEvent(event).params
  305. const pollAlternative = await getPollAlternative(store, threadId.toString(), alternativeIndex.toNumber())
  306. const votingMember = new Membership({ id: forumUserId.toString() })
  307. const voteOnPollEvent = new VoteOnPollEvent({
  308. ...genericEventFields(event),
  309. pollAlternative,
  310. votingMember,
  311. })
  312. await store.save<VoteOnPollEvent>(voteOnPollEvent)
  313. }
  314. export async function forum_PostAdded({ event, store }: EventContext & StoreContext): Promise<void> {
  315. const [postId, forumUserId, , threadId, metadataBytes, isEditable] = new Forum.PostAddedEvent(event).params
  316. const eventTime = new Date(event.blockTimestamp)
  317. const metadata = deserializeMetadata(ForumPostMetadata, metadataBytes)
  318. const postText = metadata ? metadata.text || '' : bytesToString(metadataBytes)
  319. const repliesToPost =
  320. typeof metadata?.repliesTo === 'number' &&
  321. (await store.get(ForumPost, { where: { id: metadata.repliesTo.toString() } }))
  322. const postStatus = isEditable.valueOf() ? new PostStatusActive() : new PostStatusLocked()
  323. const postOrigin = new PostOriginThreadReply()
  324. const post = new ForumPost({
  325. id: postId.toString(),
  326. createdAt: eventTime,
  327. updatedAt: eventTime,
  328. text: postText,
  329. thread: new ForumThread({ id: threadId.toString() }),
  330. status: postStatus,
  331. author: new Membership({ id: forumUserId.toString() }),
  332. origin: postOrigin,
  333. repliesTo: repliesToPost || undefined,
  334. })
  335. await store.save<ForumPost>(post)
  336. const postAddedEvent = new PostAddedEvent({
  337. ...genericEventFields(event),
  338. post,
  339. isEditable: isEditable.valueOf(),
  340. text: postText,
  341. })
  342. await store.save<PostAddedEvent>(postAddedEvent)
  343. // Update the other side of cross-relationship
  344. postOrigin.postAddedEventId = postAddedEvent.id
  345. await store.save<ForumPost>(post)
  346. }
  347. export async function forum_CategoryStickyThreadUpdate({ event, store }: EventContext & StoreContext): Promise<void> {
  348. const [categoryId, newStickyThreadsIdsVec, privilegedActor] = new Forum.CategoryStickyThreadUpdateEvent(event).params
  349. const eventTime = new Date(event.blockTimestamp)
  350. const actorWorker = await getActorWorker(store, privilegedActor)
  351. const newStickyThreadsIds = newStickyThreadsIdsVec.map((id) => id.toString())
  352. const threadsToSetSticky = await store.getMany(ForumThread, {
  353. where: { category: { id: categoryId.toString() }, id: In(newStickyThreadsIds) },
  354. })
  355. const threadsToUnsetSticky = await store.getMany(ForumThread, {
  356. where: { category: { id: categoryId.toString() }, isSticky: true, id: Not(In(newStickyThreadsIds)) },
  357. })
  358. const setStickyUpdates = (threadsToSetSticky || []).map(async (t) => {
  359. t.updatedAt = eventTime
  360. t.isSticky = true
  361. await store.save<ForumThread>(t)
  362. })
  363. const unsetStickyUpdates = (threadsToUnsetSticky || []).map(async (t) => {
  364. t.updatedAt = eventTime
  365. t.isSticky = false
  366. await store.save<ForumThread>(t)
  367. })
  368. await Promise.all(setStickyUpdates.concat(unsetStickyUpdates))
  369. const categoryStickyThreadUpdateEvent = new CategoryStickyThreadUpdateEvent({
  370. ...genericEventFields(event),
  371. actor: actorWorker,
  372. category: new ForumCategory({ id: categoryId.toString() }),
  373. newStickyThreads: threadsToSetSticky,
  374. })
  375. await store.save<CategoryStickyThreadUpdateEvent>(categoryStickyThreadUpdateEvent)
  376. }
  377. export async function forum_CategoryMembershipOfModeratorUpdated({
  378. store,
  379. event,
  380. }: EventContext & StoreContext): Promise<void> {
  381. const [moderatorId, categoryId, canModerate] = new Forum.CategoryMembershipOfModeratorUpdatedEvent(event).params
  382. const eventTime = new Date(event.blockTimestamp)
  383. const moderator = await getWorker(store, 'forumWorkingGroup', moderatorId.toNumber())
  384. const category = await getCategory(store, categoryId.toString(), ['moderators'])
  385. if (canModerate.valueOf()) {
  386. category.moderators.push(moderator)
  387. category.updatedAt = eventTime
  388. await store.save<ForumCategory>(category)
  389. } else {
  390. category.moderators.splice(category.moderators.map((m) => m.id).indexOf(moderator.id), 1)
  391. category.updatedAt = eventTime
  392. await store.save<ForumCategory>(category)
  393. }
  394. const categoryMembershipOfModeratorUpdatedEvent = new CategoryMembershipOfModeratorUpdatedEvent({
  395. ...genericEventFields(event),
  396. category,
  397. moderator,
  398. newCanModerateValue: canModerate.valueOf(),
  399. })
  400. await store.save<CategoryMembershipOfModeratorUpdatedEvent>(categoryMembershipOfModeratorUpdatedEvent)
  401. }
  402. export async function forum_PostModerated({ event, store }: EventContext & StoreContext): Promise<void> {
  403. const [postId, rationaleBytes, privilegedActor] = new Forum.PostModeratedEvent(event).params
  404. const eventTime = new Date(event.blockTimestamp)
  405. const actorWorker = await getActorWorker(store, privilegedActor)
  406. const post = await getPost(store, postId.toString())
  407. const postModeratedEvent = new PostModeratedEvent({
  408. ...genericEventFields(event),
  409. actor: actorWorker,
  410. post,
  411. rationale: bytesToString(rationaleBytes),
  412. })
  413. await store.save<PostModeratedEvent>(postModeratedEvent)
  414. const newStatus = new PostStatusModerated()
  415. newStatus.postModeratedEventId = postModeratedEvent.id
  416. post.updatedAt = eventTime
  417. post.status = newStatus
  418. await store.save<ForumPost>(post)
  419. }
  420. export async function forum_PostReacted({ event, store }: EventContext & StoreContext): Promise<void> {
  421. const [userId, postId, reactionId] = new Forum.PostReactedEvent(event).params
  422. const eventTime = new Date(event.blockTimestamp)
  423. const reactionResult = parseReaction(reactionId)
  424. const postReactedEvent = new PostReactedEvent({
  425. ...genericEventFields(event),
  426. post: new ForumPost({ id: postId.toString() }),
  427. reactingMember: new Membership({ id: userId.toString() }),
  428. reactionResult,
  429. })
  430. await store.save<PostReactedEvent>(postReactedEvent)
  431. const existingUserPostReaction = await store.get(ForumPostReaction, {
  432. where: { post: { id: postId.toString() }, member: { id: userId.toString() } },
  433. })
  434. if (reactionResult.isTypeOf === 'PostReactionResultValid') {
  435. const { reaction } = reactionResult as PostReactionResultValid
  436. if (existingUserPostReaction) {
  437. existingUserPostReaction.updatedAt = eventTime
  438. existingUserPostReaction.reaction = reaction
  439. await store.save<ForumPostReaction>(existingUserPostReaction)
  440. } else {
  441. const newUserPostReaction = new ForumPostReaction({
  442. createdAt: eventTime,
  443. updatedAt: eventTime,
  444. post: new ForumPost({ id: postId.toString() }),
  445. member: new Membership({ id: userId.toString() }),
  446. reaction,
  447. })
  448. await store.save<ForumPostReaction>(newUserPostReaction)
  449. }
  450. } else if (existingUserPostReaction) {
  451. await store.remove<ForumPostReaction>(existingUserPostReaction)
  452. }
  453. }
  454. export async function forum_PostTextUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
  455. const [postId, , , , newTextBytes] = new Forum.PostTextUpdatedEvent(event).params
  456. const eventTime = new Date(event.blockTimestamp)
  457. const post = await getPost(store, postId.toString())
  458. const postTextUpdatedEvent = new PostTextUpdatedEvent({
  459. ...genericEventFields(event),
  460. post,
  461. newText: bytesToString(newTextBytes),
  462. })
  463. await store.save<PostTextUpdatedEvent>(postTextUpdatedEvent)
  464. post.updatedAt = eventTime
  465. post.text = bytesToString(newTextBytes)
  466. await store.save<ForumPost>(post)
  467. }
  468. export async function forum_PostDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
  469. const [rationaleBytes, userId, postsData] = new Forum.PostDeletedEvent(event).params
  470. const eventTime = new Date(event.blockTimestamp)
  471. const postDeletedEvent = new PostDeletedEvent({
  472. ...genericEventFields(event),
  473. actor: new Membership({ id: userId.toString() }),
  474. rationale: bytesToString(rationaleBytes),
  475. })
  476. await store.save<PostDeletedEvent>(postDeletedEvent)
  477. await Promise.all(
  478. postsData.map(async ([, , postId, hideFlag]) => {
  479. const post = await getPost(store, postId.toString())
  480. const newStatus = hideFlag.valueOf() ? new PostStatusRemoved() : new PostStatusLocked()
  481. newStatus.postDeletedEventId = postDeletedEvent.id
  482. post.updatedAt = eventTime
  483. post.status = newStatus
  484. post.deletedInEvent = postDeletedEvent
  485. await store.save<ForumPost>(post)
  486. })
  487. )
  488. }