Add new plugin/peertube version notifs

pull/3888/head
Chocobozzz 2021-03-11 16:54:52 +01:00
parent 3fbc697433
commit 32a18cbf33
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
44 changed files with 808 additions and 37 deletions

View File

@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
newInstanceFollower: $localize`Your instance has a new follower`, newInstanceFollower: $localize`Your instance has a new follower`,
autoInstanceFollowing: $localize`Your instance automatically followed another instance`, autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
abuseNewMessage: $localize`An abuse report received a new message`, 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)[] this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS, newUserRegistration: UserRight.MANAGE_USERS,
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
newPeerTubeVersion: UserRight.MANAGE_DEBUG,
newPluginVersion: UserRight.MANAGE_DEBUG
} }
} }

View File

@ -6,6 +6,7 @@ import {
AbuseState, AbuseState,
ActorInfo, ActorInfo,
FollowState, FollowState,
PluginType,
UserNotification as UserNotificationServer, UserNotification as UserNotificationServer,
UserNotificationType, UserNotificationType,
UserRight, UserRight,
@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
} }
} }
plugin?: {
name: string
type: PluginType
latestVersion: string
}
peertube?: {
latestVersion: string
}
createdAt: string createdAt: string
updatedAt: string updatedAt: string
// Additional fields // Additional fields
videoUrl?: string videoUrl?: string
commentUrl?: any[] commentUrl?: any[]
abuseUrl?: string abuseUrl?: string
abuseQueryParams?: { [id: string]: string } = {} abuseQueryParams?: { [id: string]: string } = {}
videoAutoBlacklistUrl?: string videoAutoBlacklistUrl?: string
accountUrl?: string accountUrl?: string
videoImportIdentifier?: string videoImportIdentifier?: string
videoImportUrl?: string videoImportUrl?: string
instanceFollowUrl?: string instanceFollowUrl?: string
peertubeVersionLink?: string
pluginUrl?: string
pluginQueryParams?: { [id: string]: string } = {}
constructor (hash: UserNotificationServer, user: AuthUser) { constructor (hash: UserNotificationServer, user: AuthUser) {
this.id = hash.id this.id = hash.id
this.type = hash.type this.type = hash.type
@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
this.actorFollow = hash.actorFollow this.actorFollow = hash.actorFollow
if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
this.plugin = hash.plugin
this.peertube = hash.peertube
this.createdAt = hash.createdAt this.createdAt = hash.createdAt
this.updatedAt = hash.updatedAt this.updatedAt = hash.updatedAt
@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
case UserNotificationType.AUTO_INSTANCE_FOLLOWING: case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
this.instanceFollowUrl = '/admin/follows/following-list' this.instanceFollowUrl = '/admin/follows/following-list'
break 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) { } catch (err) {
this.type = null this.type = null

View File

@ -191,6 +191,22 @@
</div> </div>
</ng-container> </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> <ng-container *ngSwitchDefault>
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>

View File

@ -45,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
} }
loadNotifications (reset?: boolean) { loadNotifications (reset?: boolean) {
this.userNotificationService.listMyNotifications({ const options = {
pagination: this.componentPagination, pagination: this.componentPagination,
ignoreLoadingBar: this.ignoreLoadingBar, ignoreLoadingBar: this.ignoreLoadingBar,
sort: { sort: {
@ -53,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
// if we order by creation date, we want DESC. all other fields are ASC (like unread). // if we order by creation date, we want DESC. all other fields are ASC (like unread).
order: this.sortField === 'createdAt' ? -1 : 1 order: this.sortField === 'createdAt' ? -1 : 1
} }
}) }
this.userNotificationService.listMyNotifications(options)
.subscribe( .subscribe(
result => { result => {
this.notifications = reset ? result.data : this.notifications.concat(result.data) this.notifications = reset ? result.data : this.notifications.concat(result.data)

View File

@ -198,6 +198,13 @@ federation:
# We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes # 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 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: cache:
previews: previews:
size: 500 # Max number of previews you want to cache size: 500 # Max number of previews you want to cache

View File

@ -196,6 +196,12 @@ federation:
# We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes # 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 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'
############################################################################### ###############################################################################
# #

View File

@ -38,6 +38,10 @@ log:
contact_form: contact_form:
enabled: true enabled: true
peertube:
check_latest_version:
enabled: false
redundancy: redundancy:
videos: videos:
check_interval: '1 minute' check_interval: '1 minute'

View File

@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
import { PeerTubeSocket } from './server/lib/peertube-socket' import { PeerTubeSocket } from './server/lib/peertube-socket'
import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler' 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 { Hooks } from './server/lib/plugins/hooks'
import { PluginManager } from './server/lib/plugins/plugin-manager' import { PluginManager } from './server/lib/plugins/plugin-manager'
import { LiveManager } from './server/lib/live-manager' import { LiveManager } from './server/lib/live-manager'
@ -277,6 +278,7 @@ async function startApplication () {
RemoveOldHistoryScheduler.Instance.enable() RemoveOldHistoryScheduler.Instance.enable()
RemoveOldViewsScheduler.Instance.enable() RemoveOldViewsScheduler.Instance.enable()
PluginsCheckScheduler.Instance.enable() PluginsCheckScheduler.Instance.enable()
PeerTubeVersionCheckScheduler.Instance.enable()
AutoFollowIndexInstances.Instance.enable() AutoFollowIndexInstances.Instance.enable()
// Redis initialization // Redis initialization

View File

@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
newInstanceFollower: body.newInstanceFollower, newInstanceFollower: body.newInstanceFollower,
autoInstanceFollowing: body.autoInstanceFollowing, autoInstanceFollowing: body.autoInstanceFollowing,
abuseNewMessage: body.abuseNewMessage, abuseNewMessage: body.abuseNewMessage,
abuseStateChange: body.abuseStateChange abuseStateChange: body.abuseStateChange,
newPeerTubeVersion: body.newPeerTubeVersion,
newPluginVersion: body.newPluginVersion
} }
await UserNotificationSettingModel.update(values, query) await UserNotificationSettingModel.update(values, query)

View File

@ -251,6 +251,7 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
} }
} }
type SemVersion = { major: number, minor: number, patch: number }
function parseSemVersion (s: string) { function parseSemVersion (s: string) {
const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
@ -258,7 +259,7 @@ function parseSemVersion (s: string) {
major: parseInt(parsed[1]), major: parseInt(parsed[1]),
minor: parseInt(parsed[2]), minor: parseInt(parsed[2]),
patch: parseInt(parsed[3]) patch: parseInt(parsed[3])
} } as SemVersion
} }
const randomBytesPromise = promisify1<number, Buffer>(randomBytes) const randomBytesPromise = promisify1<number, Buffer>(randomBytes)

