+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, Op } 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 { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
+import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server'
+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 { UserModel } from './user'
enum ScopeNames {
WITH_ALL = 'WITH_ALL'
function buildActorWithAvatarInclude () {
return {
attributes: [ 'preferredUsername' ],
- model: () => ActorModel.unscoped(),
+ model: ActorModel.unscoped(),
required: true,
include: [
{
attributes: [ 'filename' ],
- model: () => AvatarModel.unscoped(),
+ model: AvatarModel.unscoped(),
required: false
},
{
attributes: [ 'host' ],
- model: () => ServerModel.unscoped(),
+ model: ServerModel.unscoped(),
required: false
}
]
function buildVideoInclude (required: boolean) {
return {
attributes: [ 'id', 'uuid', 'name' ],
- model: () => VideoModel.unscoped(),
+ model: VideoModel.unscoped(),
required
}
}
return {
required,
attributes: [ 'id', 'name' ],
- model: () => VideoChannelModel.unscoped(),
+ model: VideoChannelModel.unscoped(),
include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
}
}
return {
required,
attributes: [ 'id', 'name' ],
- model: () => AccountModel.unscoped(),
+ model: AccountModel.unscoped(),
include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
}
}
-@Scopes({
+@Scopes(() => ({
[ScopeNames.WITH_ALL]: {
include: [
Object.assign(buildVideoInclude(false), {
{
attributes: [ 'id', 'originCommentId' ],
- model: () => VideoCommentModel.unscoped(),
+ model: VideoCommentModel.unscoped(),
required: false,
include: [
buildAccountInclude(true, true),
},
{
- 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() ]
+ }
+ ]
},
{
attributes: [ 'id' ],
- model: () => VideoBlacklistModel.unscoped(),
+ model: VideoBlacklistModel.unscoped(),
required: false,
include: [ buildVideoInclude(true) ]
},
{
attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
- model: () => VideoImportModel.unscoped(),
+ model: VideoImportModel.unscoped(),
required: 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(),
+ model: ActorFollowModel.unscoped(),
required: false,
include: [
{
attributes: [ 'preferredUsername' ],
- model: () => ActorModel.unscoped(),
+ model: ActorModel.unscoped(),
required: true,
as: 'ActorFollower',
include: [
{
attributes: [ 'id', 'name' ],
- model: () => AccountModel.unscoped(),
+ model: AccountModel.unscoped(),
required: true
},
{
attributes: [ 'filename' ],
- model: () => AvatarModel.unscoped(),
+ model: AvatarModel.unscoped(),
required: false
},
{
attributes: [ 'host' ],
- model: () => ServerModel.unscoped(),
+ model: ServerModel.unscoped(),
required: false
}
]
},
{
- attributes: [ 'preferredUsername' ],
- model: () => ActorModel.unscoped(),
+ attributes: [ 'preferredUsername', 'type' ],
+ model: ActorModel.unscoped(),
required: true,
as: 'ActorFollowing',
include: [
buildChannelInclude(false),
- buildAccountInclude(false)
+ buildAccountInclude(false),
+ {
+ attributes: [ 'host' ],
+ model: ServerModel.unscoped(),
+ required: false
+ }
]
}
]
},
buildAccountInclude(false, true)
- ] as any // FIXME: sequelize typings
+ ]
}
-})
+}))
@Table({
tableName: 'userNotification',
indexes: [
}
},
{
- fields: [ 'videoAbuseId' ],
+ fields: [ 'abuseId' ],
where: {
- videoAbuseId: {
+ abuseId: {
[Op.ne]: null
}
}
[Op.ne]: null
}
}
+ },
+ {
+ fields: [ 'pluginId' ],
+ where: {
+ pluginId: {
+ [Op.ne]: null
+ }
+ }
+ },
+ {
+ fields: [ 'applicationId' ],
+ where: {
+ applicationId: {
+ [Op.ne]: null
+ }
+ }
}
- ] as any // FIXME: sequelize typings
+ ] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
-export class UserNotificationModel extends Model<UserNotificationModel> {
+export class UserNotificationModel extends Model {
@AllowNull(false)
@Default(null)
})
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
})
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[]) {
where: {
userId,
id: {
- [Op.any]: notificationIds
+ [Op.in]: notificationIds
}
}
}
return UserNotificationModel.update({ read: true }, query)
}
- toFormattedJSON (): UserNotification {
+ 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.getWebserverPath() } : 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,
video,
videoImport,
comment,
- videoAbuse,
+ abuse,
videoBlacklist,
account,
actorFollow,
+ plugin,
+ peertube,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}
}
- private formatVideo (video: VideoModel) {
+ formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
return {
id: video.id,
uuid: video.uuid,
}
}
- private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
+ 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
+ ) {
const avatar = accountOrChannel.Actor.Avatar
- ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
+ ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
: undefined
return {