]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/account/user-notification.ts
Fix live ending banner
[github/Chocobozzz/PeerTube.git] / server / models / account / user-notification.ts
index 9b13a83763eb572f688665429c639b1d2003abbb..805095002de1b9d07baac518f025990ba934fa4e 100644 (file)
@@ -1,22 +1,26 @@
+import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
 import { UserNotification, UserNotificationType } from '../../../shared'
-import { getSort, throwIfNotValid } from '../utils'
 import { isBooleanValid } from '../../helpers/custom-validators/misc'
 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
-import { UserModel } from './user'
-import { VideoModel } from '../video/video'
-import { VideoCommentModel } from '../video/video-comment'
-import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
-import { VideoChannelModel } from '../video/video-channel'
-import { AccountModel } from './account'
-import { VideoAbuseModel } from '../video/video-abuse'
-import { VideoBlacklistModel } from '../video/video-blacklist'
-import { VideoImportModel } from '../video/video-import'
+import { AbuseModel } from '../abuse/abuse'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
 import { ActorModel } from '../activitypub/actor'
 import { ActorFollowModel } from '../activitypub/actor-follow'
-import { AvatarModel } from '../avatar/avatar'
+import { ApplicationModel } from '../application/application'
+import { PluginModel } from '../server/plugin'
 import { ServerModel } from '../server/server'
