Add notification on subscription live stream

This commit is contained in:
Chocobozzz 2024-02-15 13:55:12 +01:00
parent 4300cc1ee1
commit a012d6c2a9
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 273 additions and 55 deletions

View File

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

View File

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

View File

@ -6,9 +6,7 @@
<ng-container [ngSwitch]="notification.type">
<ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
<ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
<ng-template #hasVideo>
@if (notification.video) {
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
</a>
@ -16,15 +14,13 @@
<div class="message" i18n>
{{ notification.video.channel.displayName }} published a new video: <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
</div>
</ng-template>
<ng-template #noVideo>
} @else {
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
The notification concerns a video now unavailable
</div>
</ng-template>
}
</ng-container>
<ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
@ -224,6 +220,24 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="21"> <!-- UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION -->
@if (notification.video) {
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
</a>
<div class="message" i18n>
{{ notification.video.channel.displayName }} is live streaming in <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
</div>
} @else {
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
The notification concerns a video now unavailable
</div>
}
</ng-container>
<ng-container *ngSwitchDefault>
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>

View File

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

View File

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

View File

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

View File

@ -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[] = [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <MVideoAccountLight> {
export class NewVideoOrLiveForSubscribers extends AbstractNotification <MVideoAccountLight> {
private users: MUserWithNotificationSetting[]
async prepare () {
@ -32,7 +32,10 @@ export class NewVideoForSubscribers extends AbstractNotification <MVideoAccountL
createNotification (user: MUserWithNotificationSetting) {
const notification = UserNotificationModel.build<UserNotificationModelForApi>({
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 <MVideoAccountL
return notification
}
// ---------------------------------------------------------------------------
createEmail (to: string) {
const channelName = this.payload.VideoChannel.getDisplayName()
const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath()
if (this.payload.isLive) return this.createLiveEmail(to, channelName, videoUrl)
return this.createVideoEmail(to, channelName, videoUrl)
}
private createVideoEmail (to: string, channelName: string, videoUrl: string) {
return {
to,
subject: channelName + ' just published a new video',
@ -58,4 +69,19 @@ export class NewVideoForSubscribers extends AbstractNotification <MVideoAccountL
}
}
}
private createLiveEmail (to: string, channelName: string, videoUrl: string) {
return {
to,
subject: channelName + ' is live streaming',
text: `Your subscription ${channelName} is live streaming in "${this.payload.name}".`,
locals: {
title: 'New content ',
action: {
text: 'View video',
url: videoUrl
}
}
}
}
}

View File

@ -110,7 +110,7 @@ async function unblacklistVideo (videoBlacklist: MVideoBlacklist, video: MVideoF
// Delete on object so new video notifications will send
delete video.VideoBlacklist
Notifier.Instance.notifyOnNewVideoIfNeeded(video)
Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video)
}
}

View File

@ -185,7 +185,7 @@ async function moveToPublishedState (options: {
}
if (isNewVideo) {
Notifier.Instance.notifyOnNewVideoIfNeeded(video)
Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video)
if (previousState === VideoState.TO_TRANSCODE) {
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video)