View File

@ -37,6 +37,7 @@ function checkMissedConfig () {
'theme.default', 'theme.default',
'remote_redundancy.videos.accept_from', 'remote_redundancy.videos.accept_from',
'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', '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.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', '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', 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',

View File

@ -163,6 +163,12 @@ const CONFIG = {
CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') 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: { ADMIN: {
get EMAIL () { return config.get<string>('admin.email') } get EMAIL () { return config.get<string>('admin.email') }
}, },

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 610 const LAST_MIGRATION_VERSION = 625
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
updateVideos: 60000, // 1 minute updateVideos: 60000, // 1 minute
youtubeDLUpdate: 60000 * 60 * 24, // 1 day youtubeDLUpdate: 60000 * 60 * 24, // 1 day
checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
autoFollowIndexInstances: 60000 * 60 * 24, // 1 day autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
removeOldViews: 60000 * 60 * 24, // 1 day removeOldViews: 60000 * 60 * 24, // 1 day
removeOldHistory: 60000 * 60 * 24, // 1 day removeOldHistory: 60000 * 60 * 24, // 1 day
@ -763,6 +764,7 @@ if (isTestInstance() === true) {
SCHEDULER_INTERVALS_MS.updateVideos = 5000 SCHEDULER_INTERVALS_MS.updateVideos = 5000
SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
REPEAT_JOBS['videos-views'] = { every: 5000 } REPEAT_JOBS['videos-views'] = { every: 5000 }
REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }

View File

@ -0,0 +1,44 @@
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
}

View File

@ -0,0 +1,27 @@
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
}

View File

@ -0,0 +1,26 @@
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
}

View File