-import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/typings/models/user'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoModel } from '../video/video'
+import { VideoBlacklistModel } from '../video/video-blacklist'
+import { VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
+import { VideoImportModel } from '../video/video-import'
+import { AccountModel } from './account'
+import { ActorImageModel } from './actor-image'
+import { UserModel } from './user'
 
 enum ScopeNames {
   WITH_ALL = 'WITH_ALL'
@@ -30,7 +34,8 @@ function buildActorWithAvatarInclude () {
     include: [
       {
         attributes: [ 'filename' ],
-        model: AvatarModel.unscoped(),
+        as: 'Avatar',
+        model: ActorImageModel.unscoped(),
         required: false
       },
       {
@@ -86,10 +91,42 @@ function buildAccountInclude (required: boolean, withActor = false) {
       },
 
       {
-        attributes: [ 'id' ],
-        model: VideoAbuseModel.unscoped(),
+        attributes: [ 'id', 'state' ],
+        model: AbuseModel.unscoped(),
         required: false,
-        include: [ buildVideoInclude(true) ]
+        include: [
+          {
+            attributes: [ 'id' ],
+            model: VideoAbuseModel.unscoped(),
+            required: false,
+            include: [ buildVideoInclude(false) ]
+          },
+          {
+            attributes: [ 'id' ],
+            model: VideoCommentAbuseModel.unscoped(),
+            required: false,
+            include: [
+              {
+                attributes: [ 'id', 'originCommentId' ],
+                model: VideoCommentModel.unscoped(),
+                required: false,
+                include: [
+                  {
+                    attributes: [ 'id', 'name', 'uuid' ],
+                    model: VideoModel.unscoped(),
+                    required: false
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            model: AccountModel,
+            as: 'FlaggedAccount',
+            required: false,
+            include: [ buildActorWithAvatarInclude() ]
+          }
+        ]
       },
 
       {
@@ -106,6 +143,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
         include: [ buildVideoInclude(false) ]
       },
 
+      {
+        attributes: [ 'id', 'name', 'type', 'latestVersion' ],
+        model: PluginModel.unscoped(),
+        required: false
+      },
+
+      {
+        attributes: [ 'id', 'latestPeerTubeVersion' ],
+        model: ApplicationModel.unscoped(),
+        required: false
+      },
+
       {
         attributes: [ 'id', 'state' ],
         model: ActorFollowModel.unscoped(),
@@ -124,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) {
               },
               {
                 attributes: [ 'filename' ],
-                model: AvatarModel.unscoped(),
+                as: 'Avatar',
+                model: ActorImageModel.unscoped(),
                 required: false
               },
               {
@@ -135,13 +185,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
             ]
           },
           {
-            attributes: [ 'preferredUsername' ],
+            attributes: [ 'preferredUsername', 'type' ],
             model: ActorModel.unscoped(),
             required: true,
             as: 'ActorFollowing',
             include: [
               buildChannelInclude(false),
-              buildAccountInclude(false)
+              buildAccountInclude(false),
+              {
+                attributes: [ 'host' ],
+                model: ServerModel.unscoped(),
+                required: false
+              }
             ]
           }
         ]
@@ -174,9 +229,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
       }
     },
     {
-      fields: [ 'videoAbuseId' ],
+      fields: [ 'abuseId' ],
       where: {
-        videoAbuseId: {
+        abuseId: {
           [Op.ne]: null
         }
       }
@@ -212,10 +267,26 @@ function buildAccountInclude (required: boolean, withActor = false) {
           [Op.ne]: null
         }
       }
+    },
+    {
+      fields: [ 'pluginId' ],
+      where: {
+        pluginId: {
+          [Op.ne]: null
+        }
+      }
+    },
+    {
+      fields: [ 'applicationId' ],
+      where: {
+        applicationId: {
+          [Op.ne]: null
+        }
+      }
     }
   ] as (ModelIndexesOptions & { where?: WhereOptions })[]
 })
-export class UserNotificationModel extends Model<UserNotificationModel> {
+export class UserNotificationModel extends Model {
 
   @AllowNull(false)
   @Default(null)
@@ -271,17 +342,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
   })
   Comment: VideoCommentModel
 
-  @ForeignKey(() => VideoAbuseModel)
+  @ForeignKey(() => AbuseModel)
   @Column
-  videoAbuseId: number
+  abuseId: number
 
-  @BelongsTo(() => VideoAbuseModel, {
+  @BelongsTo(() => AbuseModel, {
     foreignKey: {
       allowNull: true
     },
     onDelete: 'cascade'
   })
-  VideoAbuse: VideoAbuseModel
+  Abuse: AbuseModel
 
   @ForeignKey(() => VideoBlacklistModel)
   @Column
@@ -331,26 +402,50 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
   })
   ActorFollow: ActorFollowModel
 
+  @ForeignKey(() => PluginModel)
+  @Column
+  pluginId: number
+
+  @BelongsTo(() => PluginModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+  Plugin: PluginModel
+
+  @ForeignKey(() => ApplicationModel)
+  @Column
+  applicationId: number
+
+  @BelongsTo(() => ApplicationModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+  Application: ApplicationModel
+
   static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
+    const where = { userId }
+
     const query: FindOptions = {
       offset: start,
       limit: count,
       order: getSort(sort),
-      where: {
-        userId
-      }
+      where
     }
 
     if (unread !== undefined) query.where['read'] = !unread
 
-    return UserNotificationModel.scope(ScopeNames.WITH_ALL)
-                                .findAndCountAll(query)
-                                .then(({ rows, count }) => {
-                                  return {
-                                    data: rows,
-                                    total: count
-                                  }
-                                })
+    return Promise.all([
+      UserNotificationModel.count({ where })
+        .then(count => count || 0),
+
+      count === 0
+        ? []
+        : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query)
+    ]).then(([ total, data ]) => ({ total, data }))
   }
 
   static markAsRead (userId: number, notificationIds: number[]) {
@@ -358,7 +453,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
       where: {
         userId,
         id: {
-          [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
+          [Op.in]: notificationIds
         }
       }
     }
@@ -372,54 +467,130 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     return UserNotificationModel.update({ read: true }, query)
   }
 
+  static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
+    const id = parseInt(options.id + '', 10)
+
+    function buildAccountWhereQuery (base: string) {
+      const whereSuffix = options.forUserId
+        ? ` AND "userNotification"."userId" = ${options.forUserId}`
+        : ''
+
+      if (options.type === 'account') {
+        return base +
+          ` WHERE "account"."id" = ${id} ${whereSuffix}`
+      }
+
+      return base +
+        ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
+    }
+
+    const queries = [
+      buildAccountWhereQuery(
+        `SELECT "userNotification"."id" FROM "userNotification" ` +
+        `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
+        `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
+      ),
+
+      // Remove notifications from muted accounts that followed ours
+      buildAccountWhereQuery(
+        `SELECT "userNotification"."id" FROM "userNotification" ` +
+        `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
+        `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
+        `INNER JOIN account ON account."actorId" = actor.id `
+      ),
+
+      // Remove notifications from muted accounts that commented something
+      buildAccountWhereQuery(
+        `SELECT "userNotification"."id" FROM "userNotification" ` +
+        `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
+        `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
+        `INNER JOIN account ON account."actorId" = actor.id `
+      ),
+
+      buildAccountWhereQuery(
+        `SELECT "userNotification"."id" FROM "userNotification" ` +
+        `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
+        `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
+        `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
+      )
+    ]
+
+    const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
+
+    return UserNotificationModel.sequelize.query(query)
+  }
+
   toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
     const video = this.Video
-      ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
+      ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
       : undefined
 
-    const videoImport = this.VideoImport ? {
-      id: this.VideoImport.id,
-      video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
-      torrentName: this.VideoImport.torrentName,
-      magnetUri: this.VideoImport.magnetUri,
-      targetUrl: this.VideoImport.targetUrl
-    } : undefined
-
-    const comment = this.Comment ? {
-      id: this.Comment.id,
-      threadId: this.Comment.getThreadId(),
-      account: this.formatActor(this.Comment.Account),
-      video: this.formatVideo(this.Comment.Video)
-    } : undefined
-
-    const videoAbuse = this.VideoAbuse ? {
-      id: this.VideoAbuse.id,
-      video: this.formatVideo(this.VideoAbuse.Video)
-    } : undefined
-
-    const videoBlacklist = this.VideoBlacklist ? {
-      id: this.VideoBlacklist.id,
-      video: this.formatVideo(this.VideoBlacklist.Video)
-    } : undefined
+    const videoImport = this.VideoImport
+      ? {
+        id: this.VideoImport.id,
+        video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
+        torrentName: this.VideoImport.torrentName,
+        magnetUri: this.VideoImport.magnetUri,
+        targetUrl: this.VideoImport.targetUrl
+      }
+      : undefined
+
+    const comment = this.Comment
+      ? {
+        id: this.Comment.id,
+        threadId: this.Comment.getThreadId(),
+        account: this.formatActor(this.Comment.Account),
+        video: this.formatVideo(this.Comment.Video)
+      }
+      : undefined
+
+    const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
+
+    const videoBlacklist = this.VideoBlacklist
+      ? {
+        id: this.VideoBlacklist.id,
+        video: this.formatVideo(this.VideoBlacklist.Video)
+      }
+      : undefined
 
     const account = this.Account ? this.formatActor(this.Account) : undefined
 
-    const actorFollow = this.ActorFollow ? {
-      id: this.ActorFollow.id,
-      state: this.ActorFollow.state,
-      follower: {
-        id: this.ActorFollow.ActorFollower.Account.id,
-        displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
-        name: this.ActorFollow.ActorFollower.preferredUsername,
-        avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
-        host: this.ActorFollow.ActorFollower.getHost()
-      },
-      following: {
-        type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
-        displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
-        name: this.ActorFollow.ActorFollowing.preferredUsername
+    const actorFollowingType = {
+      Application: 'instance' as 'instance',
+      Group: 'channel' as 'channel',
+      Person: 'account' as 'account'
+    }
+    const actorFollow = this.ActorFollow
+      ? {
+        id: this.ActorFollow.id,
+        state: this.ActorFollow.state,
+        follower: {
+          id: this.ActorFollow.ActorFollower.Account.id,
+          displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
+          name: this.ActorFollow.ActorFollower.preferredUsername,
+          avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
+          host: this.ActorFollow.ActorFollower.getHost()
+        },
+        following: {
+          type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
+          displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
+          name: this.ActorFollow.ActorFollowing.preferredUsername,
+          host: this.ActorFollow.ActorFollowing.getHost()
+        }
       }
-    } : undefined
+      : undefined
+
+    const plugin = this.Plugin
+      ? {
+        name: this.Plugin.name,
+        type: this.Plugin.type,
+        latestVersion: this.Plugin.latestVersion
+      }
+      : undefined
+
+    const peertube = this.Application
+      ? { latestVersion: this.Application.latestPeerTubeVersion }
+      : undefined
 
     return {
       id: this.id,
@@ -428,10 +599,12 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
       video,
       videoImport,
       comment,
-      videoAbuse,
+      abuse,
       videoBlacklist,
       account,
       actorFollow,
+      plugin,
+      peertube,
       createdAt: this.createdAt.toISOString(),
       updatedAt: this.updatedAt.toISOString()
     }
@@ -445,6 +618,34 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     }
   }
 
+  formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
+    const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
+      ? {
+        threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
+
+        video: abuse.VideoCommentAbuse.VideoComment.Video
+          ? {
+            id: abuse.VideoCommentAbuse.VideoComment.Video.id,
+            name: abuse.VideoCommentAbuse.VideoComment.Video.name,
+            uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
+          }
+          : undefined
+      }
+      : undefined
+
+    const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
+
+    const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
+
+    return {
+      id: abuse.id,
+      state: abuse.state,
+      video: videoAbuse,
+      comment: commentAbuse,
+      account: accountAbuse
+    }
+  }
+
   formatActor (
     this: UserNotificationModelForApi,
     accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor