user-notification.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
  2. import { UserNotification, UserNotificationType } from '../../../shared'
  3. import { getSort, throwIfNotValid } from '../utils'
  4. import { isBooleanValid } from '../../helpers/custom-validators/misc'
  5. import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
  6. import { UserModel } from './user'
  7. import { VideoModel } from '../video/video'
  8. import { VideoCommentModel } from '../video/video-comment'
  9. import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
  10. import { VideoChannelModel } from '../video/video-channel'
  11. import { AccountModel } from './account'
  12. import { VideoAbuseModel } from '../video/video-abuse'
  13. import { VideoBlacklistModel } from '../video/video-blacklist'
  14. import { VideoImportModel } from '../video/video-import'
  15. import { ActorModel } from '../activitypub/actor'
  16. import { ActorFollowModel } from '../activitypub/actor-follow'
  17. import { AvatarModel } from '../avatar/avatar'
  18. import { ServerModel } from '../server/server'
  19. enum ScopeNames {
  20. WITH_ALL = 'WITH_ALL'
  21. }
  22. function buildActorWithAvatarInclude () {
  23. return {
  24. attributes: [ 'preferredUsername' ],
  25. model: ActorModel.unscoped(),
  26. required: true,
  27. include: [
  28. {
  29. attributes: [ 'filename' ],
  30. model: AvatarModel.unscoped(),
  31. required: false
  32. },
  33. {
  34. attributes: [ 'host' ],
  35. model: ServerModel.unscoped(),
  36. required: false
  37. }
  38. ]
  39. }
  40. }
  41. function buildVideoInclude (required: boolean) {
  42. return {
  43. attributes: [ 'id', 'uuid', 'name' ],
  44. model: VideoModel.unscoped(),
  45. required
  46. }
  47. }
  48. function buildChannelInclude (required: boolean, withActor = false) {
  49. return {
  50. required,
  51. attributes: [ 'id', 'name' ],
  52. model: VideoChannelModel.unscoped(),
  53. include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
  54. }
  55. }
  56. function buildAccountInclude (required: boolean, withActor = false) {
  57. return {
  58. required,
  59. attributes: [ 'id', 'name' ],
  60. model: AccountModel.unscoped(),
  61. include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
  62. }
  63. }
  64. @Scopes(() => ({
  65. [ScopeNames.WITH_ALL]: {
  66. include: [
  67. Object.assign(buildVideoInclude(false), {
  68. include: [ buildChannelInclude(true, true) ]
  69. }),
  70. {
  71. attributes: [ 'id', 'originCommentId' ],
  72. model: VideoCommentModel.unscoped(),
  73. required: false,
  74. include: [
  75. buildAccountInclude(true, true),
  76. buildVideoInclude(true)
  77. ]
  78. },
  79. {
  80. attributes: [ 'id' ],
  81. model: VideoAbuseModel.unscoped(),
  82. required: false,
  83. include: [ buildVideoInclude(true) ]
  84. },
  85. {
  86. attributes: [ 'id' ],
  87. model: VideoBlacklistModel.unscoped(),
  88. required: false,
  89. include: [ buildVideoInclude(true) ]
  90. },
  91. {
  92. attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
  93. model: VideoImportModel.unscoped(),
  94. required: false,
  95. include: [ buildVideoInclude(false) ]
  96. },
  97. {
  98. attributes: [ 'id', 'state' ],
  99. model: ActorFollowModel.unscoped(),
  100. required: false,
  101. include: [
  102. {
  103. attributes: [ 'preferredUsername' ],
  104. model: ActorModel.unscoped(),
  105. required: true,
  106. as: 'ActorFollower',
  107. include: [
  108. {
  109. attributes: [ 'id', 'name' ],
  110. model: AccountModel.unscoped(),
  111. required: true
  112. },
  113. {
  114. attributes: [ 'filename' ],
  115. model: AvatarModel.unscoped(),
  116. required: false
  117. },
  118. {
  119. attributes: [ 'host' ],
  120. model: ServerModel.unscoped(),
  121. required: false
  122. }
  123. ]
  124. },
  125. {
  126. attributes: [ 'preferredUsername' ],
  127. model: ActorModel.unscoped(),
  128. required: true,
  129. as: 'ActorFollowing',
  130. include: [
  131. buildChannelInclude(false),
  132. buildAccountInclude(false)
  133. ]
  134. }
  135. ]
  136. },
  137. buildAccountInclude(false, true)
  138. ]
  139. }
  140. }))
  141. @Table({
  142. tableName: 'userNotification',
  143. indexes: [
  144. {
  145. fields: [ 'userId' ]
  146. },
  147. {
  148. fields: [ 'videoId' ],
  149. where: {
  150. videoId: {
  151. [Op.ne]: null
  152. }
  153. }
  154. },
  155. {
  156. fields: [ 'commentId' ],
  157. where: {
  158. commentId: {
  159. [Op.ne]: null
  160. }
  161. }
  162. },
  163. {
  164. fields: [ 'videoAbuseId' ],
  165. where: {
  166. videoAbuseId: {
  167. [Op.ne]: null
  168. }
  169. }
  170. },
  171. {
  172. fields: [ 'videoBlacklistId' ],
  173. where: {
  174. videoBlacklistId: {
  175. [Op.ne]: null
  176. }
  177. }
  178. },
  179. {
  180. fields: [ 'videoImportId' ],
  181. where: {
  182. videoImportId: {
  183. [Op.ne]: null
  184. }
  185. }
  186. },
  187. {
  188. fields: [ 'accountId' ],
  189. where: {
  190. accountId: {
  191. [Op.ne]: null
  192. }
  193. }
  194. },
  195. {
  196. fields: [ 'actorFollowId' ],
  197. where: {
  198. actorFollowId: {
  199. [Op.ne]: null
  200. }
  201. }
  202. }
  203. ] as (ModelIndexesOptions & { where?: WhereOptions })[]
  204. })
  205. export class UserNotificationModel extends Model<UserNotificationModel> {
  206. @AllowNull(false)
  207. @Default(null)
  208. @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
  209. @Column
  210. type: UserNotificationType
  211. @AllowNull(false)
  212. @Default(false)
  213. @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
  214. @Column
  215. read: boolean
  216. @CreatedAt
  217. createdAt: Date
  218. @UpdatedAt
  219. updatedAt: Date
  220. @ForeignKey(() => UserModel)
  221. @Column
  222. userId: number
  223. @BelongsTo(() => UserModel, {
  224. foreignKey: {
  225. allowNull: false
  226. },
  227. onDelete: 'cascade'
  228. })
  229. User: UserModel
  230. @ForeignKey(() => VideoModel)
  231. @Column
  232. videoId: number
  233. @BelongsTo(() => VideoModel, {
  234. foreignKey: {
  235. allowNull: true
  236. },
  237. onDelete: 'cascade'
  238. })
  239. Video: VideoModel
  240. @ForeignKey(() => VideoCommentModel)
  241. @Column
  242. commentId: number
  243. @BelongsTo(() => VideoCommentModel, {
  244. foreignKey: {
  245. allowNull: true
  246. },
  247. onDelete: 'cascade'
  248. })
  249. Comment: VideoCommentModel
  250. @ForeignKey(() => VideoAbuseModel)
  251. @Column
  252. videoAbuseId: number
  253. @BelongsTo(() => VideoAbuseModel, {
  254. foreignKey: {
  255. allowNull: true
  256. },
  257. onDelete: 'cascade'
  258. })
  259. VideoAbuse: VideoAbuseModel
  260. @ForeignKey(() => VideoBlacklistModel)
  261. @Column
  262. videoBlacklistId: number
  263. @BelongsTo(() => VideoBlacklistModel, {
  264. foreignKey: {
  265. allowNull: true
  266. },
  267. onDelete: 'cascade'
  268. })
  269. VideoBlacklist: VideoBlacklistModel
  270. @ForeignKey(() => VideoImportModel)
  271. @Column
  272. videoImportId: number
  273. @BelongsTo(() => VideoImportModel, {
  274. foreignKey: {
  275. allowNull: true
  276. },
  277. onDelete: 'cascade'
  278. })
  279. VideoImport: VideoImportModel
  280. @ForeignKey(() => AccountModel)
  281. @Column
  282. accountId: number
  283. @BelongsTo(() => AccountModel, {
  284. foreignKey: {
  285. allowNull: true
  286. },
  287. onDelete: 'cascade'
  288. })
  289. Account: AccountModel
  290. @ForeignKey(() => ActorFollowModel)
  291. @Column
  292. actorFollowId: number
  293. @BelongsTo(() => ActorFollowModel, {
  294. foreignKey: {
  295. allowNull: true
  296. },
  297. onDelete: 'cascade'
  298. })
  299. ActorFollow: ActorFollowModel
  300. static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
  301. const query: FindOptions = {
  302. offset: start,
  303. limit: count,
  304. order: getSort(sort),
  305. where: {
  306. userId
  307. }
  308. }
  309. if (unread !== undefined) query.where['read'] = !unread
  310. return UserNotificationModel.scope(ScopeNames.WITH_ALL)
  311. .findAndCountAll(query)
  312. .then(({ rows, count }) => {
  313. return {
  314. data: rows,
  315. total: count
  316. }
  317. })
  318. }
  319. static markAsRead (userId: number, notificationIds: number[]) {
  320. const query = {
  321. where: {
  322. userId,
  323. id: {
  324. [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
  325. }
  326. }
  327. }
  328. return UserNotificationModel.update({ read: true }, query)
  329. }
  330. static markAllAsRead (userId: number) {
  331. const query = { where: { userId } }
  332. return UserNotificationModel.update({ read: true }, query)
  333. }
  334. toFormattedJSON (): UserNotification {
  335. const video = this.Video
  336. ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
  337. : undefined
  338. const videoImport = this.VideoImport ? {
  339. id: this.VideoImport.id,
  340. video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
  341. torrentName: this.VideoImport.torrentName,
  342. magnetUri: this.VideoImport.magnetUri,
  343. targetUrl: this.VideoImport.targetUrl
  344. } : undefined
  345. const comment = this.Comment ? {
  346. id: this.Comment.id,
  347. threadId: this.Comment.getThreadId(),
  348. account: this.formatActor(this.Comment.Account),
  349. video: this.formatVideo(this.Comment.Video)
  350. } : undefined
  351. const videoAbuse = this.VideoAbuse ? {
  352. id: this.VideoAbuse.id,
  353. video: this.formatVideo(this.VideoAbuse.Video)
  354. } : undefined
  355. const videoBlacklist = this.VideoBlacklist ? {
  356. id: this.VideoBlacklist.id,
  357. video: this.formatVideo(this.VideoBlacklist.Video)
  358. } : undefined
  359. const account = this.Account ? this.formatActor(this.Account) : undefined
  360. const actorFollow = this.ActorFollow ? {
  361. id: this.ActorFollow.id,
  362. state: this.ActorFollow.state,
  363. follower: {
  364. id: this.ActorFollow.ActorFollower.Account.id,
  365. displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
  366. name: this.ActorFollow.ActorFollower.preferredUsername,
  367. avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined,
  368. host: this.ActorFollow.ActorFollower.getHost()
  369. },
  370. following: {
  371. type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
  372. displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
  373. name: this.ActorFollow.ActorFollowing.preferredUsername
  374. }
  375. } : undefined
  376. return {
  377. id: this.id,
  378. type: this.type,
  379. read: this.read,
  380. video,
  381. videoImport,
  382. comment,
  383. videoAbuse,
  384. videoBlacklist,
  385. account,
  386. actorFollow,
  387. createdAt: this.createdAt.toISOString(),
  388. updatedAt: this.updatedAt.toISOString()
  389. }
  390. }
  391. private formatVideo (video: VideoModel) {
  392. return {
  393. id: video.id,
  394. uuid: video.uuid,
  395. name: video.name
  396. }
  397. }
  398. private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
  399. const avatar = accountOrChannel.Actor.Avatar
  400. ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
  401. : undefined
  402. return {
  403. id: accountOrChannel.id,
  404. displayName: accountOrChannel.getDisplayName(),
  405. name: accountOrChannel.Actor.preferredUsername,
  406. host: accountOrChannel.Actor.getHost(),
  407. avatar
  408. }
  409. }
  410. }