@ -12,7 +12,7 @@ import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger' import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config' import { CONFIG, isEmailEnabled } from '../initializers/config'
import { WEBSERVER } from '../initializers/constants' 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 { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
import { JobQueue } from './job-queue' import { JobQueue } from './job-queue'
@ -403,7 +403,7 @@ class Emailer {
} }
async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 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 videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
@ -417,7 +417,7 @@ class Emailer {
videoName: videoBlacklist.Video.name, videoName: videoBlacklist.Video.name,
action: { action: {
text: 'Review autoblacklist', text: 'Review autoblacklist',
url: VIDEO_AUTO_BLACKLIST_URL url: videoAutoBlacklistUrl
} }
} }
} }
@ -472,6 +472,42 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 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) { addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
const emailPayload: EmailPayload = { const emailPayload: EmailPayload = {
template: 'password-reset', template: 'password-reset',

View File

@ -0,0 +1,9 @@
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].

View File

@ -0,0 +1,9 @@
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].

View File

@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification' 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 { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist' import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer' import { Emailer } from './emailer'
@ -144,6 +144,20 @@ class Notifier {
}) })
} }
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) { private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
// List all followers that are users // List all followers that are users
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@ -667,6 +681,64 @@ class Notifier {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 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: { private async notify<T extends MUserWithNotificationSetting> (options: {
users: T[] users: T[]
notificationCreator: (user: T) => Promise<UserNotificationModelForApi> notificationCreator: (user: T) => Promise<UserNotificationModelForApi>

View File

@ -0,0 +1,55 @@
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())
}
}

View File

@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
import { chunk } from 'lodash' import { chunk } from 'lodash'
import { getLatestPluginsVersion } from '../plugins/plugin-index' import { getLatestPluginsVersion } from '../plugins/plugin-index'
import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
import { Notifier } from '../notifier'
export class PluginsCheckScheduler extends AbstractScheduler { export class PluginsCheckScheduler extends AbstractScheduler {
@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
plugin.latestVersion = result.latestVersion plugin.latestVersion = result.latestVersion
await plugin.save() 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) logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
} }
} }

View File

@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
newInstanceFollower: UserNotificationSettingValue.WEB, newInstanceFollower: UserNotificationSettingValue.WEB,
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseStateChange: 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 }) return UserNotificationSettingModel.create(values, { transaction: t })

View File

@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
@Column @Column
abuseNewMessage: UserNotificationSettingValue 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) @ForeignKey(() => UserModel)
@Column @Column
userId: number userId: number
@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
newInstanceFollower: this.newInstanceFollower, newInstanceFollower: this.newInstanceFollower,
autoInstanceFollowing: this.autoInstanceFollowing, autoInstanceFollowing: this.autoInstanceFollowing,
abuseNewMessage: this.abuseNewMessage, abuseNewMessage: this.abuseNewMessage,
abuseStateChange: this.abuseStateChange abuseStateChange: this.abuseStateChange,
newPeerTubeVersion: this.newPeerTubeVersion,
newPluginVersion: this.newPluginVersion
} }
} }
} }

View File

