From dc13348070d808d0ba3feb56a435b835c2e7e791 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 2 Jan 2019 16:37:43 +0100 Subject: [PATCH] Add import finished and video published notifs --- .../controllers/api/users/my-notifications.ts | 12 +- .../migrations/0315-user-notifications.ts | 6 +- server/lib/emailer.ts | 61 +++ server/lib/job-queue/handlers/video-file.ts | 24 +- server/lib/job-queue/handlers/video-import.ts | 3 + server/lib/notifier.ts | 76 ++++ .../lib/schedulers/update-videos-scheduler.ts | 14 +- server/lib/user.ts | 2 + .../validators/user-notifications.ts | 18 +- server/models/account/account-blocklist.ts | 15 + .../account/user-notification-setting.ts | 22 +- server/models/account/user-notification.ts | 150 ++++--- server/models/account/user.ts | 24 ++ server/models/video/video-file.ts | 2 - server/models/video/video-import.ts | 4 + server/models/video/video.ts | 10 + .../api/check-params/user-notifications.ts | 16 +- server/tests/api/users/user-notifications.ts | 401 ++++++++++++------ server/tests/fixtures/video_short_240p.mp4 | Bin 0 -> 14082 bytes .../users/user-notification-setting.model.ts | 2 + .../models/users/user-notification.model.ts | 17 +- shared/utils/users/user-notifications.ts | 180 ++++++-- shared/utils/videos/video-imports.ts | 5 + 23 files changed, 814 insertions(+), 250 deletions(-) create mode 100644 server/tests/fixtures/video_short_240p.mp4 diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index cef1d237c..4b81777a4 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts @@ -14,10 +14,11 @@ import { getFormattedObjects } from '../../../helpers/utils' import { UserNotificationModel } from '../../../models/account/user-notification' import { meRouter } from './me' import { + listUserNotificationsValidator, markAsReadUserNotificationsValidator, updateNotificationSettingsValidator } from '../../../middlewares/validators/user-notifications' -import { UserNotificationSetting } from '../../../../shared/models/users' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users' import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' const myNotificationsRouter = express.Router() @@ -34,6 +35,7 @@ myNotificationsRouter.get('/me/notifications', userNotificationsSortValidator, setDefaultSort, setDefaultPagination, + listUserNotificationsValidator, asyncMiddleware(listUserNotifications) ) @@ -61,7 +63,11 @@ async function updateNotificationSettings (req: express.Request, res: express.Re await UserNotificationSettingModel.update({ newVideoFromSubscription: body.newVideoFromSubscription, - newCommentOnMyVideo: body.newCommentOnMyVideo + newCommentOnMyVideo: body.newCommentOnMyVideo, + videoAbuseAsModerator: body.videoAbuseAsModerator, + blacklistOnMyVideo: body.blacklistOnMyVideo, + myVideoPublished: body.myVideoPublished, + myVideoImportFinished: body.myVideoImportFinished }, query) return res.status(204).end() @@ -70,7 +76,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re async function listUserNotifications (req: express.Request, res: express.Response) { const user: UserModel = res.locals.oauth.token.User - const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort) + const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/initializers/migrations/0315-user-notifications.ts b/server/initializers/migrations/0315-user-notifications.ts index 2bd9c657d..8c54c5d6c 100644 --- a/server/initializers/migrations/0315-user-notifications.ts +++ b/server/initializers/migrations/0315-user-notifications.ts @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS "userNotificationSetting" ("id" SERIAL, "newCommentOnMyVideo" INTEGER NOT NULL DEFAULT NULL, "videoAbuseAsModerator" INTEGER NOT NULL DEFAULT NULL, "blacklistOnMyVideo" INTEGER NOT NULL DEFAULT NULL, +"myVideoPublished" INTEGER NOT NULL DEFAULT NULL, +"myVideoImportFinished" INTEGER NOT NULL DEFAULT NULL, "userId" INTEGER REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, @@ -24,8 +26,8 @@ PRIMARY KEY ("id")) { const query = 'INSERT INTO "userNotificationSetting" ' + '("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' + - '"userId", "createdAt", "updatedAt") ' + - '(SELECT 2, 2, 4, 4, id, NOW(), NOW() FROM "user")' + '"myVideoPublished", "myVideoImportFinished", "userId", "createdAt", "updatedAt") ' + + '(SELECT 2, 2, 4, 4, 2, 2, id, NOW(), NOW() FROM "user")' await utils.sequelize.query(query) } diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index d766e655b..6dc8f2adf 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -10,6 +10,7 @@ import { readFileSync } from 'fs-extra' import { VideoCommentModel } from '../models/video/video-comment' import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoBlacklistModel } from '../models/video/video-blacklist' +import { VideoImportModel } from '../models/video/video-import' class Emailer { @@ -102,6 +103,66 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } + myVideoPublishedNotification (to: string[], video: VideoModel) { + const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() + + const text = `Hi dear user,\n\n` + + `Your video ${video.name} has been published.` + + `\n\n` + + `You can view it on ${videoUrl} ` + + `\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: `Your video ${video.name} is published`, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + + myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) { + const videoUrl = CONFIG.WEBSERVER.URL + videoImport.Video.getWatchStaticPath() + + const text = `Hi dear user,\n\n` + + `Your video import ${videoImport.getTargetIdentifier()} is finished.` + + `\n\n` + + `You can view the imported video on ${videoUrl} ` + + `\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: `Your video import ${videoImport.getTargetIdentifier()} is finished`, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + + myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { + const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports' + + const text = `Hi dear user,\n\n` + + `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + + `\n\n` + + `See your videos import dashboard for more information: ${importUrl}` + + `\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { const accountName = comment.Account.getDisplayName() const video = comment.Video diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 480d324dc..593e43cc5 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts @@ -68,17 +68,17 @@ async function processVideoFile (job: Bull.Job) { async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { if (video === undefined) return undefined - const { videoDatabase, isNewVideo } = await sequelizeTypescript.transaction(async t => { + const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { // Maybe the video changed in database, refresh it let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) // Video does not exist anymore if (!videoDatabase) return undefined - let isNewVideo = false + let videoPublished = false // We transcoded the video file in another format, now we can publish it if (videoDatabase.state !== VideoState.PUBLISHED) { - isNewVideo = true + videoPublished = true videoDatabase.state = VideoState.PUBLISHED videoDatabase.publishedAt = new Date() @@ -86,12 +86,15 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { } // If the video was not published, we consider it is a new one for other instances - await federateVideoIfNeeded(videoDatabase, isNewVideo, t) + await federateVideoIfNeeded(videoDatabase, videoPublished, t) - return { videoDatabase, isNewVideo } + return { videoDatabase, videoPublished } }) - if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) + if (videoPublished) { + Notifier.Instance.notifyOnNewVideo(videoDatabase) + Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) + } } async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { @@ -100,7 +103,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo // Outside the transaction (IO on disk) const { videoFileResolution } = await videoArg.getOriginalFileResolution() - const videoDatabase = await sequelizeTypescript.transaction(async t => { + const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { // Maybe the video changed in database, refresh it let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) // Video does not exist anymore @@ -113,6 +116,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo { resolutions: resolutionsEnabled } ) + let videoPublished = false + if (resolutionsEnabled.length !== 0) { const tasks: Bluebird>[] = [] @@ -130,6 +135,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) } else { + videoPublished = true + // No transcoding to do, it's now published videoDatabase.state = VideoState.PUBLISHED videoDatabase = await videoDatabase.save({ transaction: t }) @@ -139,10 +146,11 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo await federateVideoIfNeeded(videoDatabase, isNewVideo, t) - return videoDatabase + return { videoDatabase, videoPublished } }) if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) + if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) } // --------------------------------------------------------------------------- diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 29cd1198c..12004dcd7 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -197,6 +197,7 @@ async function processFile (downloader: () => Promise, videoImport: Vide }) Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video) + Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) // Create transcoding jobs? if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { @@ -220,6 +221,8 @@ async function processFile (downloader: () => Promise, videoImport: Vide videoImport.state = VideoImportState.FAILED await videoImport.save() + Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false) + throw err } } diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index a21b50b2d..11b0937e9 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -11,6 +11,8 @@ import { VideoPrivacy, VideoState } from '../../shared/models/videos' import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoBlacklistModel } from '../models/video/video-blacklist' import * as Bluebird from 'bluebird' +import { VideoImportModel } from '../models/video/video-import' +import { AccountBlocklistModel } from '../models/account/account-blocklist' class Notifier { @@ -26,6 +28,14 @@ class Notifier { .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) } + notifyOnPendingVideoPublished (video: VideoModel): void { + // Only notify on public videos that has been published while the user waited transcoding/scheduled update + if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return + + this.notifyOwnedVideoHasBeenPublished(video) + .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err })) + } + notifyOnNewComment (comment: VideoCommentModel): void { this.notifyVideoOwnerOfNewComment(comment) .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err })) @@ -46,6 +56,11 @@ class Notifier { .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err })) } + notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void { + this.notifyOwnerVideoImportIsFinished(videoImport, success) + .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) + } + private async notifySubscribersOfNewVideo (video: VideoModel) { // List all followers that are users const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) @@ -80,6 +95,9 @@ class Notifier { // Not our user or user comments its own video if (!user || comment.Account.userId === user.id) return + const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, comment.accountId) + if (accountMuted) return + logger.info('Notifying user %s of new comment %s.', user.username, comment.url) function settingGetter (user: UserModel) { @@ -188,6 +206,64 @@ class Notifier { return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) } + private async notifyOwnedVideoHasBeenPublished (video: VideoModel) { + const user = await UserModel.loadByVideoId(video.id) + if (!user) return + + logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) + + function settingGetter (user: UserModel) { + return user.NotificationSetting.myVideoPublished + } + + async function notificationCreator (user: UserModel) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.MY_VIDEO_PUBLISHED, + userId: user.id, + videoId: video.id + }) + notification.Video = video + + return notification + } + + function emailSender (emails: string[]) { + return Emailer.Instance.myVideoPublishedNotification(emails, video) + } + + return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) + } + + private async notifyOwnerVideoImportIsFinished (videoImport: VideoImportModel, success: boolean) { + const user = await UserModel.loadByVideoImportId(videoImport.id) + if (!user) return + + logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) + + function settingGetter (user: UserModel) { + return user.NotificationSetting.myVideoImportFinished + } + + async function notificationCreator (user: UserModel) { + const notification = await UserNotificationModel.create({ + type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, + userId: user.id, + videoImportId: videoImport.id + }) + notification.VideoImport = videoImport + + return notification + } + + function emailSender (emails: string[]) { + return success + ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport) + : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport) + } + + return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) + } + private async notify (options: { users: UserModel[], notificationCreator: (user: UserModel) => Promise, diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index b7fb029f1..2618a5857 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts @@ -6,6 +6,7 @@ import { federateVideoIfNeeded } from '../activitypub' import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' import { VideoPrivacy } from '../../../shared/models/videos' import { Notifier } from '../notifier' +import { VideoModel } from '../../models/video/video' export class UpdateVideosScheduler extends AbstractScheduler { @@ -24,8 +25,9 @@ export class UpdateVideosScheduler extends AbstractScheduler { private async updateVideos () { if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined - return sequelizeTypescript.transaction(async t => { + const publishedVideos = await sequelizeTypescript.transaction(async t => { const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) + const publishedVideos: VideoModel[] = [] for (const schedule of schedules) { const video = schedule.Video @@ -42,13 +44,21 @@ export class UpdateVideosScheduler extends AbstractScheduler { await federateVideoIfNeeded(video, isNewVideo, t) if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { - Notifier.Instance.notifyOnNewVideo(video) + video.ScheduleVideoUpdate = schedule + publishedVideos.push(video) } } await schedule.destroy({ transaction: t }) } + + return publishedVideos }) + + for (const v of publishedVideos) { + Notifier.Instance.notifyOnNewVideo(v) + Notifier.Instance.notifyOnPendingVideoPublished(v) + } } static get Instance () { diff --git a/server/lib/user.ts b/server/lib/user.ts index 72127819c..481571828 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -100,6 +100,8 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr userId: user.id, newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION, newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, + myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION, + myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION, videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL }, { transaction: t }) diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts index 8202f307e..1c31f0a73 100644 --- a/server/middlewares/validators/user-notifications.ts +++ b/server/middlewares/validators/user-notifications.ts @@ -1,11 +1,26 @@ import * as express from 'express' import 'express-validator' -import { body } from 'express-validator/check' +import { body, query } from 'express-validator/check' import { logger } from '../../helpers/logger' import { areValidationErrors } from './utils' import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' import { isIntArray } from '../../helpers/custom-validators/misc' +const listUserNotificationsValidator = [ + query('unread') + .optional() + .toBoolean() + .isBoolean().withMessage('Should have a valid unread boolean'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listUserNotificationsValidator parameters', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + const updateNotificationSettingsValidator = [ body('newVideoFromSubscription') .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), @@ -41,6 +56,7 @@ const markAsReadUserNotificationsValidator = [ // --------------------------------------------------------------------------- export { + listUserNotificationsValidator, updateNotificationSettingsValidator, markAsReadUserNotificationsValidator } diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fa2819235..54ac290c4 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -72,6 +72,21 @@ export class AccountBlocklistModel extends Model { }) BlockedAccount: AccountModel + static isAccountMutedBy (accountId: number, targetAccountId: number) { + const query = { + attributes: [ 'id' ], + where: { + accountId, + targetAccountId + }, + raw: true + } + + return AccountBlocklistModel.unscoped() + .findOne(query) + .then(a => !!a) + } + static loadByAccountAndTarget (accountId: number, targetAccountId: number) { const query = { where: { diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index bc24b1e33..6470defa7 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts @@ -65,6 +65,24 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') + ) + @Column + myVideoPublished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoImportFinished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') + ) + @Column + myVideoImportFinished: UserNotificationSettingValue + @ForeignKey(() => UserModel) @Column userId: number @@ -94,7 +112,9 @@ export class UserNotificationSettingModel extends Model VideoModel.unscoped(), + required + } +} + +function buildChannelInclude () { + return { + required: true, + attributes: [ 'id', 'name' ], + model: () => VideoChannelModel.unscoped() + } +} + +function buildAccountInclude () { + return { + required: true, + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped() + } +} + @Scopes({ [ScopeNames.WITH_ALL]: { include: [ + Object.assign(buildVideoInclude(false), { + include: [ buildChannelInclude() ] + }), { - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped(), - required: false, - include: [ - { - required: true, - attributes: [ 'id', 'name' ], - model: () => VideoChannelModel.unscoped() - } - ] - }, - { - attributes: [ 'id' ], + attributes: [ 'id', 'originCommentId' ], model: () => VideoCommentModel.unscoped(), required: false, include: [ - { - required: true, - attributes: [ 'id', 'name' ], - model: () => AccountModel.unscoped() - }, - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } + buildAccountInclude(), + buildVideoInclude(true) ] }, { attributes: [ 'id' ], model: () => VideoAbuseModel.unscoped(), required: false, - include: [ - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } - ] + include: [ buildVideoInclude(true) ] }, { attributes: [ 'id' ], model: () => VideoBlacklistModel.unscoped(), required: false, - include: [ - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } - ] + include: [ buildVideoInclude(true) ] + }, + { + attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], + model: () => VideoImportModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] } ] } @@ -166,8 +181,20 @@ export class UserNotificationModel extends Model { }) VideoBlacklist: VideoBlacklistModel - static listForApi (userId: number, start: number, count: number, sort: string) { - const query = { + @ForeignKey(() => VideoImportModel) + @Column + videoImportId: number + + @BelongsTo(() => VideoImportModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoImport: VideoImportModel + + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { + const query: IFindOptions = { offset: start, limit: count, order: getSort(sort), @@ -176,6 +203,8 @@ export class UserNotificationModel extends Model { } } + if (unread !== undefined) query.where['read'] = !unread + return UserNotificationModel.scope(ScopeNames.WITH_ALL) .findAndCountAll(query) .then(({ rows, count }) => { @@ -200,45 +229,39 @@ export class UserNotificationModel extends Model { } toFormattedJSON (): UserNotification { - const video = this.Video ? { - id: this.Video.id, - uuid: this.Video.uuid, - name: this.Video.name, + const video = this.Video ? Object.assign(this.formatVideo(this.Video), { channel: { id: this.Video.VideoChannel.id, displayName: this.Video.VideoChannel.getDisplayName() } + }) : 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: { id: this.Comment.Account.id, displayName: this.Comment.Account.getDisplayName() }, - video: { - id: this.Comment.Video.id, - uuid: this.Comment.Video.uuid, - name: this.Comment.Video.name - } + video: this.formatVideo(this.Comment.Video) } : undefined const videoAbuse = this.VideoAbuse ? { id: this.VideoAbuse.id, - video: { - id: this.VideoAbuse.Video.id, - uuid: this.VideoAbuse.Video.uuid, - name: this.VideoAbuse.Video.name - } + video: this.formatVideo(this.VideoAbuse.Video) } : undefined const videoBlacklist = this.VideoBlacklist ? { id: this.VideoBlacklist.id, - video: { - id: this.VideoBlacklist.Video.id, - uuid: this.VideoBlacklist.Video.uuid, - name: this.VideoBlacklist.Video.name - } + video: this.formatVideo(this.VideoBlacklist.Video) } : undefined return { @@ -246,6 +269,7 @@ export class UserNotificationModel extends Model { type: this.type, read: this.read, video, + videoImport, comment, videoAbuse, videoBlacklist, @@ -253,4 +277,12 @@ export class UserNotificationModel extends Model { updatedAt: this.updatedAt.toISOString() } } + + private formatVideo (video: VideoModel) { + return { + id: video.id, + uuid: video.uuid, + name: video.name + } + } } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 55ec14d05..33f56f641 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -48,6 +48,7 @@ import { UserNotificationSettingModel } from './user-notification-setting' import { VideoModel } from '../video/video' import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' +import { VideoImportModel } from '../video/video-import' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -186,6 +187,12 @@ export class UserModel extends Model { }) NotificationSetting: UserNotificationSettingModel + @HasMany(() => VideoImportModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + VideoImports: VideoImportModel[] + @HasMany(() => OAuthTokenModel, { foreignKey: 'userId', onDelete: 'cascade' @@ -400,6 +407,23 @@ export class UserModel extends Model { return UserModel.findOne(query) } + static loadByVideoImportId (videoImportId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoImportModel.unscoped(), + where: { + id: videoImportId + } + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 3fd2d5a99..0fd868cd6 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -1,4 +1,3 @@ -import { values } from 'lodash' import { AllowNull, BelongsTo, @@ -20,7 +19,6 @@ import { isVideoFileSizeValid, isVideoFPSResolutionValid } from '../../helpers/custom-validators/videos' -import { CONSTRAINTS_FIELDS } from '../../initializers' import { throwIfNotValid } from '../utils' import { VideoModel } from './video' import * as Sequelize from 'sequelize' diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8d442b3f8..c723e57c0 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts @@ -144,6 +144,10 @@ export class VideoImportModel extends Model { }) } + getTargetIdentifier () { + return this.targetUrl || this.magnetUri || this.torrentName + } + toFormattedJSON (): VideoImport { const videoFormatOptions = { completeDescription: true, diff --git a/server/models/video/video.ts b/server/models/video/video.ts index fc200e5d1..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -94,6 +94,7 @@ import { import * as validator from 'validator' import { UserVideoHistoryModel } from '../account/user-video-history' import { UserModel } from '../account/user' +import { VideoImportModel } from './video-import' // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -785,6 +786,15 @@ export class VideoModel extends Model { }) VideoBlacklist: VideoBlacklistModel + @HasOne(() => VideoImportModel, { + foreignKey: { + name: 'videoId', + allowNull: true + }, + onDelete: 'set null' + }) + VideoImport: VideoImportModel + @HasMany(() => VideoCaptionModel, { foreignKey: { name: 'videoId', diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts index 3ae36ddb3..4f21f7b95 100644 --- a/server/tests/api/check-params/user-notifications.ts +++ b/server/tests/api/check-params/user-notifications.ts @@ -52,6 +52,18 @@ describe('Test user notifications API validators', function () { await checkBadSortPagination(server.url, path, server.accessToken) }) + it('Should fail with an incorrect unread parameter', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + unread: 'toto' + }, + token: server.accessToken, + statusCodeExpected: 200 + }) + }) + it('Should fail with a non authenticated user', async function () { await makeGetRequest({ url: server.url, @@ -125,7 +137,9 @@ describe('Test user notifications API validators', function () { newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION, newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION, - blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION + blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, + myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION, + myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION } it('Should fail with missing fields', async function () { diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts index 09c0479fd..e4966dbf5 100644 --- a/server/tests/api/users/user-notifications.ts +++ b/server/tests/api/users/user-notifications.ts @@ -29,33 +29,46 @@ import { getLastNotification, getUserNotifications, markAsReadNotifications, - updateMyNotificationSettings + updateMyNotificationSettings, + checkVideoIsPublished, checkMyVideoImportIsFinished } from '../../../../shared/utils/users/user-notifications' -import { User, UserNotification, UserNotificationSettingValue } from '../../../../shared/models/users' +import { + User, + UserNotification, + UserNotificationSetting, + UserNotificationSettingValue, + UserNotificationType +} from '../../../../shared/models/users' import { MockSmtpServer } from '../../../../shared/utils/miscs/email' import { addUserSubscription } from '../../../../shared/utils/users/user-subscriptions' import { VideoPrivacy } from '../../../../shared/models/videos' -import { getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' +import { getYoutubeVideoUrl, importVideo, getBadVideoUrl } from '../../../../shared/utils/videos/video-imports' import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' +import * as uuidv4 from 'uuid/v4' +import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist' const expect = chai.expect -async function uploadVideoByRemoteAccount (servers: ServerInfo[], videoNameId: number, additionalParams: any = {}) { - const data = Object.assign({ name: 'remote video ' + videoNameId }, additionalParams) +async function uploadVideoByRemoteAccount (servers: ServerInfo[], additionalParams: any = {}) { + const name = 'remote video ' + uuidv4() + + const data = Object.assign({ name }, additionalParams) const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, data) await waitJobs(servers) - return res.body.video.uuid + return { uuid: res.body.video.uuid, name } } -async function uploadVideoByLocalAccount (servers: ServerInfo[], videoNameId: number, additionalParams: any = {}) { - const data = Object.assign({ name: 'local video ' + videoNameId }, additionalParams) +async function uploadVideoByLocalAccount (servers: ServerInfo[], additionalParams: any = {}) { + const name = 'local video ' + uuidv4() + + const data = Object.assign({ name }, additionalParams) const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, data) await waitJobs(servers) - return res.body.video.uuid + return { uuid: res.body.video.uuid, name } } describe('Test users notifications', function () { @@ -63,7 +76,18 @@ describe('Test users notifications', function () { let userAccessToken: string let userNotifications: UserNotification[] = [] let adminNotifications: UserNotification[] = [] + let adminNotificationsServer2: UserNotification[] = [] const emails: object[] = [] + let channelId: number + + const allNotificationSettings: UserNotificationSetting = { + myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL + } before(async function () { this.timeout(120000) @@ -94,12 +118,9 @@ describe('Test users notifications', function () { await createUser(servers[0].url, servers[0].accessToken, user.username, user.password, 10 * 1000 * 1000) userAccessToken = await userLogin(servers[0], user) - await updateMyNotificationSettings(servers[0].url, userAccessToken, { - newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL - }) + await updateMyNotificationSettings(servers[0].url, userAccessToken, allNotificationSettings) + await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, allNotificationSettings) + await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, allNotificationSettings) { const socket = getUserNotificationSocket(servers[ 0 ].url, userAccessToken) @@ -109,6 +130,15 @@ describe('Test users notifications', function () { const socket = getUserNotificationSocket(servers[ 0 ].url, servers[0].accessToken) socket.on('new-notification', n => adminNotifications.push(n)) } + { + const socket = getUserNotificationSocket(servers[ 1 ].url, servers[1].accessToken) + socket.on('new-notification', n => adminNotificationsServer2.push(n)) + } + + { + const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) + channelId = resChannel.body.videoChannels[0].id + } }) describe('New video from my subscription notification', function () { @@ -124,7 +154,7 @@ describe('Test users notifications', function () { }) it('Should not send notifications if the user does not follow the video publisher', async function () { - await uploadVideoByLocalAccount(servers, 1) + await uploadVideoByLocalAccount(servers) const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) expect(notification).to.be.undefined @@ -136,11 +166,8 @@ describe('Test users notifications', function () { it('Should send a new video notification if the user follows the local video publisher', async function () { await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001') - const videoNameId = 10 - const videoName = 'local video ' + videoNameId - - const uuid = await uploadVideoByLocalAccount(servers, videoNameId) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + const { name, uuid } = await uploadVideoByLocalAccount(servers) + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should send a new video notification from a remote account', async function () { @@ -148,21 +175,13 @@ describe('Test users notifications', function () { await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002') - const videoNameId = 20 - const videoName = 'remote video ' + videoNameId - - const uuid = await uploadVideoByRemoteAccount(servers, videoNameId) - await waitJobs(servers) - - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + const { name, uuid } = await uploadVideoByRemoteAccount(servers) + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should send a new video notification on a scheduled publication', async function () { this.timeout(20000) - const videoNameId = 30 - const videoName = 'local video ' + videoNameId - // In 2 seconds let updateAt = new Date(new Date().getTime() + 2000) @@ -173,18 +192,15 @@ describe('Test users notifications', function () { privacy: VideoPrivacy.PUBLIC } } - const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) + const { name, uuid } = await uploadVideoByLocalAccount(servers, data) await wait(6000) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should send a new video notification on a remote scheduled publication', async function () { this.timeout(20000) - const videoNameId = 40 - const videoName = 'remote video ' + videoNameId - // In 2 seconds let updateAt = new Date(new Date().getTime() + 2000) @@ -195,19 +211,16 @@ describe('Test users notifications', function () { privacy: VideoPrivacy.PUBLIC } } - const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) + const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) await waitJobs(servers) await wait(6000) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should not send a notification before the video is published', async function () { this.timeout(20000) - const videoNameId = 50 - const videoName = 'local video ' + videoNameId - let updateAt = new Date(new Date().getTime() + 100000) const data = { @@ -217,86 +230,70 @@ describe('Test users notifications', function () { privacy: VideoPrivacy.PUBLIC } } - const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) + const { name, uuid } = await uploadVideoByLocalAccount(servers, data) await wait(6000) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') }) it('Should send a new video notification when a video becomes public', async function () { this.timeout(10000) - const videoNameId = 60 - const videoName = 'local video ' + videoNameId - const data = { privacy: VideoPrivacy.PRIVATE } - const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) + const { name, uuid } = await uploadVideoByLocalAccount(servers, data) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) await wait(500) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should send a new video notification when a remote video becomes public', async function () { this.timeout(20000) - const videoNameId = 70 - const videoName = 'remote video ' + videoNameId - const data = { privacy: VideoPrivacy.PRIVATE } - const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) - await waitJobs(servers) + const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) await waitJobs(servers) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) it('Should not send a new video notification when a video becomes unlisted', async function () { this.timeout(20000) - const videoNameId = 80 - const videoName = 'local video ' + videoNameId - const data = { privacy: VideoPrivacy.PRIVATE } - const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) + const { name, uuid } = await uploadVideoByLocalAccount(servers, data) await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') }) it('Should not send a new video notification when a remote video becomes unlisted', async function () { this.timeout(20000) - const videoNameId = 90 - const videoName = 'remote video ' + videoNameId - const data = { privacy: VideoPrivacy.PRIVATE } - const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) - await waitJobs(servers) + const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) await waitJobs(servers) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence') }) it('Should send a new video notification after a video import', async function () { this.timeout(30000) - const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) - const channelId = resChannel.body.videoChannels[0].id - const videoName = 'local video 100' + const name = 'video import ' + uuidv4() const attributes = { - name: videoName, + name, channelId, privacy: VideoPrivacy.PUBLIC, targetUrl: getYoutubeVideoUrl() @@ -306,7 +303,7 @@ describe('Test users notifications', function () { await waitJobs(servers) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) }) @@ -348,6 +345,23 @@ describe('Test users notifications', function () { await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') }) + it('Should not send a new comment notification if the account is muted', async function () { + this.timeout(10000) + + await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') + + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) + const uuid = resVideo.body.video.uuid + + const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') + const commentId = resComment.body.comment.id + + await wait(500) + await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') + + await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') + }) + it('Should send a new comment notification after a local comment on my video', async function () { this.timeout(10000) @@ -425,23 +439,21 @@ describe('Test users notifications', function () { it('Should send a notification to moderators on local video abuse', async function () { this.timeout(10000) - const videoName = 'local video 110' - - const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) const uuid = resVideo.body.video.uuid await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') await waitJobs(servers) - await checkNewVideoAbuseForModerators(baseParams, uuid, videoName, 'presence') + await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') }) it('Should send a notification to moderators on remote video abuse', async function () { this.timeout(10000) - const videoName = 'remote video 120' - - const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) const uuid = resVideo.body.video.uuid await waitJobs(servers) @@ -449,7 +461,7 @@ describe('Test users notifications', function () { await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') await waitJobs(servers) - await checkNewVideoAbuseForModerators(baseParams, uuid, videoName, 'presence') + await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') }) }) @@ -468,23 +480,21 @@ describe('Test users notifications', function () { it('Should send a notification to video owner on blacklist', async function () { this.timeout(10000) - const videoName = 'local video 130' - - const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) const uuid = resVideo.body.video.uuid await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) await waitJobs(servers) - await checkNewBlacklistOnMyVideo(baseParams, uuid, videoName, 'blacklist') + await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'blacklist') }) it('Should send a notification to video owner on unblacklist', async function () { this.timeout(10000) - const videoName = 'local video 130' - - const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) const uuid = resVideo.body.video.uuid await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) @@ -494,38 +504,187 @@ describe('Test users notifications', function () { await waitJobs(servers) await wait(500) - await checkNewBlacklistOnMyVideo(baseParams, uuid, videoName, 'unblacklist') + await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'unblacklist') + }) + }) + + describe('My video is published', function () { + let baseParams: CheckerBaseParams + + before(() => { + baseParams = { + server: servers[1], + emails, + socketNotifications: adminNotificationsServer2, + token: servers[1].accessToken + } + }) + + it('Should not send a notification if transcoding is not enabled', async function () { + const { name, uuid } = await uploadVideoByLocalAccount(servers) + await waitJobs(servers) + + await checkVideoIsPublished(baseParams, name, uuid, 'absence') + }) + + it('Should not send a notification if the wait transcoding is false', async function () { + this.timeout(50000) + + await uploadVideoByRemoteAccount(servers, { waitTranscoding: false }) + await waitJobs(servers) + + const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) + if (notification) { + expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED) + } + }) + + it('Should send a notification even if the video is not transcoded in other resolutions', async function () { + this.timeout(50000) + + const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) + await waitJobs(servers) + + await checkVideoIsPublished(baseParams, name, uuid, 'presence') + }) + + it('Should send a notification with a transcoded video', async function () { + this.timeout(50000) + + const { name, uuid } = await uploadVideoByRemoteAccount(servers, { waitTranscoding: true }) + await waitJobs(servers) + + await checkVideoIsPublished(baseParams, name, uuid, 'presence') + }) + + it('Should send a notification when an imported video is transcoded', async function () { + this.timeout(50000) + + const name = 'video import ' + uuidv4() + + const attributes = { + name, + channelId, + privacy: VideoPrivacy.PUBLIC, + targetUrl: getYoutubeVideoUrl(), + waitTranscoding: true + } + const res = await importVideo(servers[1].url, servers[1].accessToken, attributes) + const uuid = res.body.video.uuid + + await waitJobs(servers) + await checkVideoIsPublished(baseParams, name, uuid, 'presence') + }) + + it('Should send a notification when the scheduled update has been proceeded', async function () { + this.timeout(70000) + + // In 2 seconds + let updateAt = new Date(new Date().getTime() + 2000) + + const data = { + privacy: VideoPrivacy.PRIVATE, + scheduleUpdate: { + updateAt: updateAt.toISOString(), + privacy: VideoPrivacy.PUBLIC + } + } + const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) + + await wait(6000) + await checkVideoIsPublished(baseParams, name, uuid, 'presence') + }) + }) + + describe('My video is imported', function () { + let baseParams: CheckerBaseParams + + before(() => { + baseParams = { + server: servers[0], + emails, + socketNotifications: adminNotifications, + token: servers[0].accessToken + } + }) + + it('Should send a notification when the video import failed', async function () { + this.timeout(70000) + + const name = 'video import ' + uuidv4() + + const attributes = { + name, + channelId, + privacy: VideoPrivacy.PRIVATE, + targetUrl: getBadVideoUrl() + } + const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) + const uuid = res.body.video.uuid + + await waitJobs(servers) + await checkMyVideoImportIsFinished(baseParams, name, uuid, getBadVideoUrl(), false, 'presence') + }) + + it('Should send a notification when the video import succeeded', async function () { + this.timeout(70000) + + const name = 'video import ' + uuidv4() + + const attributes = { + name, + channelId, + privacy: VideoPrivacy.PRIVATE, + targetUrl: getYoutubeVideoUrl() + } + const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) + const uuid = res.body.video.uuid + + await waitJobs(servers) + await checkMyVideoImportIsFinished(baseParams, name, uuid, getYoutubeVideoUrl(), true, 'presence') }) }) describe('Mark as read', function () { it('Should mark as read some notifications', async function () { - const res = await getUserNotifications(servers[0].url, userAccessToken, 2, 3) + const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) const ids = res.body.data.map(n => n.id) - await markAsReadNotifications(servers[0].url, userAccessToken, ids) + await markAsReadNotifications(servers[ 0 ].url, userAccessToken, ids) }) it('Should have the notifications marked as read', async function () { - const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10) + const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10) + + const notifications = res.body.data as UserNotification[] + expect(notifications[ 0 ].read).to.be.false + expect(notifications[ 1 ].read).to.be.false + expect(notifications[ 2 ].read).to.be.true + expect(notifications[ 3 ].read).to.be.true + expect(notifications[ 4 ].read).to.be.true + expect(notifications[ 5 ].read).to.be.false + }) + + it('Should only list read notifications', async function () { + const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, false) const notifications = res.body.data as UserNotification[] - expect(notifications[0].read).to.be.false - expect(notifications[1].read).to.be.false - expect(notifications[2].read).to.be.true - expect(notifications[3].read).to.be.true - expect(notifications[4].read).to.be.true - expect(notifications[5].read).to.be.false + for (const notification of notifications) { + expect(notification.read).to.be.true + } + }) + + it('Should only list unread notifications', async function () { + const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) + + const notifications = res.body.data as UserNotification[] + for (const notification of notifications) { + expect(notification.read).to.be.false + } }) }) describe('Notification settings', function () { - const baseUpdateNotificationParams = { - newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL - } let baseParams: CheckerBaseParams before(() => { @@ -538,7 +697,7 @@ describe('Test users notifications', function () { }) it('Should not have notifications', async function () { - await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { + await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { newVideoFromSubscription: UserNotificationSettingValue.NONE })) @@ -548,16 +707,14 @@ describe('Test users notifications', function () { expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE) } - const videoNameId = 42 - const videoName = 'local video ' + videoNameId - const uuid = await uploadVideoByLocalAccount(servers, videoNameId) + const { name, uuid } = await uploadVideoByLocalAccount(servers) const check = { web: true, mail: true } - await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') + await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') }) it('Should only have web notifications', async function () { - await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { + await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION })) @@ -567,23 +724,21 @@ describe('Test users notifications', function () { expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION) } - const videoNameId = 52 - const videoName = 'local video ' + videoNameId - const uuid = await uploadVideoByLocalAccount(servers, videoNameId) + const { name, uuid } = await uploadVideoByLocalAccount(servers) { const check = { mail: true, web: false } - await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') + await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') } { const check = { mail: false, web: true } - await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'presence') + await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') } }) it('Should only have mail notifications', async function () { - await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { + await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { newVideoFromSubscription: UserNotificationSettingValue.EMAIL })) @@ -593,23 +748,21 @@ describe('Test users notifications', function () { expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL) } - const videoNameId = 62 - const videoName = 'local video ' + videoNameId - const uuid = await uploadVideoByLocalAccount(servers, videoNameId) + const { name, uuid } = await uploadVideoByLocalAccount(servers) { const check = { mail: false, web: true } - await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') + await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence') } { const check = { mail: true, web: false } - await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'presence') + await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence') } }) it('Should have email and web notifications', async function () { - await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { + await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL })) @@ -619,11 +772,9 @@ describe('Test users notifications', function () { expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL) } - const videoNameId = 72 - const videoName = 'local video ' + videoNameId - const uuid = await uploadVideoByLocalAccount(servers, videoNameId) + const { name, uuid } = await uploadVideoByLocalAccount(servers) - await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') + await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence') }) }) diff --git a/server/tests/fixtures/video_short_240p.mp4 b/server/tests/fixtures/video_short_240p.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..db074940bac2905f7daa392bcb26675c883b9e37 GIT binary patch literal 14082 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

