mappingsContent.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. // TODO: add logging of mapping events (entity found/not found, entity updated/deleted, etc.)
  2. // TODO: split file into multiple files
  3. // TODO: make sure assets are updated when VideoUpdateParameters have only `assets` parameter set (no `new_meta` set) - if this situation can even happend
  4. import { SubstrateEvent } from '@dzlzv/hydra-common'
  5. import { DatabaseManager } from '@dzlzv/hydra-db-utils'
  6. // protobuf definitions
  7. import {
  8. ChannelMetadata,
  9. ChannelCategoryMetadata,
  10. PublishedBeforeJoystream as PublishedBeforeJoystreamMetadata,
  11. License as LicenseMetadata,
  12. MediaType as MediaTypeMetadata,
  13. VideoMetadata,
  14. VideoCategoryMetadata,
  15. } from '@joystream/content-metadata-protobuf'
  16. import {
  17. Content,
  18. } from '../generated/types'
  19. /* TODO: can it be imported nicely like this?
  20. import {
  21. // primary entites
  22. Network,
  23. Block,
  24. Channel,
  25. ChannelCategory,
  26. Video,
  27. VideoCategory,
  28. // secondary entities
  29. Language,
  30. License,
  31. MediaType,
  32. VideoMediaEncoding,
  33. VideoMediaMetadata,
  34. // Asset
  35. Asset,
  36. AssetUrl,
  37. AssetUploadStatus,
  38. AssetDataObject,
  39. LiaisonJudgement,
  40. AssetStorage,
  41. AssetOwner,
  42. AssetOwnerMember,
  43. } from 'query-node'
  44. */
  45. import {
  46. inconsistentState,
  47. prepareBlock,
  48. prepareAssetDataObject,
  49. } from './common'
  50. // primary entities
  51. import { Block } from 'query-node/src/modules/block/block.model'
  52. import { Channel } from 'query-node/src/modules/channel/channel.model'
  53. import { ChannelCategory } from 'query-node/src/modules/channel-category/channel-category.model'
  54. import { Video } from 'query-node/src/modules/video/video.model'
  55. import { VideoCategory } from 'query-node/src/modules/video-category/video-category.model'
  56. // secondary entities
  57. import { Language } from 'query-node/src/modules/language/language.model'
  58. import { License } from 'query-node/src/modules/license/license.model'
  59. import { VideoMediaEncoding } from 'query-node/src/modules/video-media-encoding/video-media-encoding.model'
  60. import { VideoMediaMetadata } from 'query-node/src/modules/video-media-metadata/video-media-metadata.model'
  61. // Asset
  62. import {
  63. Asset,
  64. AssetUrl,
  65. AssetUploadStatus,
  66. AssetStorage,
  67. AssetOwner,
  68. AssetOwnerMember,
  69. } from 'query-node/src/modules/variants/variants.model'
  70. import {
  71. AssetDataObject,
  72. LiaisonJudgement
  73. } from 'query-node/src/modules/asset-data-object/asset-data-object.model'
  74. // Joystream types
  75. import {
  76. ContentParameters,
  77. NewAsset,
  78. } from '@joystream/types/augment'
  79. /////////////////// Utils //////////////////////////////////////////////////////
  80. async function readProtobuf(
  81. type: Channel | ChannelCategory | Video | VideoCategory,
  82. metadata: Uint8Array,
  83. assets: NewAsset[],
  84. db: DatabaseManager,
  85. event: SubstrateEvent,
  86. ): Promise<Partial<typeof type>> {
  87. // process channel
  88. if (type instanceof Channel) {
  89. const meta = ChannelMetadata.deserializeBinary(metadata)
  90. const metaAsObject = meta.toObject()
  91. const result = metaAsObject as any as Channel
  92. // prepare cover photo asset if needed
  93. if (metaAsObject.coverPhoto !== undefined) {
  94. result.coverPhoto = await extractAsset(metaAsObject.coverPhoto, assets, db, event)
  95. }
  96. // prepare avatar photo asset if needed
  97. if (metaAsObject.avatarPhoto !== undefined) {
  98. result.avatarPhoto = await extractAsset(metaAsObject.avatarPhoto, assets, db, event)
  99. }
  100. // prepare language if needed
  101. if (metaAsObject.language) {
  102. result.language = await prepareLanguage(metaAsObject.language, db)
  103. }
  104. return result
  105. }
  106. // process channel category
  107. if (type instanceof ChannelCategory) {
  108. return ChannelCategoryMetadata.deserializeBinary(metadata).toObject()
  109. }
  110. // process video
  111. if (type instanceof Video) {
  112. const meta = VideoMetadata.deserializeBinary(metadata)
  113. const metaAsObject = meta.toObject()
  114. const result = metaAsObject as any as Video
  115. // prepare video category if needed
  116. if (metaAsObject.category !== undefined) {
  117. result.category = await prepareVideoCategory(metaAsObject.category, db)
  118. }
  119. // prepare media meta information if needed
  120. if (metaAsObject.mediaType) {
  121. result.mediaMetadata = await prepareVideoMetadata(metaAsObject)
  122. delete metaAsObject.mediaType
  123. }
  124. // prepare license if needed
  125. if (metaAsObject.license) {
  126. result.license = await prepareLicense(metaAsObject.license)
  127. }
  128. // prepare thumbnail photo asset if needed
  129. if (metaAsObject.thumbnailPhoto !== undefined) {
  130. result.thumbnailPhoto = await extractAsset(metaAsObject.thumbnailPhoto, assets, db, event)
  131. }
  132. // prepare video asset if needed
  133. if (metaAsObject.video !== undefined) {
  134. result.media = await extractAsset(metaAsObject.video, assets, db, event)
  135. }
  136. // prepare language if needed
  137. if (metaAsObject.language) {
  138. result.language = await prepareLanguage(metaAsObject.language, db)
  139. }
  140. // prepare information about media published somewhere else before Joystream if needed.
  141. if (metaAsObject.publishedBeforeJoystream) {
  142. // TODO: is ok to just ignore `isPublished?: boolean` here?
  143. if (metaAsObject.publishedBeforeJoystream.date) {
  144. result.publishedBeforeJoystream = new Date(metaAsObject.publishedBeforeJoystream.date)
  145. } else {
  146. delete result.publishedBeforeJoystream
  147. }
  148. }
  149. return result
  150. }
  151. // process video category
  152. if (type instanceof VideoCategory) {
  153. return VideoCategoryMetadata.deserializeBinary(metadata).toObject()
  154. }
  155. // this should never happen
  156. throw `Not implemented type: ${type}`
  157. }
  158. async function convertAsset(rawAsset: NewAsset, db: DatabaseManager, event: SubstrateEvent): Promise<typeof Asset> {
  159. if (rawAsset.isUrls) {
  160. const assetUrl = new AssetUrl()
  161. assetUrl.url = rawAsset.asUrls.toArray()[0].toString() // TODO: find out why asUrl() returns array
  162. return assetUrl
  163. }
  164. // !rawAsset.isUrls && rawAsset.isUpload
  165. const contentParameters: ContentParameters = rawAsset.asUpload
  166. const block = await prepareBlock(db, event)
  167. const assetStorage = await prepareAssetDataObject(contentParameters, block)
  168. return assetStorage
  169. }
  170. async function extractAsset(
  171. assetIndex: number | undefined,
  172. assets: NewAsset[],
  173. db: DatabaseManager,
  174. event: SubstrateEvent,
  175. ): Promise<typeof Asset | undefined> {
  176. if (assetIndex === undefined) {
  177. return undefined
  178. }
  179. if (assetIndex > assets.length) {
  180. throw 'Inconsistent state' // TODO: more sophisticated inconsistency handling; unify handling with other critical errors
  181. }
  182. return convertAsset(assets[assetIndex], db, event)
  183. }
  184. async function prepareLanguage(languageIso: string, db: DatabaseManager): Promise<Language> {
  185. // TODO: ensure language is ISO name
  186. const isValidIso = true;
  187. if (!isValidIso) {
  188. throw 'Inconsistent state' // TODO: create a proper way of handling inconsistent state
  189. }
  190. const language = await db.get(Language, { where: { iso: languageIso }})
  191. if (language) {
  192. return language;
  193. }
  194. const newLanguage = new Language({
  195. iso: languageIso
  196. })
  197. return newLanguage
  198. }
  199. async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject): Promise<License> {
  200. // TODO: add old license removal (when existing) or rework the whole function
  201. const license = new License(licenseProtobuf)
  202. return license
  203. }
  204. async function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject): Promise<VideoMediaMetadata> {
  205. const encoding = new VideoMediaEncoding(videoProtobuf.mediaType)
  206. const videoMeta = new VideoMediaMetadata({
  207. encoding,
  208. pixelWidth: videoProtobuf.mediaPixelWidth,
  209. pixelHeight: videoProtobuf.mediaPixelHeight,
  210. size: 0, // TODO: retrieve proper file size
  211. })
  212. return videoMeta
  213. }
  214. async function prepareVideoCategory(categoryId: number, db: DatabaseManager): Promise<VideoCategory> {
  215. const category = await db.get(VideoCategory, { where: { id: categoryId }})
  216. if (!category) {
  217. throw 'Inconsistent state' // TODO: create a proper way of handling inconsistent state
  218. }
  219. return category
  220. }
  221. /////////////////// Channel ////////////////////////////////////////////////////
  222. // eslint-disable-next-line @typescript-eslint/naming-convention
  223. export async function content_ChannelCreated(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
  224. const {channelId, channelCreationParameters} = new Content.ChannelCreatedEvent(event).data
  225. const protobufContent = await readProtobuf(
  226. new Channel(),
  227. channelCreationParameters.meta,
  228. channelCreationParameters.assets,
  229. db,
  230. event,
  231. )
  232. const channel = new Channel({
  233. id: channelId,
  234. isCensored: false,
  235. videos: [],
  236. happenedIn: await prepareBlock(db, event),
  237. ...Object(protobufContent)
  238. })
  239. await db.save<Channel>(channel)
  240. }
  241. // eslint-disable-next-line @typescript-eslint/naming-convention
  242. export async function content_ChannelUpdated(
  243. db: DatabaseManager,
  244. event: SubstrateEvent
  245. ) {
  246. const {channelId , channelUpdateParameters} = new Content.ChannelUpdatedEvent(event).data
  247. const channel = await db.get(Channel, { where: { id: channelId } })
  248. if (!channel) {
  249. return inconsistentState()
  250. }
  251. // metadata change happened?
  252. if (channelUpdateParameters.new_meta.isSome) {
  253. const protobufContent = await readProtobuf(
  254. new Channel(),
  255. channelUpdateParameters.new_meta.unwrap(), // TODO: is there any better way to get value without unwrap?
  256. channelUpdateParameters.assets.unwrapOr([]),
  257. db,
  258. event,
  259. )
  260. // update all fields read from protobuf
  261. for (let [key, value] of Object(protobufContent).entries()) {
  262. channel[key] = value
  263. }
  264. }
  265. // reward account change happened?
  266. if (channelUpdateParameters.reward_account.isSome) {
  267. // TODO: separate to function
  268. // new different reward account set
  269. if (channelUpdateParameters.reward_account.unwrap().isSome) {
  270. channel.rewardAccount = channelUpdateParameters.reward_account.unwrap().unwrap().toString()
  271. } else { // reward account removed
  272. delete channel.rewardAccount
  273. }
  274. }
  275. await db.save<Channel>(channel)
  276. }
  277. export async function content_ChannelAssetsRemoved(
  278. db: DatabaseManager,
  279. event: SubstrateEvent
  280. ) {
  281. // TODO - what should happen here?
  282. }
  283. // eslint-disable-next-line @typescript-eslint/naming-convention
  284. export async function content_ChannelCensored(
  285. db: DatabaseManager,
  286. event: SubstrateEvent
  287. ) {
  288. const channelId = event.params[1].value.toString()
  289. const channel = await db.get(Channel, { where: { id: channelId } })
  290. if (!channel) {
  291. return inconsistentState()
  292. }
  293. channel.isCensored = true;
  294. await db.save<Channel>(channel)
  295. }
  296. // eslint-disable-next-line @typescript-eslint/naming-convention
  297. export async function content_ChannelUncensored(
  298. db: DatabaseManager,
  299. event: SubstrateEvent
  300. ) {
  301. const channelId = event.params[1].value.toString()
  302. const channel = await db.get(Channel, { where: { id: channelId } })
  303. if (!channel) {
  304. return inconsistentState()
  305. }
  306. channel.isCensored = false;
  307. await db.save<Channel>(channel)
  308. }
  309. // eslint-disable-next-line @typescript-eslint/naming-convention
  310. export async function content_ChannelOwnershipTransferRequested(
  311. db: DatabaseManager,
  312. event: SubstrateEvent
  313. ) {
  314. // TODO - is mapping for this event needed?
  315. }
  316. // eslint-disable-next-line @typescript-eslint/naming-convention
  317. export async function content_ChannelOwnershipTransferRequestWithdrawn(
  318. db: DatabaseManager,
  319. event: SubstrateEvent
  320. ) {
  321. // TODO - is mapping for this event needed?
  322. }
  323. // eslint-disable-next-line @typescript-eslint/naming-convention
  324. export async function content_ChannelOwnershipTransferred(
  325. db: DatabaseManager,
  326. event: SubstrateEvent
  327. ) {
  328. // TODO
  329. }
  330. /////////////////// ChannelCategory ////////////////////////////////////////////
  331. // eslint-disable-next-line @typescript-eslint/naming-convention
  332. export async function content_ChannelCategoryCreated(
  333. db: DatabaseManager,
  334. event: SubstrateEvent
  335. ) {
  336. const {channelCategoryCreationParameters} = new Content.ChannelCategoryCreatedEvent(event).data
  337. const protobufContent = await readProtobuf(
  338. new ChannelCategory(),
  339. channelCategoryCreationParameters.meta,
  340. [],
  341. db,
  342. event,
  343. )
  344. const channelCategory = new ChannelCategory({
  345. id: event.params[0].value.toString(), // ChannelCategoryId
  346. channels: [],
  347. happenedIn: await prepareBlock(db, event),
  348. ...Object(protobufContent)
  349. })
  350. await db.save<ChannelCategory>(channelCategory)
  351. }
  352. // eslint-disable-next-line @typescript-eslint/naming-convention
  353. export async function content_ChannelCategoryUpdated(
  354. db: DatabaseManager,
  355. event: SubstrateEvent
  356. ) {
  357. const {channelCategoryId, channelCategoryUpdateParameters} = new Content.ChannelCategoryUpdatedEvent(event).data
  358. const channelCategory = await db.get(ChannelCategory, { where: { id: channelCategoryId } })
  359. if (!channelCategory) {
  360. return inconsistentState()
  361. }
  362. const protobufContent = await readProtobuf(
  363. new ChannelCategory(),
  364. channelCategoryUpdateParameters.new_meta,
  365. [],
  366. db,
  367. event,
  368. )
  369. // update all fields read from protobuf
  370. for (let [key, value] of Object(protobufContent).entries()) {
  371. channelCategory[key] = value
  372. }
  373. await db.save<ChannelCategory>(channelCategory)
  374. }
  375. // eslint-disable-next-line @typescript-eslint/naming-convention
  376. export async function content_ChannelCategoryDeleted(
  377. db: DatabaseManager,
  378. event: SubstrateEvent
  379. ) {
  380. const {channelCategoryId} = new Content.ChannelCategoryDeletedEvent(event).data
  381. const channelCategory = await db.get(ChannelCategory, { where: { id: channelCategoryId } })
  382. if (!channelCategory) {
  383. return inconsistentState()
  384. }
  385. await db.remove<ChannelCategory>(channelCategory)
  386. }
  387. /////////////////// VideoCategory //////////////////////////////////////////////
  388. // eslint-disable-next-line @typescript-eslint/naming-convention
  389. export async function content_VideoCategoryCreated(
  390. db: DatabaseManager,
  391. event: SubstrateEvent
  392. ) {
  393. const {videoCategoryId, videoCategoryCreationParameters} = new Content.VideoCategoryCreatedEvent(event).data
  394. const protobufContent = readProtobuf(
  395. new VideoCategory(),
  396. videoCategoryCreationParameters.meta,
  397. [],
  398. db,
  399. event
  400. )
  401. const videoCategory = new VideoCategory({
  402. id: videoCategoryId.toString(), // ChannelId
  403. isCensored: false,
  404. videos: [],
  405. happenedIn: await prepareBlock(db, event),
  406. ...Object(protobufContent)
  407. })
  408. await db.save<VideoCategory>(videoCategory)
  409. }
  410. // eslint-disable-next-line @typescript-eslint/naming-convention
  411. export async function content_VideoCategoryUpdated(
  412. db: DatabaseManager,
  413. event: SubstrateEvent
  414. ) {
  415. const {videoCategoryId, videoCategoryUpdateParameters} = new Content.VideoCategoryUpdatedEvent(event).data
  416. const videoCategory = await db.get(VideoCategory, { where: { id: videoCategoryId } })
  417. if (!videoCategory) {
  418. return inconsistentState()
  419. }
  420. const protobufContent = await readProtobuf(
  421. new VideoCategory(),
  422. videoCategoryUpdateParameters.new_meta,
  423. [],
  424. db,
  425. event,
  426. )
  427. // update all fields read from protobuf
  428. for (let [key, value] of Object(protobufContent).entries()) {
  429. videoCategory[key] = value
  430. }
  431. await db.save<VideoCategory>(videoCategory)
  432. }
  433. // eslint-disable-next-line @typescript-eslint/naming-convention
  434. export async function content_VideoCategoryDeleted(
  435. db: DatabaseManager,
  436. event: SubstrateEvent
  437. ) {
  438. const {videoCategoryId} = new Content.VideoCategoryDeletedEvent(event).data
  439. const videoCategory = await db.get(VideoCategory, { where: { id: videoCategoryId } })
  440. if (!videoCategory) {
  441. return inconsistentState()
  442. }
  443. await db.remove<VideoCategory>(videoCategory)
  444. }
  445. /////////////////// Video //////////////////////////////////////////////////////
  446. // eslint-disable-next-line @typescript-eslint/naming-convention
  447. export async function content_VideoCreated(
  448. db: DatabaseManager,
  449. event: SubstrateEvent
  450. ) {
  451. const {channelId, videoId, videoCreationParameters} = new Content.VideoCreatedEvent(event).data
  452. const protobufContent = await readProtobuf(
  453. new Video(),
  454. videoCreationParameters.meta,
  455. videoCreationParameters.assets,
  456. db,
  457. event,
  458. )
  459. const channel = new Video({
  460. id: videoId,
  461. isCensored: false,
  462. channel: channelId,
  463. happenedIn: await prepareBlock(db, event),
  464. ...Object(protobufContent)
  465. })
  466. await db.save<Video>(channel)
  467. }
  468. // eslint-disable-next-line @typescript-eslint/naming-convention
  469. export async function content_VideoUpdated(
  470. db: DatabaseManager,
  471. event: SubstrateEvent
  472. ) {
  473. const {videoId, videoUpdateParameters} = new Content.VideoUpdatedEvent(event).data
  474. const video = await db.get(Video, { where: { id: videoId } })
  475. if (!video) {
  476. return inconsistentState()
  477. }
  478. if (videoUpdateParameters.new_meta.isSome) {
  479. const protobufContent = await readProtobuf(
  480. new Video(),
  481. videoUpdateParameters.new_meta.unwrap(), // TODO: is there any better way to get value without unwrap?
  482. videoUpdateParameters.assets.unwrapOr([]),
  483. db,
  484. event,
  485. )
  486. // update all fields read from protobuf
  487. for (let [key, value] of Object(protobufContent).entries()) {
  488. video[key] = value
  489. }
  490. }
  491. await db.save<Video>(video)
  492. }
  493. // eslint-disable-next-line @typescript-eslint/naming-convention
  494. export async function content_VideoDeleted(
  495. db: DatabaseManager,
  496. event: SubstrateEvent
  497. ) {
  498. const {videoId} = new Content.VideoDeletedEvent(event).data
  499. const video = await db.get(Video, { where: { id: videoId } })
  500. if (!video) {
  501. return inconsistentState()
  502. }
  503. await db.remove<Video>(video)
  504. }
  505. // eslint-disable-next-line @typescript-eslint/naming-convention
  506. export async function content_VideoCensored(
  507. db: DatabaseManager,
  508. event: SubstrateEvent
  509. ) {
  510. const {videoId} = new Content.VideoCensoredEvent(event).data
  511. const video = await db.get(Video, { where: { id: videoId } })
  512. if (!video) {
  513. return inconsistentState()
  514. }
  515. video.isCensored = true;
  516. await db.save<Video>(video)
  517. }
  518. // eslint-disable-next-line @typescript-eslint/naming-convention
  519. export async function content_VideoUncensored(
  520. db: DatabaseManager,
  521. event: SubstrateEvent
  522. ) {
  523. const {videoId} = new Content.VideoUncensoredEvent(event).data
  524. const video = await db.get(Video, { where: { id: videoId } })
  525. if (!video) {
  526. return inconsistentState()
  527. }
  528. video.isCensored = false;
  529. await db.save<Video>(video)
  530. }
  531. // eslint-disable-next-line @typescript-eslint/naming-convention
  532. export async function content_FeaturedVideosSet(
  533. db: DatabaseManager,
  534. event: SubstrateEvent
  535. ) {
  536. const {videoId: videoIds} = new Content.FeaturedVideosSetEvent(event).data
  537. const existingFeaturedVideos = await db.getMany(Video, { where: { isFeatured: true } })
  538. // comparsion utility
  539. const isSame = (videoIdA: string) => (videoIdB: string) => videoIdA == videoIdB
  540. // calculate diff sets
  541. const toRemove = existingFeaturedVideos.filter(existingFV =>
  542. !videoIds
  543. .map(item => item.toHex())
  544. .some(isSame(existingFV.id))
  545. )
  546. const toAdd = videoIds.filter(video =>
  547. !existingFeaturedVideos
  548. .map(item => item.id)
  549. .some(isSame(video.toHex()))
  550. )
  551. // mark previously featured videos as not-featured
  552. for (let video of toRemove) {
  553. video.isFeatured = false;
  554. await db.save<Video>(video)
  555. }
  556. // escape if no featured video needs to be added
  557. if (!toAdd) {
  558. return
  559. }
  560. // read videos previously not-featured videos that are meant to be featured
  561. const videosToAdd = await db.getMany(Video, { where: { id: [toAdd] } })
  562. if (videosToAdd.length != toAdd.length) {
  563. return inconsistentState()
  564. }
  565. // mark previously not-featured videos as featured
  566. for (let video of videosToAdd) {
  567. video.isFeatured = true;
  568. await db.save<Video>(video)
  569. }
  570. }