@ -9,7 +9,9 @@ import { VideoAbuseModel } from '../abuse/video-abuse'
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow' import { ActorFollowModel } from '../activitypub/actor-follow'
import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar' import { AvatarModel } from '../avatar/avatar'
import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
@ -96,7 +98,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
attributes: [ 'id' ], attributes: [ 'id' ],
model: VideoAbuseModel.unscoped(), model: VideoAbuseModel.unscoped(),
required: false, required: false,
include: [ buildVideoInclude(true) ] include: [ buildVideoInclude(false) ]
}, },
{ {
attributes: [ 'id' ], attributes: [ 'id' ],
@ -106,12 +108,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
{ {
attributes: [ 'id', 'originCommentId' ], attributes: [ 'id', 'originCommentId' ],
model: VideoCommentModel.unscoped(), model: VideoCommentModel.unscoped(),
required: true, required: false,
include: [ include: [
{ {
attributes: [ 'id', 'name', 'uuid' ], attributes: [ 'id', 'name', 'uuid' ],
model: VideoModel.unscoped(), model: VideoModel.unscoped(),
required: true required: false
} }
] ]
} }
@ -120,7 +122,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
{ {
model: AccountModel, model: AccountModel,
as: 'FlaggedAccount', as: 'FlaggedAccount',
required: true, required: false,
include: [ buildActorWithAvatarInclude() ] include: [ buildActorWithAvatarInclude() ]
} }
] ]
@ -140,6 +142,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
include: [ buildVideoInclude(false) ] include: [ buildVideoInclude(false) ]
}, },
{
attributes: [ 'id', 'name', 'type', 'latestVersion' ],
model: PluginModel.unscoped(),
required: false
},
{
attributes: [ 'id', 'latestPeerTubeVersion' ],
model: ApplicationModel.unscoped(),
required: false
},
{ {
attributes: [ 'id', 'state' ], attributes: [ 'id', 'state' ],
model: ActorFollowModel.unscoped(), model: ActorFollowModel.unscoped(),
@ -251,6 +265,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
[Op.ne]: null [Op.ne]: null
} }
} }
},
{
fields: [ 'pluginId' ],
where: {
pluginId: {
[Op.ne]: null
}
}
},
{
fields: [ 'applicationId' ],
where: {
applicationId: {
[Op.ne]: null
}
}
} }
] as (ModelIndexesOptions & { where?: WhereOptions })[] ] as (ModelIndexesOptions & { where?: WhereOptions })[]
}) })
@ -370,6 +400,30 @@ export class UserNotificationModel extends Model {
}) })
ActorFollow: ActorFollowModel 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) { static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
const where = { userId } const where = { userId }
@ -524,6 +578,18 @@ export class UserNotificationModel extends Model {
} }
: undefined : 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 { return {
id: this.id, id: this.id,
type: this.type, type: this.type,
@ -535,6 +601,8 @@ export class UserNotificationModel extends Model {
videoBlacklist, videoBlacklist,
account, account,
actorFollow, actorFollow,
plugin,
peertube,
createdAt: this.createdAt.toISOString(), createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString() updatedAt: this.updatedAt.toISOString()
} }
@ -553,17 +621,19 @@ export class UserNotificationModel extends Model {
? { ? {
threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
video: { video: abuse.VideoCommentAbuse.VideoComment.Video
id: abuse.VideoCommentAbuse.VideoComment.Video.id, ? {
name: abuse.VideoCommentAbuse.VideoComment.Video.name, id: abuse.VideoCommentAbuse.VideoComment.Video.id,
uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid name: abuse.VideoCommentAbuse.VideoComment.Video.name,
} uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
}
: undefined
} }
: undefined : undefined
const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : 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 { return {
id: abuse.id, id: abuse.id,

View File

@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
@Column @Column
migrationVersion: number migrationVersion: number
@AllowNull(true)
@Column
latestPeerTubeVersion: string
@HasOne(() => AccountModel, { @HasOne(() => AccountModel, {
foreignKey: { foreignKey: {
allowNull: true allowNull: true

View File

@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () {
newInstanceFollower: UserNotificationSettingValue.WEB, newInstanceFollower: UserNotificationSettingValue.WEB,
autoInstanceFollowing: UserNotificationSettingValue.WEB, autoInstanceFollowing: UserNotificationSettingValue.WEB,
abuseNewMessage: 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 () { it('Should fail with missing fields', async function () {

View File

@ -0,0 +1,165 @@
/* 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 ])
})
})

View File

@ -1,3 +1,4 @@
import './admin-notifications'
import './comments-notifications' import './comments-notifications'
import './moderation-notifications' import './moderation-notifications'
import './notifications-api' import './notifications-api'

View File

@ -0,0 +1,5 @@
import { ApplicationModel } from '@server/models/application/application'
// ############################################################################
export type MApplication = Omit<ApplicationModel, 'Account'>

View File

@ -0,0 +1 @@
export * from './application'

View File

@ -1,4 +1,5 @@
export * from './account' export * from './account'
export * from './application'
export * from './moderation' export * from './moderation'
export * from './oauth' export * from './oauth'
export * from './server' export * from './server'

View File

@ -1,5 +1,7 @@
import { VideoAbuseModel } from '@server/models/abuse/video-abuse' import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-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 { PickWith, PickWithOpt } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse' import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
@ -85,13 +87,19 @@ export module UserNotificationIncludes {
Pick<ActorFollowModel, 'id' | 'state'> & Pick<ActorFollowModel, 'id' | 'state'> &
PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
export type PluginInclude =
Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
export type ApplicationInclude =
Pick<ApplicationModel, 'latestPeerTubeVersion'>
} }
// ############################################################################ // ############################################################################
export type MUserNotification = export type MUserNotification =
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
'VideoImport' | 'Account' | 'ActorFollow'> 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
// ############################################################################ // ############################################################################
@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
Use<'Application', UserNotificationIncludes.ApplicationInclude> &
Use<'Account', UserNotificationIncludes.AccountIncludeActor> Use<'Account', UserNotificationIncludes.AccountIncludeActor>

View File

@ -1,7 +1,7 @@
export * from './bulk/bulk' export * from './bulk/bulk'
export * from './cli/cli' export * from './cli/cli'
export * from './feeds/feeds' 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/miscs'
export * from './miscs/sql' export * from './miscs/sql'
export * from './miscs/stubs' export * from './miscs/stubs'

View File

@ -106,12 +106,20 @@ async function closeAllSequelize (servers: ServerInfo[]) {
} }
} }
function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) { function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
const seq = getSequelize(internalServerNumber) const seq = getSequelize(internalServerNumber)
const options = { type: QueryTypes.UPDATE } 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) { function setActorFollowScores (internalServerNumber: number, newScore: number) {
@ -128,6 +136,7 @@ export {
setActorField, setActorField,
countVideoViewsOf, countVideoViewsOf,
setPluginVersion, setPluginVersion,
setPluginLatestVersion,
selectQuery, selectQuery,
deleteAll, deleteAll,
updateQuery, updateQuery,

View File

@ -0,0 +1,31 @@
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
}
}

View File

@ -2,7 +2,8 @@
import { expect } from 'chai' import { expect } from 'chai'
import { inspect } from 'util' 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 { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
import { MockSmtpServer } from '../miscs/email' import { MockSmtpServer } from '../miscs/email'
import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
import { getUserNotificationSocket } from '../socket/socket-io' import { getUserNotificationSocket } from '../socket/socket-io'
import { setAccessTokensToServers, userLogin } from './login' import { setAccessTokensToServers, userLogin } from './login'
import { createUser, getMyUserInformation } from './users' import { createUser, getMyUserInformation } from './users'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
function updateMyNotificationSettings ( function updateMyNotificationSettings (
url: string, url: string,
@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo (
await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') 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 { return {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
@ -644,11 +696,13 @@ function getAllNotificationsSettings () {
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
} as UserNotificationSetting 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 userNotifications: UserNotification[] = []
const adminNotifications: UserNotification[] = [] const adminNotifications: UserNotification[] = []
const adminNotificationsServer2: UserNotification[] = [] const adminNotificationsServer2: UserNotification[] = []
@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) {
limit: 20 limit: 20
} }
} }
const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
await setAccessTokensToServers(servers) await setAccessTokensToServers(servers)
@ -749,5 +803,7 @@ export {
checkNewInstanceFollower, checkNewInstanceFollower,
prepareNotificationsTest, prepareNotificationsTest,
checkNewCommentAbuseForModerators, checkNewCommentAbuseForModerators,
checkNewAccountAbuseForModerators checkNewAccountAbuseForModerators,
checkNewPeerTubeVersion,
checkNewPluginVersion
} }

View File

@ -7,6 +7,7 @@ export * from './redundancy'
export * from './users' export * from './users'
export * from './videos' export * from './videos'
export * from './feeds' export * from './feeds'
export * from './joinpeertube'
export * from './overviews' export * from './overviews'
export * from './plugins' export * from './plugins'
export * from './search' export * from './search'

View File

@ -0,0 +1 @@
export * from './versions.model'

View File

@ -0,0 +1,5 @@
export interface JoinPeerTubeVersions {
peertube: {
latestVersion: string
}
}

View File

@ -24,4 +24,7 @@ export interface UserNotificationSetting {
abuseStateChange: UserNotificationSettingValue abuseStateChange: UserNotificationSettingValue
abuseNewMessage: UserNotificationSettingValue abuseNewMessage: UserNotificationSettingValue
newPeerTubeVersion: UserNotificationSettingValue
newPluginVersion: UserNotificationSettingValue
} }

View File

@ -1,5 +1,6 @@
import { FollowState } from '../actors' import { FollowState } from '../actors'
import { AbuseState } from '../moderation' import { AbuseState } from '../moderation'
import { PluginType } from '../plugins'
export const enum UserNotificationType { export const enum UserNotificationType {
NEW_VIDEO_FROM_SUBSCRIPTION = 1, NEW_VIDEO_FROM_SUBSCRIPTION = 1,
@ -26,7 +27,10 @@ export const enum UserNotificationType {
ABUSE_STATE_CHANGE = 15, ABUSE_STATE_CHANGE = 15,
ABUSE_NEW_MESSAGE = 16 ABUSE_NEW_MESSAGE = 16,
NEW_PLUGIN_VERSION = 17,
NEW_PEERTUBE_VERSION = 18
} }
export interface VideoInfo { export interface VideoInfo {
@ -108,6 +112,16 @@ export interface UserNotification {
} }
} }
plugin?: {
name: string
type: PluginType
latestVersion: string
}
peertube?: {
latestVersion: string
}
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }