newInstanceFollower: $localize`Your instance has a new follower`,
autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
abuseNewMessage: $localize`An abuse report received a new message`,
- abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`
+ abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
+ newPeerTubeVersion: $localize`A new PeerTube version is available`,
+ newPluginVersion: $localize`One of your plugin/theme has a new available version`
}
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS,
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
- autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION
+ autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
+ newPeerTubeVersion: UserRight.MANAGE_DEBUG,
+ newPluginVersion: UserRight.MANAGE_DEBUG
}
}
AbuseState,
ActorInfo,
FollowState,
+ PluginType,
UserNotification as UserNotificationServer,
UserNotificationType,
UserRight,
}
}
+ plugin?: {
+ name: string
+ type: PluginType
+ latestVersion: string
+ }
+
+ peertube?: {
+ latestVersion: string
+ }
+
createdAt: string
updatedAt: string
// Additional fields
videoUrl?: string
commentUrl?: any[]
+
abuseUrl?: string
abuseQueryParams?: { [id: string]: string } = {}
+
videoAutoBlacklistUrl?: string
+
accountUrl?: string
+
videoImportIdentifier?: string
videoImportUrl?: string
+
instanceFollowUrl?: string
+ peertubeVersionLink?: string
+
+ pluginUrl?: string
+ pluginQueryParams?: { [id: string]: string } = {}
+
constructor (hash: UserNotificationServer, user: AuthUser) {
this.id = hash.id
this.type = hash.type
this.actorFollow = hash.actorFollow
if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
+ this.plugin = hash.plugin
+ this.peertube = hash.peertube
+
this.createdAt = hash.createdAt
this.updatedAt = hash.updatedAt
case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
this.instanceFollowUrl = '/admin/follows/following-list'
break
+
+ case UserNotificationType.NEW_PEERTUBE_VERSION:
+ this.peertubeVersionLink = 'https://joinpeertube.org/news'
+ break
+
+ case UserNotificationType.NEW_PLUGIN_VERSION:
+ this.pluginUrl = `/admin/plugins/list-installed`
+ this.pluginQueryParams.pluginType = this.plugin.type + ''
+ break
}
} catch (err) {
this.type = null
</div>
</ng-container>
+ <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
+ <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+ <div class="message" i18n>
+ <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
+ </div>
+ </ng-container>
+
+ <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
+ <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+ <div class="message" i18n>
+ <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
+ </div>
+ </ng-container>
+
<ng-container *ngSwitchDefault>
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
}
loadNotifications (reset?: boolean) {
- this.userNotificationService.listMyNotifications({
+ const options = {
pagination: this.componentPagination,
ignoreLoadingBar: this.ignoreLoadingBar,
sort: {
// if we order by creation date, we want DESC. all other fields are ASC (like unread).
order: this.sortField === 'createdAt' ? -1 : 1
}
- })
+ }
+
+ this.userNotificationService.listMyNotifications(options)
.subscribe(
result => {
this.notifications = reset ? result.data : this.notifications.concat(result.data)
# We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
cleanup_remote_interactions: false
+peertube:
+ check_latest_version:
+ # Check and notify admins of new PeerTube versions
+ enabled: true
+ # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
+ url: 'https://joinpeertube.org/api/v1/versions.json'
+
cache:
previews:
size: 500 # Max number of previews you want to cache
# We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
cleanup_remote_interactions: false
+peertube:
+ check_latest_version:
+ # Check and notify admins of new PeerTube versions
+ enabled: true
+ # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
+ url: 'https://joinpeertube.org/api/v1/versions.json'
###############################################################################
#
contact_form:
enabled: true
+peertube:
+ check_latest_version:
+ enabled: false
+
redundancy:
videos:
check_interval: '1 minute'
import { PeerTubeSocket } from './server/lib/peertube-socket'
import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler'
+import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-version-check-scheduler'
import { Hooks } from './server/lib/plugins/hooks'
import { PluginManager } from './server/lib/plugins/plugin-manager'
import { LiveManager } from './server/lib/live-manager'
RemoveOldHistoryScheduler.Instance.enable()
RemoveOldViewsScheduler.Instance.enable()
PluginsCheckScheduler.Instance.enable()
+ PeerTubeVersionCheckScheduler.Instance.enable()
AutoFollowIndexInstances.Instance.enable()
// Redis initialization
newInstanceFollower: body.newInstanceFollower,
autoInstanceFollowing: body.autoInstanceFollowing,
abuseNewMessage: body.abuseNewMessage,
- abuseStateChange: body.abuseStateChange
+ abuseStateChange: body.abuseStateChange,
+ newPeerTubeVersion: body.newPeerTubeVersion,
+ newPluginVersion: body.newPluginVersion
}
await UserNotificationSettingModel.update(values, query)
}
}
+type SemVersion = { major: number, minor: number, patch: number }
function parseSemVersion (s: string) {
const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
major: parseInt(parsed[1]),
minor: parseInt(parsed[2]),
patch: parseInt(parsed[3])
- }
+ } as SemVersion
}
const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
'theme.default',
'remote_redundancy.videos.accept_from',
'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
+ 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
'search.search_index.disable_local_search', 'search.search_index.is_default_search',
'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
}
},
+ PEERTUBE: {
+ CHECK_LATEST_VERSION: {
+ ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
+ URL: config.get<string>('peertube.check_latest_version.url')
+ }
+ },
ADMIN: {
get EMAIL () { return config.get<string>('admin.email') }
},
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 610
+const LAST_MIGRATION_VERSION = 625
// ---------------------------------------------------------------------------
updateVideos: 60000, // 1 minute
youtubeDLUpdate: 60000 * 60 * 24, // 1 day
checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
+ checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
removeOldViews: 60000 * 60 * 24, // 1 day
removeOldHistory: 60000 * 60 * 24, // 1 day
SCHEDULER_INTERVALS_MS.updateVideos = 5000
SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
+ SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
REPEAT_JOBS['videos-views'] = { every: 5000 }
REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+ {
+ const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
+
+ for (const column of notificationSettingColumns) {
+ const data = {
+ type: Sequelize.INTEGER,
+ defaultValue: null,
+ allowNull: true
+ }
+ await utils.queryInterface.addColumn('userNotificationSetting', column, data)
+ }
+
+ {
+ const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
+ await utils.sequelize.query(query)
+ }
+
+ for (const column of notificationSettingColumns) {
+ const data = {
+ type: Sequelize.INTEGER,
+ defaultValue: null,
+ allowNull: false
+ }
+ await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
+ }
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+
+ {
+ const data = {
+ type: Sequelize.STRING,
+ defaultValue: null,
+ allowNull: true
+ }
+ await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+
+ {
+ await utils.sequelize.query(`
+ ALTER TABLE "userNotification"
+ ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+ `)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config'
import { WEBSERVER } from '../initializers/constants'
-import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
import { JobQueue } from './job-queue'
}
async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
- const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
+ const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
videoName: videoBlacklist.Video.name,
action: {
text: 'Review autoblacklist',
- url: VIDEO_AUTO_BLACKLIST_URL
+ url: videoAutoBlacklistUrl
}
}
}
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
+ const subject = `A new PeerTube version is available: ${latestVersion}`
+
+ const emailPayload: EmailPayload = {
+ to,
+ template: 'peertube-version-new',
+ subject,
+ text: subject,
+ locals: {
+ latestVersion
+ }
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
+ addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
+ const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
+
+ const subject = `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`
+
+ const emailPayload: EmailPayload = {
+ to,
+ template: 'plugin-version-new',
+ subject,
+ text: subject,
+ locals: {
+ pluginName: plugin.name,
+ latestVersion: plugin.latestVersion,
+ pluginUrl
+ }
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
const emailPayload: EmailPayload = {
template: 'password-reset',
--- /dev/null
+extends ../common/greetings
+
+block title
+ | New PeerTube version available
+
+block content
+ p
+ | A new version of PeerTube is available: #{latestVersion}.
+ | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
--- /dev/null
+extends ../common/greetings
+
+block title
+ | New plugin version available
+
+block content
+ p
+ | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
+ | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification'
-import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer'
})
}
+ notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
+ this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
+ .catch(err => {
+ logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
+ })
+ }
+
+ notifyOfNewPluginVersion (plugin: MPlugin) {
+ this.notifyAdminsOfNewPluginVersion(plugin)
+ .catch(err => {
+ logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
+ })
+ }
+
private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
// List all followers that are users
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
}
+ private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
+ // Use the debug right to know who is an administrator
+ const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
+ if (admins.length === 0) return
+
+ logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
+
+ function settingGetter (user: MUserWithNotificationSetting) {
+ return user.NotificationSetting.newPeerTubeVersion
+ }
+
+ async function notificationCreator (user: MUserWithNotificationSetting) {
+ const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+ type: UserNotificationType.NEW_PEERTUBE_VERSION,
+ userId: user.id,
+ applicationId: application.id
+ })
+ notification.Application = application
+
+ return notification
+ }
+
+ function emailSender (emails: string[]) {
+ return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
+ }
+
+ return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+ }
+
+ private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
+ // Use the debug right to know who is an administrator
+ const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
+ if (admins.length === 0) return
+
+ logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
+
+ function settingGetter (user: MUserWithNotificationSetting) {
+ return user.NotificationSetting.newPluginVersion
+ }
+
+ async function notificationCreator (user: MUserWithNotificationSetting) {
+ const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+ type: UserNotificationType.NEW_PLUGIN_VERSION,
+ userId: user.id,
+ pluginId: plugin.id
+ })
+ notification.Plugin = plugin
+
+ return notification
+ }
+
+ function emailSender (emails: string[]) {
+ return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
+ }
+
+ return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+ }
+
private async notify<T extends MUserWithNotificationSetting> (options: {
users: T[]
notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
--- /dev/null
+
+import { doJSONRequest } from '@server/helpers/requests'
+import { ApplicationModel } from '@server/models/application/application'
+import { compareSemVer } from '@shared/core-utils'
+import { JoinPeerTubeVersions } from '@shared/models'
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
+import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
+import { Notifier } from '../notifier'
+import { AbstractScheduler } from './abstract-scheduler'
+
+export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
+
+ private static instance: AbstractScheduler
+
+ protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
+
+ private constructor () {
+ super()
+ }
+
+ protected async internalExecute () {
+ return this.checkLatestVersion()
+ }
+
+ private async checkLatestVersion () {
+ if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
+
+ logger.info('Checking latest PeerTube version.')
+
+ const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
+
+ if (!body?.peertube?.latestVersion) {
+ logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
+ return
+ }
+
+ const latestVersion = body.peertube.latestVersion
+ const application = await ApplicationModel.load()
+
+ // Already checked this version
+ if (application.latestPeerTubeVersion === latestVersion) return
+
+ if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
+ application.latestPeerTubeVersion = latestVersion
+ await application.save()
+
+ Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
+ }
+ }
+
+ static get Instance () {
+ return this.instance || (this.instance = new this())
+ }
+}
import { chunk } from 'lodash'
import { getLatestPluginsVersion } from '../plugins/plugin-index'
import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
+import { Notifier } from '../notifier'
export class PluginsCheckScheduler extends AbstractScheduler {
plugin.latestVersion = result.latestVersion
await plugin.save()
+ // Notify if there is an higher plugin version available
+ if (compareSemVer(plugin.version, result.latestVersion) < 0) {
+ Notifier.Instance.notifyOfNewPluginVersion(plugin)
+ }
+
logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
}
}
newInstanceFollower: UserNotificationSettingValue.WEB,
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
- autoInstanceFollowing: UserNotificationSettingValue.WEB
+ autoInstanceFollowing: UserNotificationSettingValue.WEB,
+ newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ newPluginVersion: UserNotificationSettingValue.WEB
}
return UserNotificationSettingModel.create(values, { transaction: t })
@Column
abuseNewMessage: UserNotificationSettingValue
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingNewPeerTubeVersion',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
+ )
+ @Column
+ newPeerTubeVersion: UserNotificationSettingValue
+
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingNewPeerPluginVersion',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
+ )
+ @Column
+ newPluginVersion: UserNotificationSettingValue
+
@ForeignKey(() => UserModel)
@Column
userId: number
newInstanceFollower: this.newInstanceFollower,
autoInstanceFollowing: this.autoInstanceFollowing,
abuseNewMessage: this.abuseNewMessage,
- abuseStateChange: this.abuseStateChange
+ abuseStateChange: this.abuseStateChange,
+ newPeerTubeVersion: this.newPeerTubeVersion,
+ newPluginVersion: this.newPluginVersion
}
}
}
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
+import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
attributes: [ 'id' ],
model: VideoAbuseModel.unscoped(),
required: false,
- include: [ buildVideoInclude(true) ]
+ include: [ buildVideoInclude(false) ]
},
{
attributes: [ 'id' ],
{
attributes: [ 'id', 'originCommentId' ],
model: VideoCommentModel.unscoped(),
- required: true,
+ required: false,
include: [
{
attributes: [ 'id', 'name', 'uuid' ],
model: VideoModel.unscoped(),
- required: true
+ required: false
}
]
}
{
model: AccountModel,
as: 'FlaggedAccount',
- required: true,
+ required: false,
include: [ buildActorWithAvatarInclude() ]
}
]
include: [ buildVideoInclude(false) ]
},
+ {
+ attributes: [ 'id', 'name', 'type', 'latestVersion' ],
+ model: PluginModel.unscoped(),
+ required: false
+ },
+
+ {
+ attributes: [ 'id', 'latestPeerTubeVersion' ],
+ model: ApplicationModel.unscoped(),
+ required: false
+ },
+
{
attributes: [ 'id', 'state' ],
model: ActorFollowModel.unscoped(),
[Op.ne]: null
}
}
+ },
+ {
+ fields: [ 'pluginId' ],
+ where: {
+ pluginId: {
+ [Op.ne]: null
+ }
+ }
+ },
+ {
+ fields: [ 'applicationId' ],
+ where: {
+ applicationId: {
+ [Op.ne]: null
+ }
+ }
}
] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
})
ActorFollow: ActorFollowModel
+ @ForeignKey(() => PluginModel)
+ @Column
+ pluginId: number
+
+ @BelongsTo(() => PluginModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ Plugin: PluginModel
+
+ @ForeignKey(() => ApplicationModel)
+ @Column
+ applicationId: number
+
+ @BelongsTo(() => ApplicationModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ Application: ApplicationModel
+
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
const where = { userId }
}
: undefined
+ const plugin = this.Plugin
+ ? {
+ name: this.Plugin.name,
+ type: this.Plugin.type,
+ latestVersion: this.Plugin.latestVersion
+ }
+ : undefined
+
+ const peertube = this.Application
+ ? { latestVersion: this.Application.latestPeerTubeVersion }
+ : undefined
+
return {
id: this.id,
type: this.type,
videoBlacklist,
account,
actorFollow,
+ plugin,
+ peertube,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}
? {
threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
- video: {
- id: abuse.VideoCommentAbuse.VideoComment.Video.id,
- name: abuse.VideoCommentAbuse.VideoComment.Video.name,
- uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
- }
+ video: abuse.VideoCommentAbuse.VideoComment.Video
+ ? {
+ id: abuse.VideoCommentAbuse.VideoComment.Video.id,
+ name: abuse.VideoCommentAbuse.VideoComment.Video.name,
+ uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
+ }
+ : undefined
}
: undefined
const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
- const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
+ const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
return {
id: abuse.id,
@Column
migrationVersion: number
+ @AllowNull(true)
+ @Column
+ latestPeerTubeVersion: string
+
@HasOne(() => AccountModel, {
foreignKey: {
allowNull: true
newInstanceFollower: UserNotificationSettingValue.WEB,
autoInstanceFollowing: UserNotificationSettingValue.WEB,
abuseNewMessage: UserNotificationSettingValue.WEB,
- abuseStateChange: UserNotificationSettingValue.WEB
+ abuseStateChange: UserNotificationSettingValue.WEB,
+ newPeerTubeVersion: UserNotificationSettingValue.WEB,
+ newPluginVersion: UserNotificationSettingValue.WEB
}
it('Should fail with missing fields', async function () {
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { expect } from 'chai'
+import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
+import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
+import { ServerInfo } from '../../../../shared/extra-utils/index'
+import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
+import {
+ CheckerBaseParams,
+ checkNewPeerTubeVersion,
+ checkNewPluginVersion,
+ prepareNotificationsTest
+} from '../../../../shared/extra-utils/users/user-notifications'
+import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
+import { PluginType } from '@shared/models'
+
+describe('Test admin notifications', function () {
+ let server: ServerInfo
+ let userNotifications: UserNotification[] = []
+ let adminNotifications: UserNotification[] = []
+ let emails: object[] = []
+ let baseParams: CheckerBaseParams
+ let joinPeerTubeServer: MockJoinPeerTubeVersions
+
+ before(async function () {
+ this.timeout(120000)
+
+ const config = {
+ peertube: {
+ check_latest_version: {
+ enabled: true,
+ url: 'http://localhost:42102/versions.json'
+ }
+ },
+ plugins: {
+ index: {
+ enabled: true,
+ check_latest_versions_interval: '5 seconds'
+ }
+ }
+ }
+
+ const res = await prepareNotificationsTest(1, config)
+ emails = res.emails
+ server = res.servers[0]
+
+ userNotifications = res.userNotifications
+ adminNotifications = res.adminNotifications
+
+ baseParams = {
+ server: server,
+ emails,
+ socketNotifications: adminNotifications,
+ token: server.accessToken
+ }
+
+ await installPlugin({
+ url: server.url,
+ accessToken: server.accessToken,
+ npmName: 'peertube-plugin-hello-world'
+ })
+
+ await installPlugin({
+ url: server.url,
+ accessToken: server.accessToken,
+ npmName: 'peertube-theme-background-red'
+ })
+
+ joinPeerTubeServer = new MockJoinPeerTubeVersions()
+ await joinPeerTubeServer.initialize()
+ })
+
+ describe('Latest PeerTube version notification', function () {
+
+ it('Should not send a notification to admins if there is not a new version', async function () {
+ this.timeout(30000)
+
+ joinPeerTubeServer.setLatestVersion('1.4.2')
+
+ await wait(3000)
+ await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
+ })
+
+ it('Should send a notification to admins on new plugin version', async function () {
+ this.timeout(30000)
+
+ joinPeerTubeServer.setLatestVersion('15.4.2')
+
+ await wait(3000)
+ await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
+ })
+
+ it('Should not send the same notification to admins', async function () {
+ this.timeout(30000)
+
+ await wait(3000)
+ expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
+ })
+
+ it('Should not have sent a notification to users', async function () {
+ this.timeout(30000)
+
+ expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
+ })
+
+ it('Should send a new notification after a new release', async function () {
+ this.timeout(30000)
+
+ joinPeerTubeServer.setLatestVersion('15.4.3')
+
+ await wait(3000)
+ await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
+ expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
+ })
+ })
+
+ describe('Latest plugin version notification', function () {
+
+ it('Should not send a notification to admins if there is no new plugin version', async function () {
+ this.timeout(30000)
+
+ await wait(6000)
+ await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
+ })
+
+ it('Should send a notification to admins on new plugin version', async function () {
+ this.timeout(30000)
+
+ await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+ await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+ await wait(6000)
+
+ await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
+ })
+
+ it('Should not send the same notification to admins', async function () {
+ this.timeout(30000)
+
+ await wait(6000)
+
+ expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
+ })
+
+ it('Should not have sent a notification to users', async function () {
+ expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
+ })
+
+ it('Should send a new notification after a new plugin release', async function () {
+ this.timeout(30000)
+
+ await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+ await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+ await wait(6000)
+
+ expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
+ })
+ })
+
+ after(async function () {
+ MockSmtpServer.Instance.kill()
+
+ await cleanupTests([ server ])
+ })
+})
+import './admin-notifications'
import './comments-notifications'
import './moderation-notifications'
import './notifications-api'
--- /dev/null
+import { ApplicationModel } from '@server/models/application/application'
+
+// ############################################################################
+
+export type MApplication = Omit<ApplicationModel, 'Account'>
--- /dev/null
+export * from './application'
export * from './account'
+export * from './application'
export * from './moderation'
export * from './oauth'
export * from './server'
import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
+import { ApplicationModel } from '@server/models/application/application'
+import { PluginModel } from '@server/models/server/plugin'
import { PickWith, PickWithOpt } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account'
Pick<ActorFollowModel, 'id' | 'state'> &
PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
+
+ export type PluginInclude =
+ Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
+
+ export type ApplicationInclude =
+ Pick<ApplicationModel, 'latestPeerTubeVersion'>
}
// ############################################################################
export type MUserNotification =
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
- 'VideoImport' | 'Account' | 'ActorFollow'>
+ 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
// ############################################################################
Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
+ Use<'Plugin', UserNotificationIncludes.PluginInclude> &
+ Use<'Application', UserNotificationIncludes.ApplicationInclude> &
Use<'Account', UserNotificationIncludes.AccountIncludeActor>
export * from './bulk/bulk'
export * from './cli/cli'
export * from './feeds/feeds'
-export * from './instances-index/mock-instances-index'
+export * from './mock-servers/mock-instances-index'
export * from './miscs/miscs'
export * from './miscs/sql'
export * from './miscs/stubs'
}
}
-function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
const seq = getSequelize(internalServerNumber)
const options = { type: QueryTypes.UPDATE }
- return seq.query(`UPDATE "plugin" SET "version" = '${newVersion}' WHERE "name" = '${pluginName}'`, options)
+ return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
+}
+
+function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+ return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
+}
+
+function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+ return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
}
function setActorFollowScores (internalServerNumber: number, newScore: number) {
setActorField,
countVideoViewsOf,
setPluginVersion,
+ setPluginLatestVersion,
selectQuery,
deleteAll,
updateQuery,
--- /dev/null
+import * as express from 'express'
+
+export class MockJoinPeerTubeVersions {
+ private latestVersion: string
+
+ initialize () {
+ return new Promise<void>(res => {
+ const app = express()
+
+ app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
+
+ return next()
+ })
+
+ app.get('/versions.json', (req: express.Request, res: express.Response) => {
+ return res.json({
+ peertube: {
+ latestVersion: this.latestVersion
+ }
+ })
+ })
+
+ app.listen(42102, () => res())
+ })
+ }
+
+ setLatestVersion (latestVersion: string) {
+ this.latestVersion = latestVersion
+ }
+}
import { expect } from 'chai'
import { inspect } from 'util'
-import { AbuseState } from '@shared/models'
+import { AbuseState, PluginType } from '@shared/models'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
import { MockSmtpServer } from '../miscs/email'
import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
import { getUserNotificationSocket } from '../socket/socket-io'
import { setAccessTokensToServers, userLogin } from './login'
import { createUser, getMyUserInformation } from './users'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
function updateMyNotificationSettings (
url: string,
await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
}
-function getAllNotificationsSettings () {
+async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) {
+ const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ expect(notification.peertube).to.exist
+ expect(notification.peertube.latestVersion).to.equal(latestVersion)
+ } else {
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
+ })
+ }
+ }
+
+ function emailNotificationFinder (email: object) {
+ const text = email['text']
+
+ return text.includes(latestVersion)
+ }
+
+ await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
+async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) {
+ const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ expect(notification.plugin.name).to.equal(pluginName)
+ expect(notification.plugin.type).to.equal(pluginType)
+ } else {
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
+ })
+ }
+ }
+
+ function emailNotificationFinder (email: object) {
+ const text = email['text']
+
+ return text.includes(pluginName)
+ }
+
+ await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
+function getAllNotificationsSettings (): UserNotificationSetting {
return {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
- autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
- } as UserNotificationSetting
+ autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
+ }
}
-async function prepareNotificationsTest (serversCount = 3) {
+async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
const userNotifications: UserNotification[] = []
const adminNotifications: UserNotification[] = []
const adminNotificationsServer2: UserNotification[] = []
limit: 20
}
}
- const servers = await flushAndRunMultipleServers(serversCount, overrideConfig)
+ const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
await setAccessTokensToServers(servers)
checkNewInstanceFollower,
prepareNotificationsTest,
checkNewCommentAbuseForModerators,
- checkNewAccountAbuseForModerators
+ checkNewAccountAbuseForModerators,
+ checkNewPeerTubeVersion,
+ checkNewPluginVersion
}
export * from './users'
export * from './videos'
export * from './feeds'
+export * from './joinpeertube'
export * from './overviews'
export * from './plugins'
export * from './search'
--- /dev/null
+export * from './versions.model'
--- /dev/null
+export interface JoinPeerTubeVersions {
+ peertube: {
+ latestVersion: string
+ }
+}
abuseStateChange: UserNotificationSettingValue
abuseNewMessage: UserNotificationSettingValue
+
+ newPeerTubeVersion: UserNotificationSettingValue
+ newPluginVersion: UserNotificationSettingValue
}
import { FollowState } from '../actors'
import { AbuseState } from '../moderation'
+import { PluginType } from '../plugins'
export const enum UserNotificationType {
NEW_VIDEO_FROM_SUBSCRIPTION = 1,
ABUSE_STATE_CHANGE = 15,
- ABUSE_NEW_MESSAGE = 16
+ ABUSE_NEW_MESSAGE = 16,
+
+ NEW_PLUGIN_VERSION = 17,
+ NEW_PEERTUBE_VERSION = 18
}
export interface VideoInfo {
}
}
+ plugin?: {
+ name: string
+ type: PluginType
+ latestVersion: string
+ }
+
+ peertube?: {
+ latestVersion: string
+ }
+
createdAt: string
updatedAt: string
}