G801TLxddjqI{&@34bQaN7g_S zz97mMLitD!Ow=!m@*z|veF>B=jq>49pR6B3Z?e1s%2z`9$|zqI<5MGn@%t!eP zQN9|=S4a5}x|7$Vh4Qsgz8=cgNBPK~IVbu7hN;PVAe1M46O_LU<(s2?OO$Vg@|UB0 zTa>>N<-@Qu$#q2ePADISjmhhAL-`&ke-+CA4dug-G+F;zl)nz;6HxvJluttWWR&lV z^8HZ0Kgth4`AFzb^fMFX2cdj6$`3~Q$anFH=Q#}Jhok&RNPmwFT-cQ5QY+*k&o2@$ zf9vl)`1=|7w>$$=m#1QLU?~dYlNiLNSO%yJ^Z@h~mMMxL7-PlVK@i{Khd}2*wV)5M z{DkRO0>*d=7#k&E?2yUVJwo21!aPOA^8Y23QJI6NC86% z#zLu$AYe!pff_-dVMqXmG%%!HK(GRk-U9-L^c_$iEI)xE0}L5E5R7Fqz>ook%ym#F z2pF=!kTnOvSSkw)SzySPf?&*%1%@0jFcg8Im<&1(0)`?ml*B-qAYdo~ zLn#h)5(Eq-U?{^vM0q|47|JA2G$;!M3}s*_f5Tw#${=9ifq@SNrGN@Rb)b*%zsDqC zY?b5qvB+3Ru$grDx-dRwkNu>k!13{8fisbV6oQ&D$lfQI%q7C!JCHKAha^S?{p%tu sHrYfL3#oB(Q>TspggG~t%VdJ92>*8tsRXGId7H@>-e%I#Lf&TjKO4Eaj{pDw literal 0 HcmV?d00001 diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts index 7cecd70a2..55d351abf 100644 --- a/shared/models/users/user-notification-setting.model.ts +++ b/shared/models/users/user-notification-setting.model.ts @@ -10,4 +10,6 @@ export interface UserNotificationSetting { newCommentOnMyVideo: UserNotificationSettingValue videoAbuseAsModerator: UserNotificationSettingValue blacklistOnMyVideo: UserNotificationSettingValue + myVideoPublished: UserNotificationSettingValue + myVideoImportFinished: UserNotificationSettingValue } diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index 39beb2350..ee9ac275a 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts @@ -3,10 +3,13 @@ export enum UserNotificationType { NEW_COMMENT_ON_MY_VIDEO = 2, NEW_VIDEO_ABUSE_FOR_MODERATORS = 3, BLACKLIST_ON_MY_VIDEO = 4, - UNBLACKLIST_ON_MY_VIDEO = 5 + UNBLACKLIST_ON_MY_VIDEO = 5, + MY_VIDEO_PUBLISHED = 6, + MY_VIDEO_IMPORT_SUCCESS = 7, + MY_VIDEO_IMPORT_ERROR = 8 } -interface VideoInfo { +export interface VideoInfo { id: number uuid: string name: string @@ -24,12 +27,22 @@ export interface UserNotification { } } + videoImport?: { + id: number + video?: VideoInfo + torrentName?: string + magnetUri?: string + targetUrl?: string + } + comment?: { id: number + threadId: number account: { id: number displayName: string } + video: VideoInfo } videoAbuse?: { diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts index dbe87559e..75d52023a 100644 --- a/shared/utils/users/user-notifications.ts +++ b/shared/utils/users/user-notifications.ts @@ -4,6 +4,7 @@ import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requ import { UserNotification, UserNotificationSetting, UserNotificationType } from '../../models/users' import { ServerInfo } from '..' import { expect } from 'chai' +import { inspect } from 'util' function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) { const path = '/api/v1/users/me/notification-settings' @@ -17,7 +18,15 @@ function updateMyNotificationSettings (url: string, token: string, settings: Use }) } -function getUserNotifications (url: string, token: string, start: number, count: number, sort = '-createdAt', statusCodeExpected = 200) { +function getUserNotifications ( + url: string, + token: string, + start: number, + count: number, + unread?: boolean, + sort = '-createdAt', + statusCodeExpected = 200 +) { const path = '/api/v1/users/me/notifications' return makeGetRequest({ @@ -27,7 +36,8 @@ function getUserNotifications (url: string, token: string, start: number, count: query: { start, count, - sort + sort, + unread }, statusCodeExpected }) @@ -46,7 +56,7 @@ function markAsReadNotifications (url: string, token: string, ids: number[], sta } async function getLastNotification (serverUrl: string, accessToken: string) { - const res = await getUserNotifications(serverUrl, accessToken, 0, 1, '-createdAt') + const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt') if (res.body.total === 0) return undefined @@ -65,21 +75,33 @@ type CheckerType = 'presence' | 'absence' async function checkNotification ( base: CheckerBaseParams, - lastNotificationChecker: (notification: UserNotification) => void, - socketNotificationFinder: (notification: UserNotification) => boolean, + notificationChecker: (notification: UserNotification, type: CheckerType) => void, emailNotificationFinder: (email: object) => boolean, - checkType: 'presence' | 'absence' + checkType: CheckerType ) { const check = base.check || { web: true, mail: true } if (check.web) { const notification = await getLastNotification(base.server.url, base.token) - lastNotificationChecker(notification) - const socketNotification = base.socketNotifications.find(n => socketNotificationFinder(n)) + if (notification || checkType !== 'absence') { + notificationChecker(notification, checkType) + } - if (checkType === 'presence') expect(socketNotification, 'The socket notification is absent.').to.not.be.undefined - else expect(socketNotification, 'The socket notification is present.').to.be.undefined + const socketNotification = base.socketNotifications.find(n => { + try { + notificationChecker(n, 'presence') + return true + } catch { + return false + } + }) + + if (checkType === 'presence') { + expect(socketNotification, 'The socket notification is absent. ' + inspect(base.socketNotifications)).to.not.be.undefined + } else { + expect(socketNotification, 'The socket notification is present. ' + inspect(socketNotification)).to.be.undefined + } } if (check.mail) { @@ -89,45 +111,127 @@ async function checkNotification ( .reverse() .find(e => emailNotificationFinder(e)) - if (checkType === 'presence') expect(email, 'The email is present.').to.not.be.undefined - else expect(email, 'The email is absent.').to.be.undefined + if (checkType === 'presence') { + expect(email, 'The email is absent. ' + inspect(base.emails)).to.not.be.undefined + } else { + expect(email, 'The email is present. ' + inspect(email)).to.be.undefined + } } } +function checkVideo (video: any, videoName?: string, videoUUID?: string) { + expect(video.name).to.be.a('string') + expect(video.name).to.not.be.empty + if (videoName) expect(video.name).to.equal(videoName) + + expect(video.uuid).to.be.a('string') + expect(video.uuid).to.not.be.empty + if (videoUUID) expect(video.uuid).to.equal(videoUUID) + + expect(video.id).to.be.a('number') +} + +function checkActor (channel: any) { + expect(channel.id).to.be.a('number') + expect(channel.displayName).to.be.a('string') + expect(channel.displayName).to.not.be.empty +} + +function checkComment (comment: any, commentId: number, threadId: number) { + expect(comment.id).to.equal(commentId) + expect(comment.threadId).to.equal(threadId) +} + async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) { const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION - function lastNotificationChecker (notification: UserNotification) { + function notificationChecker (notification: UserNotification, type: CheckerType) { if (type === 'presence') { expect(notification).to.not.be.undefined expect(notification.type).to.equal(notificationType) - expect(notification.video.name).to.equal(videoName) + + checkVideo(notification.video, videoName, videoUUID) + checkActor(notification.video.channel) } else { expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) } } - function socketFinder (notification: UserNotification) { - return notification.type === notificationType && notification.video.name === videoName + function emailFinder (email: object) { + return email[ 'text' ].indexOf(videoUUID) !== -1 + } + + await checkNotification(base, notificationChecker, emailFinder, type) +} + +async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) { + const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED + + function notificationChecker (notification: UserNotification, type: CheckerType) { + if (type === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkVideo(notification.video, videoName, videoUUID) + checkActor(notification.video.channel) + } else { + expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) + } } function emailFinder (email: object) { - return email[ 'text' ].indexOf(videoUUID) !== -1 + const text: string = email[ 'text' ] + return text.includes(videoUUID) && text.includes('Your video') } - await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) + await checkNotification(base, notificationChecker, emailFinder, type) +} + +async function checkMyVideoImportIsFinished ( + base: CheckerBaseParams, + videoName: string, + videoUUID: string, + url: string, + success: boolean, + type: CheckerType +) { + const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR + + function notificationChecker (notification: UserNotification, type: CheckerType) { + if (type === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.videoImport.targetUrl).to.equal(url) + + if (success) checkVideo(notification.videoImport.video, videoName, videoUUID) + } else { + expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url) + } + } + + function emailFinder (email: object) { + const text: string = email[ 'text' ] + const toFind = success ? ' finished' : ' error' + + return text.includes(url) && text.includes(toFind) + } + + await checkNotification(base, notificationChecker, emailFinder, type) } let lastEmailCount = 0 async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) { const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO - function lastNotificationChecker (notification: UserNotification) { + function notificationChecker (notification: UserNotification, type: CheckerType) { if (type === 'presence') { expect(notification).to.not.be.undefined expect(notification.type).to.equal(notificationType) - expect(notification.comment.id).to.equal(commentId) - expect(notification.comment.account.displayName).to.equal('root') + + checkComment(notification.comment, commentId, threadId) + checkActor(notification.comment.account) + checkVideo(notification.comment.video, undefined, uuid) } else { expect(notification).to.satisfy((n: UserNotification) => { return n === undefined || n.comment === undefined || n.comment.id !== commentId @@ -135,18 +239,12 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, } } - function socketFinder (notification: UserNotification) { - return notification.type === notificationType && - notification.comment.id === commentId && - notification.comment.account.displayName === 'root' - } - const commentUrl = `http://localhost:9001/videos/watch/${uuid};threadId=${threadId}` function emailFinder (email: object) { return email[ 'text' ].indexOf(commentUrl) !== -1 } - await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) + await checkNotification(base, notificationChecker, emailFinder, type) if (type === 'presence') { // We cannot detect email duplicates, so check we received another email @@ -158,12 +256,13 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS - function lastNotificationChecker (notification: UserNotification) { + function notificationChecker (notification: UserNotification, type: CheckerType) { if (type === 'presence') { expect(notification).to.not.be.undefined expect(notification.type).to.equal(notificationType) - expect(notification.videoAbuse.video.uuid).to.equal(videoUUID) - expect(notification.videoAbuse.video.name).to.equal(videoName) + + expect(notification.videoAbuse.id).to.be.a('number') + checkVideo(notification.videoAbuse.video, videoName, videoUUID) } else { expect(notification).to.satisfy((n: UserNotification) => { return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID @@ -171,16 +270,12 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU } } - function socketFinder (notification: UserNotification) { - return notification.type === notificationType && notification.videoAbuse.video.uuid === videoUUID - } - function emailFinder (email: object) { const text = email[ 'text' ] return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 } - await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) + await checkNotification(base, notificationChecker, emailFinder, type) } async function checkNewBlacklistOnMyVideo ( @@ -193,18 +288,13 @@ async function checkNewBlacklistOnMyVideo ( ? UserNotificationType.BLACKLIST_ON_MY_VIDEO : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO - function lastNotificationChecker (notification: UserNotification) { + function notificationChecker (notification: UserNotification) { expect(notification).to.not.be.undefined expect(notification.type).to.equal(notificationType) const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video - expect(video.uuid).to.equal(videoUUID) - expect(video.name).to.equal(videoName) - } - - function socketFinder (notification: UserNotification) { - return notification.type === notificationType && (notification.video || notification.videoBlacklist.video).uuid === videoUUID + checkVideo(video, videoName, videoUUID) } function emailFinder (email: object) { @@ -212,7 +302,7 @@ async function checkNewBlacklistOnMyVideo ( return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1 } - await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, 'presence') + await checkNotification(base, notificationChecker, emailFinder, 'presence') } // --------------------------------------------------------------------------- @@ -221,6 +311,8 @@ export { CheckerBaseParams, CheckerType, checkNotification, + checkMyVideoImportIsFinished, + checkVideoIsPublished, checkNewVideoFromSubscription, checkNewCommentOnMyVideo, checkNewBlacklistOnMyVideo, diff --git a/shared/utils/videos/video-imports.ts b/shared/utils/videos/video-imports.ts index 3fa49b432..ec77cdcda 100644 --- a/shared/utils/videos/video-imports.ts +++ b/shared/utils/videos/video-imports.ts @@ -11,6 +11,10 @@ function getMagnetURI () { return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4' } +function getBadVideoUrl () { + return 'https://download.cpy.re/peertube/bad_video.mp4' +} + function importVideo (url: string, token: string, attributes: VideoImportCreate) { const path = '/api/v1/videos/imports' @@ -45,6 +49,7 @@ function getMyVideoImports (url: string, token: string, sort?: string) { // --------------------------------------------------------------------------- export { + getBadVideoUrl, getYoutubeVideoUrl, importVideo, getMagnetURI, -- 2.25.1