diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index 481ff275f..82dd8d6e5 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts @@ -30,7 +30,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { private notifier: Notifier ) { this.labelNotifications = { - newVideoFromSubscription: $localize`New video from your subscriptions`, + newVideoFromSubscription: $localize`New video or live from your subscriptions`, newCommentOnMyVideo: $localize`New comment on your video`, abuseAsModerator: $localize`New abuse`, videoAutoBlacklistAsModerator: $localize`An automatically blocked video is awaiting review`, diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index 369bc7396..336111caa 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts @@ -150,6 +150,7 @@ export class UserNotification implements UserNotificationServer { switch (this.type) { case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION: + case UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION: this.videoUrl = this.buildVideoUrl(this.video) break diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index bff177b6f..86165bb77 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html @@ -6,9 +6,7 @@ - - - + @if (notification.video) { @@ -16,15 +14,13 @@
{{ notification.video.channel.displayName }} published a new video: {{ notification.video.name }}
-
- - + } @else {
The notification concerns a video now unavailable
-
+ }
@@ -224,6 +220,24 @@ + + @if (notification.video) { + + + + +
+ {{ notification.video.channel.displayName }} is live streaming in {{ notification.video.name }} +
+ } @else { + + +
+ The notification concerns a video now unavailable +
+ } +
+ diff --git a/packages/models/src/users/user-notification.model.ts b/packages/models/src/users/user-notification.model.ts index 991fe6728..74cc16adb 100644 --- a/packages/models/src/users/user-notification.model.ts +++ b/packages/models/src/users/user-notification.model.ts @@ -34,7 +34,9 @@ export const UserNotificationType = { MY_VIDEO_STUDIO_EDITION_FINISHED: 19, - NEW_USER_REGISTRATION_REQUEST: 20 + NEW_USER_REGISTRATION_REQUEST: 20, + + NEW_LIVE_FROM_SUBSCRIPTION: 21 } as const export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType] diff --git a/packages/server-commands/src/videos/live-command.ts b/packages/server-commands/src/videos/live-command.ts index 793b64f40..84fb90164 100644 --- a/packages/server-commands/src/videos/live-command.ts +++ b/packages/server-commands/src/videos/live-command.ts @@ -119,12 +119,13 @@ export class LiveCommand extends AbstractCommand { } async quickCreate (options: OverrideCommandOptions & { + name: string saveReplay: boolean permanentLive: boolean privacy?: VideoPrivacyType videoPasswords?: string[] }) { - const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC, videoPasswords } = options + const { name = 'live', saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC, videoPasswords } = options const replaySettings = privacy === VideoPrivacy.PASSWORD_PROTECTED ? { privacy: VideoPrivacy.PRIVATE } @@ -134,7 +135,7 @@ export class LiveCommand extends AbstractCommand { ...options, fields: { - name: 'live', + name, permanentLive, saveReplay, replaySettings, diff --git a/packages/tests/src/api/notifications/moderation-notifications.ts b/packages/tests/src/api/notifications/moderation-notifications.ts index 493764882..e5c1cbb89 100644 --- a/packages/tests/src/api/notifications/moderation-notifications.ts +++ b/packages/tests/src/api/notifications/moderation-notifications.ts @@ -18,7 +18,7 @@ import { checkNewInstanceFollower, checkAutoInstanceFollowing, checkVideoAutoBlacklistForModerators, - checkVideoIsPublished, + checkMyVideoIsPublished, checkNewVideoFromSubscription } from '@tests/shared/notifications.js' @@ -487,7 +487,7 @@ describe('Test moderation notifications', function () { it('Should not send video publish notification if auto-blacklisted', async function () { this.timeout(120000) - await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) + await checkMyVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) }) it('Should not send a local user subscription notification if auto-blacklisted', async function () { @@ -576,7 +576,7 @@ describe('Test moderation notifications', function () { const { shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes }) await wait(6000) - await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' }) + await checkMyVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' }) await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' }) await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' }) }) diff --git a/packages/tests/src/api/notifications/user-notifications.ts b/packages/tests/src/api/notifications/user-notifications.ts index 57fddf353..275110014 100644 --- a/packages/tests/src/api/notifications/user-notifications.ts +++ b/packages/tests/src/api/notifications/user-notifications.ts @@ -10,10 +10,12 @@ import { prepareNotificationsTest, CheckerBaseParams, checkNewVideoFromSubscription, - checkVideoIsPublished, + checkMyVideoIsPublished, checkVideoStudioEditionIsFinished, checkMyVideoImportIsFinished, - checkNewActorFollow + checkNewActorFollow, + checkNewLiveFromSubscription, + waitUntilNotification } from '@tests/shared/notifications.js' import { FIXTURE_URLS } from '@tests/shared/tests.js' import { uploadRandomVideoOnServers } from '@tests/shared/videos.js' @@ -209,6 +211,82 @@ describe('Test user notifications', function () { await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) }) + + }) + + describe('New live from my subscription notification', function () { + let baseParams: CheckerBaseParams + + async function createAndStreamLive (server: PeerTubeServer) { + const name = 'video live ' + buildUUID() + + const streamDate = new Date() + const { video } = await server.live.quickCreate({ name, permanentLive: true, saveReplay: false }) + await waitJobs(servers) + + const ffmpegCommand = await server.live.sendRTMPStreamInVideo({ videoId: video.uuid }) + + return { name, video, ffmpegCommand, streamDate } + } + + before(async () => { + baseParams = { + server: servers[0], + emails, + socketNotifications: userNotifications, + token: userAccessToken + } + + await servers[0].config.enableLive({ allowReplay: false }) + + await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[0].host }) + await waitJobs(servers) + }) + + it('Should not send a notification when a live is created', async function () { + this.timeout(100000) + + const name = 'video live ' + buildUUID() + + const { video } = await servers[0].live.quickCreate({ name, permanentLive: true, saveReplay: false }) + await waitJobs(servers) + await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'absence' }) + }) + + it('Should send a local notification when streaming in the live', async function () { + this.timeout(100000) + + const { name, video, ffmpegCommand, streamDate } = await createAndStreamLive(servers[0]) + + await waitUntilNotification({ + server: servers[0], + token: userAccessToken, + notificationType: UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION, + fromDate: streamDate + }) + + await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) + + await stopFfmpeg(ffmpegCommand) + await waitJobs(servers) + }) + + it('Should send a remote notification when streaming in the live ', async function () { + this.timeout(100000) + + const { name, video, ffmpegCommand, streamDate } = await createAndStreamLive(servers[1]) + + await waitUntilNotification({ + server: servers[0], + token: userAccessToken, + notificationType: UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION, + fromDate: streamDate + }) + await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) + + await stopFfmpeg(ffmpegCommand) + await waitJobs(servers) + }) }) describe('My video is published', function () { @@ -229,7 +307,7 @@ describe('Test user notifications', function () { const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) }) it('Should not send a notification if the wait transcoding is false', async function () { @@ -250,7 +328,7 @@ describe('Test user notifications', function () { const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) }) it('Should send a notification with a transcoded video', async function () { @@ -259,7 +337,7 @@ describe('Test user notifications', function () { const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true }) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) }) it('Should send a notification when an imported video is transcoded', async function () { @@ -277,7 +355,7 @@ describe('Test user notifications', function () { const { video } = await servers[1].imports.importVideo({ attributes }) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) }) it('Should send a notification when the scheduled update has been proceeded', async function () { @@ -296,7 +374,7 @@ describe('Test user notifications', function () { const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) await wait(6000) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) }) it('Should not send a notification before the video is published', async function () { @@ -314,7 +392,7 @@ describe('Test user notifications', function () { const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) await wait(6000) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) }) }) @@ -354,7 +432,7 @@ describe('Test user notifications', function () { await servers[1].live.waitUntilReplacedByReplay({ videoId: shortUUID }) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: 'non permanent live', shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: 'non permanent live', shortUUID, checkType: 'presence' }) }) it('Should send a notification is a live replay of a permanent live is published', async function () { @@ -386,7 +464,7 @@ describe('Test user notifications', function () { const video = await findExternalSavedVideo(servers[1], liveDetails) expect(video).to.exist - await checkVideoIsPublished({ ...baseParams, videoName: video.name, shortUUID: video.shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: video.name, shortUUID: video.shortUUID, checkType: 'presence' }) }) }) @@ -408,7 +486,7 @@ describe('Test user notifications', function () { const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true }) await waitJobs(servers) - await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) + await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) const tasks: VideoStudioTask[] = [ { diff --git a/packages/tests/src/shared/notifications.ts b/packages/tests/src/shared/notifications.ts index abb28cd15..be471163b 100644 --- a/packages/tests/src/shared/notifications.ts +++ b/packages/tests/src/shared/notifications.ts @@ -7,7 +7,8 @@ import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, - UserNotificationType + UserNotificationType, + UserNotificationType_Type } from '@peertube/peertube-models' import { ConfigCommand, @@ -17,11 +18,13 @@ import { setAccessTokensToServers, setDefaultAccountAvatar, setDefaultChannelAvatar, - setDefaultVideoChannel + setDefaultVideoChannel, + waitJobs } from '@peertube/peertube-server-commands' import { expect } from 'chai' import { inspect } from 'util' import { MockSmtpServer } from './mock-servers/index.js' +import { wait } from '@peertube/peertube-core-utils' type CheckerBaseParams = { server: PeerTubeServer @@ -55,6 +58,24 @@ function getAllNotificationsSettings (): UserNotificationSetting { } } +async function waitUntilNotification (options: { + server: PeerTubeServer + notificationType: UserNotificationType_Type + token: string + fromDate: Date +}) { + const { server, fromDate, notificationType, token } = options + + do { + const { data } = await server.notifications.list({ start: 0, count: 5, token }) + if (data.some(n => n.type === notificationType && new Date(n.createdAt) >= fromDate)) break + + await wait(500) + } while (true) + + await waitJobs([ server ]) +} + async function checkNewVideoFromSubscription (options: CheckerBaseParams & { videoName: string shortUUID: string @@ -85,7 +106,37 @@ async function checkNewVideoFromSubscription (options: CheckerBaseParams & { await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) } -async function checkVideoIsPublished (options: CheckerBaseParams & { +async function checkNewLiveFromSubscription (options: CheckerBaseParams & { + videoName: string + shortUUID: string + checkType: CheckerType +}) { + const { videoName, shortUUID } = options + const notificationType = UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkVideo(notification.video, videoName, shortUUID) + checkActor(notification.video.channel) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.type !== UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION || n.video.name !== videoName + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkMyVideoIsPublished (options: CheckerBaseParams & { videoName: string shortUUID: string checkType: CheckerType @@ -780,10 +831,13 @@ export { getAllNotificationsSettings, + waitUntilNotification, + checkMyVideoImportIsFinished, checkUserRegistered, checkAutoInstanceFollowing, - checkVideoIsPublished, + checkMyVideoIsPublished, + checkNewLiveFromSubscription, checkNewVideoFromSubscription, checkNewActorFollow, checkNewCommentOnMyVideo, @@ -841,10 +895,9 @@ async function checkNotification (options: CheckerBaseParams & { if (check.mail) { // Last email - const email = emails - .slice() - .reverse() - .find(e => emailNotificationFinder(e)) + const email = emails.slice() + .reverse() + .find(e => emailNotificationFinder(e)) if (checkType === 'presence') { const texts = emails.map(e => e.text) diff --git a/server/core/lib/activitypub/process/process-announce.ts b/server/core/lib/activitypub/process/process-announce.ts index b9598c8f9..ac135eae2 100644 --- a/server/core/lib/activitypub/process/process-announce.ts +++ b/server/core/lib/activitypub/process/process-announce.ts @@ -61,5 +61,5 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act return undefined }) - if (videoCreated && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) + if (videoCreated && notify) Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video) } diff --git a/server/core/lib/activitypub/process/process-create.ts b/server/core/lib/activitypub/process/process-create.ts index 7366fe935..6e068c95b 100644 --- a/server/core/lib/activitypub/process/process-create.ts +++ b/server/core/lib/activitypub/process/process-create.ts @@ -73,7 +73,7 @@ async function processCreateVideo (videoToCreateData: VideoObject, notify: boole const syncParam = { rates: false, shares: false, comments: false, refreshVideo: false } const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) - if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) + if (created && notify) Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video) return video } diff --git a/server/core/lib/activitypub/videos/updater.ts b/server/core/lib/activitypub/videos/updater.ts index d40ddab31..a004da67d 100644 --- a/server/core/lib/activitypub/videos/updater.ts +++ b/server/core/lib/activitypub/videos/updater.ts @@ -93,11 +93,12 @@ export class APVideoUpdater extends APVideoAbstractBuilder { // Notify our users? if (this.wasPrivateVideo || this.wasUnlistedVideo) { - Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) + Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(videoUpdated) } if (videoUpdated.isLive && oldState !== videoUpdated.state) { PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) + Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(videoUpdated) } Hooks.runAction('action:activity-pub.remote-video.updated', { video: videoUpdated, videoAPObject: this.videoObject }) diff --git a/server/core/lib/job-queue/handlers/notify.ts b/server/core/lib/job-queue/handlers/notify.ts index a09f43bf8..3583d6567 100644 --- a/server/core/lib/job-queue/handlers/notify.ts +++ b/server/core/lib/job-queue/handlers/notify.ts @@ -23,5 +23,5 @@ async function doNotifyNewVideo (payload: NotifyPayload & { action: 'new-video' const refreshedVideo = await VideoModel.loadFull(payload.videoUUID) if (!refreshedVideo) return - Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) + Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(refreshedVideo) } diff --git a/server/core/lib/job-queue/handlers/video-import.ts b/server/core/lib/job-queue/handlers/video-import.ts index 4984f02eb..dfa0ca7dd 100644 --- a/server/core/lib/job-queue/handlers/video-import.ts +++ b/server/core/lib/job-queue/handlers/video-import.ts @@ -303,7 +303,7 @@ async function afterImportSuccess (options: { Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist) } else { - Notifier.Instance.notifyOnNewVideoIfNeeded(video) + Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video) } // Generate the storyboard in the job queue, and don't forget to federate an update after diff --git a/server/core/lib/live/live-manager.ts b/server/core/lib/live/live-manager.ts index de035ed9f..a5b382e33 100644 --- a/server/core/lib/live/live-manager.ts +++ b/server/core/lib/live/live-manager.ts @@ -35,6 +35,7 @@ import { computeResolutionsToTranscode } from '../transcoding/transcoding-resolu import { LiveQuotaStore } from './live-quota-store.js' import { cleanupAndDestroyPermanentLive, getLiveSegmentTime } from './live-utils.js' import { MuxingSession } from './shared/index.js' +import { Notifier } from '../notifier/notifier.js' // Disable node media server logs nodeMediaServerLogger.setLogType(0) @@ -417,6 +418,7 @@ class LiveManager { logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags }) } + Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video) PeerTubeSocket.Instance.sendVideoLiveNewState(video) Hooks.runAction('action:live.video.state.updated', { video }) diff --git a/server/core/lib/notifier/notifier.ts b/server/core/lib/notifier/notifier.ts index df2986cde..db835e89b 100644 --- a/server/core/lib/notifier/notifier.ts +++ b/server/core/lib/notifier/notifier.ts @@ -1,7 +1,7 @@ import { UserNotificationSettingValue, UserNotificationSettingValueType } from '@peertube/peertube-models' import { MRegistration, MUser, MUserDefault } from '@server/types/models/user/index.js' import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist.js' -import { logger } from '../../helpers/logger.js' +import { logger, loggerTagsFactory } from '../../helpers/logger.js' import { CONFIG } from '../../initializers/config.js' import { MAbuseFull, @@ -35,7 +35,7 @@ import { NewCommentForVideoOwner, NewPeerTubeVersionForAdmins, NewPluginVersionForAdmins, - NewVideoForSubscribers, + NewVideoOrLiveForSubscribers, OwnedPublicationAfterAutoUnblacklist, OwnedPublicationAfterScheduleUpdate, OwnedPublicationAfterTranscoding, @@ -44,10 +44,12 @@ import { UnblacklistForOwner } from './shared/index.js' +const lTags = loggerTagsFactory('notifier') + class Notifier { private readonly notificationModels = { - newVideo: [ NewVideoForSubscribers ], + newVideoOrLive: [ NewVideoOrLiveForSubscribers ], publicationAfterTranscoding: [ OwnedPublicationAfterTranscoding ], publicationAfterScheduleUpdate: [ OwnedPublicationAfterScheduleUpdate ], publicationAfterAutoUnblacklist: [ OwnedPublicationAfterAutoUnblacklist ], @@ -74,8 +76,10 @@ class Notifier { private constructor () { } - notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { - const models = this.notificationModels.newVideo + notifyOnNewVideoOrLiveIfNeeded (video: MVideoAccountLight): void { + const models = this.notificationModels.newVideoOrLive + + logger.debug('Notify on new video or live if needed', { video: video.url, ...lTags() }) this.sendNotifications(models, video) .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) @@ -84,6 +88,8 @@ class Notifier { notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { const models = this.notificationModels.publicationAfterTranscoding + logger.debug('Notify on published video after transcoding', { video: video.url, ...lTags() }) + this.sendNotifications(models, video) .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err })) } @@ -91,6 +97,8 @@ class Notifier { notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void { const models = this.notificationModels.publicationAfterScheduleUpdate + logger.debug('Notify on published video after scheduled update', { video: video.url, ...lTags() }) + this.sendNotifications(models, video) .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err })) } @@ -98,6 +106,8 @@ class Notifier { notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void { const models = this.notificationModels.publicationAfterAutoUnblacklist + logger.debug('Notify on published video after being removed from auto blacklist', { video: video.url, ...lTags() }) + this.sendNotifications(models, video) .catch(err => { logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err }) @@ -107,6 +117,8 @@ class Notifier { notifyOnNewComment (comment: MCommentOwnerVideo): void { const models = this.notificationModels.newComment + logger.debug('Notify on new comment', { comment: comment.url, ...lTags() }) + this.sendNotifications(models, comment) .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err })) } @@ -114,6 +126,8 @@ class Notifier { notifyOnNewAbuse (payload: NewAbusePayload): void { const models = this.notificationModels.newAbuse + logger.debug('Notify on new abuse', { abuse: payload.abuseInstance.id, ...lTags() }) + this.sendNotifications(models, payload) .catch(err => logger.error('Cannot notify of new abuse %d.', payload.abuseInstance.id, { err })) } @@ -121,6 +135,8 @@ class Notifier { notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { const models = this.notificationModels.newAutoBlacklist + logger.debug('Notify on video auto blacklist', { video: videoBlacklist?.Video?.url, ...lTags() }) + this.sendNotifications(models, videoBlacklist) .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) } @@ -128,6 +144,8 @@ class Notifier { notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { const models = this.notificationModels.newBlacklist + logger.debug('Notify on video manual blacklist', { video: videoBlacklist?.Video?.url, ...lTags() }) + this.sendNotifications(models, videoBlacklist) .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) } @@ -135,6 +153,8 @@ class Notifier { notifyOnVideoUnblacklist (video: MVideoFullLight): void { const models = this.notificationModels.unblacklist + logger.debug('Notify on video unblacklist', { video: video.url, ...lTags() }) + this.sendNotifications(models, video) .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) } @@ -142,6 +162,8 @@ class Notifier { notifyOnFinishedVideoImport (payload: ImportFinishedForOwnerPayload): void { const models = this.notificationModels.importFinished + logger.debug('Notify on finished video import', { import: payload.videoImport.getTargetIdentifier(), ...lTags() }) + this.sendNotifications(models, payload) .catch(err => { logger.error('Cannot notify owner that its video import %s is finished.', payload.videoImport.getTargetIdentifier(), { err }) @@ -151,6 +173,8 @@ class Notifier { notifyOnNewDirectRegistration (user: MUserDefault): void { const models = this.notificationModels.directRegistration + logger.debug('Notify on new direct registration', { user: user.username, ...lTags() }) + this.sendNotifications(models, user) .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) } @@ -158,6 +182,8 @@ class Notifier { notifyOnNewRegistrationRequest (registration: MRegistration): void { const models = this.notificationModels.registrationRequest + logger.debug('Notify on new registration request', { registration: registration.username, ...lTags() }) + this.sendNotifications(models, registration) .catch(err => logger.error('Cannot notify moderators of new registration request (%s).', registration.username, { err })) } @@ -165,20 +191,22 @@ class Notifier { notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { const models = this.notificationModels.userFollow + const following = actorFollow?.ActorFollowing?.VideoChannel?.getDisplayName() + const follower = actorFollow?.ActorFollower?.Account?.getDisplayName() + + logger.debug('Notify on new user follow', { following, follower, ...lTags() }) + this.sendNotifications(models, actorFollow) .catch(err => { - logger.error( - 'Cannot notify owner of channel %s of a new follow by %s.', - actorFollow.ActorFollowing.VideoChannel.getDisplayName(), - actorFollow.ActorFollower.Account.getDisplayName(), - { err } - ) + logger.error('Cannot notify owner of channel %s of a new follow by %s.', following, follower, { err }) }) } notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { const models = this.notificationModels.instanceFollow + logger.debug('Notify on new instance follow', { follower: actorFollow.ActorFollower.url, ...lTags() }) + this.sendNotifications(models, actorFollow) .catch(err => logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })) } @@ -186,6 +214,8 @@ class Notifier { notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void { const models = this.notificationModels.autoInstanceFollow + logger.debug('Notify on new instance auto following', { following: actorFollow.ActorFollowing.url, ...lTags() }) + this.sendNotifications(models, actorFollow) .catch(err => logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })) } @@ -193,6 +223,8 @@ class Notifier { notifyOnAbuseStateChange (abuse: MAbuseFull): void { const models = this.notificationModels.abuseStateChange + logger.debug('Notify on abuse state change', { abuse: abuse.id, ...lTags() }) + this.sendNotifications(models, abuse) .catch(err => logger.error('Cannot notify of abuse %d state change.', abuse.id, { err })) } @@ -200,6 +232,8 @@ class Notifier { notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void { const models = this.notificationModels.newAbuseMessage + logger.debug('Notify on abuse message', { abuse: abuse.id, message, ...lTags() }) + this.sendNotifications(models, { abuse, message }) .catch(err => logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })) } @@ -207,13 +241,17 @@ class Notifier { notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { const models = this.notificationModels.newPeertubeVersion + logger.debug('Notify on new peertube version', { currentVersion: application.version, latestVersion, ...lTags() }) + this.sendNotifications(models, { application, latestVersion }) - .catch(err => logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })) + .catch(err => logger.error('Cannot notify on new PeerTube version %s.', latestVersion, { err })) } notifyOfNewPluginVersion (plugin: MPlugin) { const models = this.notificationModels.newPluginVersion + logger.debug('Notify on new plugin version', { plugin: plugin.name, ...lTags() }) + this.sendNotifications(models, plugin) .catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })) } @@ -221,6 +259,8 @@ class Notifier { notifyOfFinishedVideoStudioEdition (video: MVideoFullLight) { const models = this.notificationModels.videoStudioEditionFinished + logger.debug('Notify on finished video studio edition', { video: video.url, ...lTags() }) + this.sendNotifications(models, video) .catch(err => logger.error('Cannot notify on finished studio edition %s.', video.url, { err })) } diff --git a/server/core/lib/notifier/shared/video-publication/index.ts b/server/core/lib/notifier/shared/video-publication/index.ts index fc36a9899..fdc235456 100644 --- a/server/core/lib/notifier/shared/video-publication/index.ts +++ b/server/core/lib/notifier/shared/video-publication/index.ts @@ -1,4 +1,4 @@ -export * from './new-video-for-subscribers.js' +export * from './new-video-or-live-for-subscribers.js' export * from './import-finished-for-owner.js' export * from './owned-publication-after-auto-unblacklist.js' export * from './owned-publication-after-schedule-update.js' diff --git a/server/core/lib/notifier/shared/video-publication/new-video-for-subscribers.ts b/server/core/lib/notifier/shared/video-publication/new-video-or-live-for-subscribers.ts similarity index 67% rename from server/core/lib/notifier/shared/video-publication/new-video-for-subscribers.ts rename to server/core/lib/notifier/shared/video-publication/new-video-or-live-for-subscribers.ts index df2d89fc9..9bfe7720c 100644 --- a/server/core/lib/notifier/shared/video-publication/new-video-for-subscribers.ts +++ b/server/core/lib/notifier/shared/video-publication/new-video-or-live-for-subscribers.ts @@ -6,7 +6,7 @@ import { MUserWithNotificationSetting, MVideoAccountLight, UserNotificationModel import { UserNotificationType, VideoPrivacy, VideoState } from '@peertube/peertube-models' import { AbstractNotification } from '../common/abstract-notification.js' -export class NewVideoForSubscribers extends AbstractNotification { +export class NewVideoOrLiveForSubscribers extends AbstractNotification { private users: MUserWithNotificationSetting[] async prepare () { @@ -32,7 +32,10 @@ export class NewVideoForSubscribers extends AbstractNotification ({ - type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, + type: this.payload.isLive + ? UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION + : UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, + userId: user.id, videoId: this.payload.id }) @@ -41,10 +44,18 @@ export class NewVideoForSubscribers extends AbstractNotification