1
0
Fork 0

Add import finished and video published notifs

This commit is contained in:
Chocobozzz 2019-01-02 16:37:43 +01:00 committed by Chocobozzz
parent 6e7e63b83f
commit dc13348070
23 changed files with 815 additions and 251 deletions

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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

View File

@ -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<Bull.Job<any>>[] = []
@ -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)
}
// ---------------------------------------------------------------------------

View File

@ -197,6 +197,7 @@ async function processFile (downloader: () => Promise<string>, 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<string>, videoImport: Vide
videoImport.state = VideoImportState.FAILED
await videoImport.save()
Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false)
throw err
}
}

View File

@ -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<UserNotificationModel>,

View File

@ -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 () {

View File

@ -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 })

View File

@ -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
}

View File

@ -72,6 +72,21 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
})
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: {

View File

@ -65,6 +65,24 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@Column
blacklistOnMyVideo: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@Is(
'UserNotificationSettingMyVideoPublished',
value => 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<UserNotificationSettingM
newCommentOnMyVideo: this.newCommentOnMyVideo,
newVideoFromSubscription: this.newVideoFromSubscription,
videoAbuseAsModerator: this.videoAbuseAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo
blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished,
myVideoImportFinished: this.myVideoImportFinished
}
}
}

View File

@ -1,4 +1,17 @@
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import {
AllowNull,
BelongsTo,
Column,
CreatedAt,
Default,
ForeignKey,
IFindOptions,
Is,
Model,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript'
import { UserNotification, UserNotificationType } from '../../../shared'
import { getSort, throwIfNotValid } from '../utils'
import { isBooleanValid } from '../../helpers/custom-validators/misc'
@ -11,66 +24,68 @@ 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'
enum ScopeNames {
WITH_ALL = 'WITH_ALL'
}
function buildVideoInclude (required: boolean) {
return {
attributes: [ 'id', 'uuid', 'name' ],
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<UserNotificationModel> {
})
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<UserNotificationModel> = {
offset: start,
limit: count,
order: getSort(sort),
@ -176,6 +203,8 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
}
}
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<UserNotificationModel> {
}
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<UserNotificationModel> {
type: this.type,
read: this.read,
video,
videoImport,
comment,
videoAbuse,
videoBlacklist,
@ -253,4 +277,12 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
updatedAt: this.updatedAt.toISOString()
}
}
private formatVideo (video: VideoModel) {
return {
id: video.id,
uuid: video.uuid,
name: video.name
}
}
}

View File

@ -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<UserModel> {
})
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<UserModel> {
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()

View File

@ -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'

View File

@ -144,6 +144,10 @@ export class VideoImportModel extends Model<VideoImportModel> {
})
}
getTargetIdentifier () {
return this.targetUrl || this.magnetUri || this.torrentName
}
toFormattedJSON (): VideoImport {
const videoFormatOptions = {
completeDescription: true,

View File

@ -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<VideoModel> {
})
VideoBlacklist: VideoBlacklistModel
@HasOne(() => VideoImportModel, {
foreignKey: {
name: 'videoId',
allowNull: true
},
onDelete: 'set null'
})
VideoImport: VideoImportModel
@HasMany(() => VideoCaptionModel, {
foreignKey: {
name: 'videoId',

View File

@ -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 () {

View File

@ -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
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[]
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')
})
})

Binary file not shown.

View File

@ -10,4 +10,6 @@ export interface UserNotificationSetting {
newCommentOnMyVideo: UserNotificationSettingValue
videoAbuseAsModerator: UserNotificationSettingValue
blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue
}

View File

@ -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?: {

View File

@ -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, lastNotificationChecker, socketFinder, emailFinder, type)
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) {
const text: string = email[ 'text' ]
return text.includes(videoUUID) && text.includes('Your video')
}
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,

View File

@ -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,