|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
- import { UserNotification, type UserNotificationType_Type } from '@peertube/peertube-models'
- import { uuidToShort } from '@peertube/peertube-node-utils'
- import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user/index.js'
- import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
- import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Table, UpdatedAt } from 'sequelize-typescript'
- import { isBooleanValid } from '../../helpers/custom-validators/misc.js'
- import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications.js'
- import { AbuseModel } from '../abuse/abuse.js'
- import { AccountModel } from '../account/account.js'
- import { ActorFollowModel } from '../actor/actor-follow.js'
- import { ApplicationModel } from '../application/application.js'
- import { PluginModel } from '../server/plugin.js'
- import { SequelizeModel, throwIfNotValid } from '../shared/index.js'
- import { VideoBlacklistModel } from '../video/video-blacklist.js'
- import { VideoCaptionModel } from '../video/video-caption.js'
- import { VideoCommentModel } from '../video/video-comment.js'
- import { VideoImportModel } from '../video/video-import.js'
- import { VideoModel } from '../video/video.js'
- import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder.js'
- import { UserRegistrationModel } from './user-registration.js'
- import { UserModel } from './user.js'
-
- @Table({
- tableName: 'userNotification',
- indexes: [
- {
- fields: [ 'userId' ]
- },
- {
- fields: [ 'videoId' ],
- where: {
- videoId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'commentId' ],
- where: {
- commentId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'abuseId' ],
- where: {
- abuseId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'videoBlacklistId' ],
- where: {
- videoBlacklistId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'videoImportId' ],
- where: {
- videoImportId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'accountId' ],
- where: {
- accountId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'actorFollowId' ],
- where: {
- actorFollowId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'pluginId' ],
- where: {
- pluginId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'applicationId' ],
- where: {
- applicationId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'userRegistrationId' ],
- where: {
- userRegistrationId: {
- [Op.ne]: null
- }
- }
- }
- ] as (ModelIndexesOptions & { where?: WhereOptions })[]
- })
- export class UserNotificationModel extends SequelizeModel<UserNotificationModel> {
-
- @AllowNull(false)
- @Default(null)
- @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
- @Column
- type: UserNotificationType_Type
-
- @AllowNull(false)
- @Default(false)
- @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
- @Column
- read: boolean
-
- @CreatedAt
- createdAt: Date
-
- @UpdatedAt
- updatedAt: Date
-
- @ForeignKey(() => UserModel)
- @Column
- userId: number
-
- @BelongsTo(() => UserModel, {
- foreignKey: {
- allowNull: false
- },
- onDelete: 'cascade'
- })
- User: Awaited<UserModel>
-
- @ForeignKey(() => VideoModel)
- @Column
- videoId: number
-
- @BelongsTo(() => VideoModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- Video: Awaited<VideoModel>
-
- @ForeignKey(() => VideoCommentModel)
- @Column
- commentId: number
-
- @BelongsTo(() => VideoCommentModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- VideoComment: Awaited<VideoCommentModel>
-
- @ForeignKey(() => AbuseModel)
- @Column
- abuseId: number
-
- @BelongsTo(() => AbuseModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- Abuse: Awaited<AbuseModel>
-
- @ForeignKey(() => VideoBlacklistModel)
- @Column
- videoBlacklistId: number
-
- @BelongsTo(() => VideoBlacklistModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- VideoBlacklist: Awaited<VideoBlacklistModel>
-
- @ForeignKey(() => VideoImportModel)
- @Column
- videoImportId: number
-
- @BelongsTo(() => VideoImportModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- VideoImport: Awaited<VideoImportModel>
-
- @ForeignKey(() => AccountModel)
- @Column
- accountId: number
-
- @BelongsTo(() => AccountModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- Account: Awaited<AccountModel>
-
- @ForeignKey(() => ActorFollowModel)
- @Column
- actorFollowId: number
-
- @BelongsTo(() => ActorFollowModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- ActorFollow: Awaited<ActorFollowModel>
-
- @ForeignKey(() => PluginModel)
- @Column
- pluginId: number
-
- @BelongsTo(() => PluginModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- Plugin: Awaited<PluginModel>
-
- @ForeignKey(() => ApplicationModel)
- @Column
- applicationId: number
-
- @BelongsTo(() => ApplicationModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- Application: Awaited<ApplicationModel>
-
- @ForeignKey(() => UserRegistrationModel)
- @Column
- userRegistrationId: number
-
- @BelongsTo(() => UserRegistrationModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- UserRegistration: Awaited<UserRegistrationModel>
-
- @ForeignKey(() => VideoCaptionModel)
- @Column
- videoCaptionId: number
-
- @BelongsTo(() => VideoCaptionModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'cascade'
- })
- VideoCaption: Awaited<VideoCaptionModel>
-
- static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
- const where = { userId }
-
- const query = {
- userId,
- unread,
- offset: start,
- limit: count,
- sort,
- where
- }
-
- if (unread !== undefined) query.where['read'] = !unread
-
- return Promise.all([
- UserNotificationModel.count({ where })
- .then(count => count || 0),
-
- count === 0
- ? [] as UserNotificationModelForApi[]
- : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
- ]).then(([ total, data ]) => ({ total, data }))
- }
-
- static markAsRead (userId: number, notificationIds: number[]) {
- const query = {
- where: {
- userId,
- id: {
- [Op.in]: notificationIds
- },
- read: false
- }
- }
-
- return UserNotificationModel.update({ read: true }, query)
- }
-
- static markAllAsRead (userId: number) {
- const query = { where: { userId, read: false } }
-
- return UserNotificationModel.update({ read: true }, query)
- }
-
- static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
- const id = forceNumber(options.id)
-
- 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
- ? {
- ...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.VideoComment
- ? {
- id: this.VideoComment.id,
- threadId: this.VideoComment.getThreadId(),
- account: this.formatActor(this.VideoComment.Account),
- video: this.formatVideo(this.VideoComment.Video),
- heldForReview: this.VideoComment.heldForReview
- }
- : 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 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,
- host: this.ActorFollow.ActorFollower.getHost(),
-
- ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
- },
- 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
-
- 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
-
- const registration = this.UserRegistration
- ? { id: this.UserRegistration.id, username: this.UserRegistration.username }
- : undefined
-
- const videoCaption = this.VideoCaption
- ? {
- id: this.VideoCaption.id,
- language: {
- id: this.VideoCaption.language,
- label: VideoCaptionModel.getLanguageLabel(this.VideoCaption.language)
- },
- video: this.formatVideo(this.VideoCaption.Video)
- }
- : undefined
-
- return {
- id: this.id,
- type: this.type,
- read: this.read,
- video,
- videoImport,
- comment,
- abuse,
- videoBlacklist,
- account,
- actorFollow,
- plugin,
- peertube,
- registration,
- videoCaption,
- createdAt: this.createdAt.toISOString(),
- updatedAt: this.updatedAt.toISOString()
- }
- }
-
- formatVideo (video: UserNotificationIncludes.VideoInclude) {
- return {
- id: video.id,
- uuid: video.uuid,
- shortUUID: uuidToShort(video.uuid),
- name: video.name
- }
- }
-
- formatAbuse (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,
- shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
- 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 (
- accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
- ) {
- return {
- id: accountOrChannel.id,
- displayName: accountOrChannel.getDisplayName(),
- name: accountOrChannel.Actor.preferredUsername,
- host: accountOrChannel.Actor.getHost(),
-
- ...this.formatAvatars(accountOrChannel.Actor.Avatars)
- }
- }
-
- formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) {
- if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
-
- return {
- avatar: this.formatAvatar(maxBy(avatars, 'width')),
-
- avatars: avatars.map(a => this.formatAvatar(a))
- }
- }
-
- formatAvatar (a: UserNotificationIncludes.ActorImageInclude) {
- return {
- path: a.getStaticPath(),
- width: a.width
- }
- }
-
- formatVideoCaption (a: UserNotificationIncludes.ActorImageInclude) {
- return {
- path: a.getStaticPath(),
- width: a.width
- }
- }
- }
|