mappingsContent.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  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. /////////////////// ChannelCategory ////////////////////////////////////////////
  310. // eslint-disable-next-line @typescript-eslint/naming-convention
  311. export async function content_ChannelCategoryCreated(
  312. db: DatabaseManager,
  313. event: SubstrateEvent
  314. ) {
  315. const {channelCategoryCreationParameters} = new Content.ChannelCategoryCreatedEvent(event).data
  316. const protobufContent = await readProtobuf(
  317. new ChannelCategory(),
  318. channelCategoryCreationParameters.meta,
  319. [],
  320. db,
  321. event,
  322. )
  323. const channelCategory = new ChannelCategory({
  324. id: event.params[0].value.toString(), // ChannelCategoryId
  325. channels: [],
  326. happenedIn: await prepareBlock(db, event),
  327. ...Object(protobufContent)
  328. })
  329. await db.save<ChannelCategory>(channelCategory)
  330. }
  331. // eslint-disable-next-line @typescript-eslint/naming-convention
  332. export async function content_ChannelCategoryUpdated(
  333. db: DatabaseManager,
  334. event: SubstrateEvent
  335. ) {
  336. const {channelCategoryId, channelCategoryUpdateParameters} = new Content.ChannelCategoryUpdatedEvent(event).data
  337. const channelCategory = await db.get(ChannelCategory, { where: { id: channelCategoryId } })
  338. if (!channelCategory) {
  339. return inconsistentState()
  340. }
  341. const protobufContent = await readProtobuf(
  342. new ChannelCategory(),
  343. channelCategoryUpdateParameters.new_meta,
  344. [],
  345. db,
  346. event,
  347. )
  348. // update all fields read from protobuf
  349. for (let [key, value] of Object(protobufContent).entries()) {
  350. channelCategory[key] = value
  351. }
  352. await db.save<ChannelCategory>(channelCategory)
  353. }
  354. // eslint-disable-next-line @typescript-eslint/naming-convention
  355. export async function content_ChannelCategoryDeleted(
  356. db: DatabaseManager,
  357. event: SubstrateEvent
  358. ) {
  359. const {channelCategoryId} = new Content.ChannelCategoryDeletedEvent(event).data
  360. const channelCategory = await db.get(ChannelCategory, { where: { id: channelCategoryId } })
  361. if (!channelCategory) {
  362. return inconsistentState()
  363. }
  364. await db.remove<ChannelCategory>(channelCategory)
  365. }
  366. /////////////////// VideoCategory //////////////////////////////////////////////
  367. // eslint-disable-next-line @typescript-eslint/naming-convention
  368. export async function content_VideoCategoryCreated(
  369. db: DatabaseManager,
  370. event: SubstrateEvent
  371. ) {
  372. const {videoCategoryId, videoCategoryCreationParameters} = new Content.VideoCategoryCreatedEvent(event).data
  373. const protobufContent = readProtobuf(
  374. new VideoCategory(),
  375. videoCategoryCreationParameters.meta,
  376. [],
  377. db,
  378. event
  379. )
  380. const videoCategory = new VideoCategory({
  381. id: videoCategoryId.toString(), // ChannelId
  382. isCensored: false,
  383. videos: [],
  384. happenedIn: await prepareBlock(db, event),
  385. ...Object(protobufContent)
  386. })
  387. await db.save<VideoCategory>(videoCategory)
  388. }
  389. // eslint-disable-next-line @typescript-eslint/naming-convention
  390. export async function content_VideoCategoryUpdated(
  391. db: DatabaseManager,
  392. event: SubstrateEvent
  393. ) {
  394. const {videoCategoryId, videoCategoryUpdateParameters} = new Content.VideoCategoryUpdatedEvent(event).data
  395. const videoCategory = await db.get(VideoCategory, { where: { id: videoCategoryId } })
  396. if (!videoCategory) {
  397. return inconsistentState()
  398. }
  399. const protobufContent = await readProtobuf(
  400. new VideoCategory(),
  401. videoCategoryUpdateParameters.new_meta,
  402. [],
  403. db,
  404. event,
  405. )
  406. // update all fields read from protobuf
  407. for (let [key, value] of Object(protobufContent).entries()) {
  408. videoCategory[key] = value
  409. }
  410. await db.save<VideoCategory>(videoCategory)
  411. }
  412. // eslint-disable-next-line @typescript-eslint/naming-convention
  413. export async function content_VideoCategoryDeleted(
  414. db: DatabaseManager,
  415. event: SubstrateEvent
  416. ) {
  417. const {videoCategoryId} = new Content.VideoCategoryDeletedEvent(event).data
  418. const videoCategory = await db.get(VideoCategory, { where: { id: videoCategoryId } })
  419. if (!videoCategory) {
  420. return inconsistentState()
  421. }
  422. await db.remove<VideoCategory>(videoCategory)
  423. }
  424. /////////////////// Video //////////////////////////////////////////////////////
  425. // eslint-disable-next-line @typescript-eslint/naming-convention
  426. export async function content_VideoCreated(
  427. db: DatabaseManager,
  428. event: SubstrateEvent
  429. ) {
  430. const {channelId, videoId, videoCreationParameters} = new Content.VideoCreatedEvent(event).data
  431. const protobufContent = await readProtobuf(
  432. new Video(),
  433. videoCreationParameters.meta,
  434. videoCreationParameters.assets,
  435. db,
  436. event,
  437. )
  438. const channel = new Video({
  439. id: videoId,
  440. isCensored: false,
  441. channel: channelId,
  442. happenedIn: await prepareBlock(db, event),
  443. ...Object(protobufContent)
  444. })
  445. await db.save<Video>(channel)
  446. }
  447. // eslint-disable-next-line @typescript-eslint/naming-convention
  448. export async function content_VideoUpdated(
  449. db: DatabaseManager,
  450. event: SubstrateEvent
  451. ) {
  452. const {videoId, videoUpdateParameters} = new Content.VideoUpdatedEvent(event).data
  453. const video = await db.get(Video, { where: { id: videoId } })
  454. if (!video) {
  455. return inconsistentState()
  456. }
  457. if (videoUpdateParameters.new_meta.isSome) {
  458. const protobufContent = await readProtobuf(
  459. new Video(),
  460. videoUpdateParameters.new_meta.unwrap(), // TODO: is there any better way to get value without unwrap?
  461. videoUpdateParameters.assets.unwrapOr([]),
  462. db,
  463. event,
  464. )
  465. // update all fields read from protobuf
  466. for (let [key, value] of Object(protobufContent).entries()) {
  467. video[key] = value
  468. }
  469. }
  470. await db.save<Video>(video)
  471. }
  472. // eslint-disable-next-line @typescript-eslint/naming-convention
  473. export async function content_VideoDeleted(
  474. db: DatabaseManager,
  475. event: SubstrateEvent
  476. ) {
  477. const {videoId} = new Content.VideoDeletedEvent(event).data
  478. const video = await db.get(Video, { where: { id: videoId } })
  479. if (!video) {
  480. return inconsistentState()
  481. }
  482. await db.remove<Video>(video)
  483. }
  484. // eslint-disable-next-line @typescript-eslint/naming-convention
  485. export async function content_VideoCensored(
  486. db: DatabaseManager,
  487. event: SubstrateEvent
  488. ) {
  489. const {videoId} = new Content.VideoCensoredEvent(event).data
  490. const video = await db.get(Video, { where: { id: videoId } })
  491. if (!video) {
  492. return inconsistentState()
  493. }
  494. video.isCensored = true;
  495. await db.save<Video>(video)
  496. }
  497. // eslint-disable-next-line @typescript-eslint/naming-convention
  498. export async function content_VideoUncensored(
  499. db: DatabaseManager,
  500. event: SubstrateEvent
  501. ) {
  502. const {videoId} = new Content.VideoUncensoredEvent(event).data
  503. const video = await db.get(Video, { where: { id: videoId } })
  504. if (!video) {
  505. return inconsistentState()
  506. }
  507. video.isCensored = false;
  508. await db.save<Video>(video)
  509. }
  510. // eslint-disable-next-line @typescript-eslint/naming-convention
  511. export async function content_FeaturedVideosSet(
  512. db: DatabaseManager,
  513. event: SubstrateEvent
  514. ) {
  515. const {videoId: videoIds} = new Content.FeaturedVideosSetEvent(event).data
  516. const existingFeaturedVideos = await db.getMany(Video, { where: { isFeatured: true } })
  517. // comparsion utility
  518. const isSame = (videoIdA: string) => (videoIdB: string) => videoIdA == videoIdB
  519. // calculate diff sets
  520. const toRemove = existingFeaturedVideos.filter(existingFV =>
  521. !videoIds
  522. .map(item => item.toHex())
  523. .some(isSame(existingFV.id))
  524. )
  525. const toAdd = videoIds.filter(video =>
  526. !existingFeaturedVideos
  527. .map(item => item.id)
  528. .some(isSame(video.toHex()))
  529. )
  530. // mark previously featured videos as not-featured
  531. for (let video of toRemove) {
  532. video.isFeatured = false;
  533. await db.save<Video>(video)
  534. }
  535. // escape if no featured video needs to be added
  536. if (!toAdd) {
  537. return
  538. }
  539. // read videos previously not-featured videos that are meant to be featured
  540. const videosToAdd = await db.getMany(Video, { where: { id: [toAdd] } })
  541. if (videosToAdd.length != toAdd.length) {
  542. return inconsistentState()
  543. }
  544. // mark previously not-featured videos as featured
  545. for (let video of videosToAdd) {
  546. video.isFeatured = true;
  547. await db.save<Video>(video)
  548. }
  549. }