From 32a18cbf33a7cdbbe3d4885d32e4b67e19cdc1cf Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 11 Mar 2021 16:54:52 +0100 Subject: [PATCH] Add new plugin/peertube version notifs --- ...ount-notification-preferences.component.ts | 8 +- .../users/user-notification.model.ts | 33 ++++ .../users/user-notifications.component.html | 16 ++ .../users/user-notifications.component.ts | 6 +- config/default.yaml | 7 + config/production.yaml.example | 6 + config/test.yaml | 4 + server.ts | 2 + .../controllers/api/users/my-notifications.ts | 4 +- server/helpers/core-utils.ts | 3 +- server/initializers/checker-before-init.ts | 1 + server/initializers/config.ts | 6 + server/initializers/constants.ts | 4 +- ...iews-index.ts => 0610-views-index copy.ts} | 0 ...5-latest-versions-notification-settings.ts | 44 +++++ .../0620-latest-versions-application.ts | 27 +++ .../0625-latest-versions-notification.ts | 26 +++ server/lib/emailer.ts | 42 ++++- .../lib/emails/peertube-version-new/html.pug | 9 + server/lib/emails/plugin-version-new/html.pug | 9 + server/lib/notifier.ts | 74 +++++++- .../peertube-version-check-scheduler.ts | 55 ++++++ .../lib/schedulers/plugins-check-scheduler.ts | 6 + server/lib/user.ts | 4 +- .../account/user-notification-setting.ts | 22 ++- server/models/account/user-notification.ts | 90 ++++++++-- server/models/application/application.ts | 4 + .../api/check-params/user-notifications.ts | 4 +- .../api/notifications/admin-notifications.ts | 165 ++++++++++++++++++ server/tests/api/notifications/index.ts | 1 + .../types/models/application/application.ts | 5 + server/types/models/application/index.ts | 1 + server/types/models/index.ts | 1 + server/types/models/user/user-notification.ts | 12 +- shared/extra-utils/index.ts | 2 +- shared/extra-utils/miscs/sql.ts | 13 +- .../mock-servers/joinpeertube-versions.ts | 31 ++++ .../mock-instances-index.ts | 0 .../extra-utils/users/user-notifications.ts | 72 +++++++- shared/models/index.ts | 1 + shared/models/joinpeertube/index.ts | 1 + shared/models/joinpeertube/versions.model.ts | 5 + .../users/user-notification-setting.model.ts | 3 + .../models/users/user-notification.model.ts | 16 +- 44 files changed, 808 insertions(+), 37 deletions(-) rename server/initializers/migrations/{0610-views-index.ts => 0610-views-index copy.ts} (100%) create mode 100644 server/initializers/migrations/0615-latest-versions-notification-settings.ts create mode 100644 server/initializers/migrations/0620-latest-versions-application.ts create mode 100644 server/initializers/migrations/0625-latest-versions-notification.ts create mode 100644 server/lib/emails/peertube-version-new/html.pug create mode 100644 server/lib/emails/plugin-version-new/html.pug create mode 100644 server/lib/schedulers/peertube-version-check-scheduler.ts create mode 100644 server/tests/api/notifications/admin-notifications.ts create mode 100644 server/types/models/application/application.ts create mode 100644 server/types/models/application/index.ts create mode 100644 shared/extra-utils/mock-servers/joinpeertube-versions.ts rename shared/extra-utils/{instances-index => mock-servers}/mock-instances-index.ts (100%) create mode 100644 shared/models/joinpeertube/index.ts create mode 100644 shared/models/joinpeertube/versions.model.ts diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index ad7497f45..c7e173038 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts @@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { 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)[] @@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { 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 } } diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index 1211995fd..88a4811da 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts @@ -6,6 +6,7 @@ import { AbuseState, ActorInfo, FollowState, + PluginType, UserNotification as UserNotificationServer, UserNotificationType, UserRight, @@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer { } } + 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 @@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer { 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 @@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer { 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 diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index 5e0e2f8e8..325f0eaae 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html @@ -191,6 +191,22 @@ + + + +
+ A new version of the plugin/theme {{ notification.plugin.name }} is available: {{ notification.plugin.latestVersion }} +
+
+ + + + +
+ A new version of PeerTube is available: {{ notification.peertube.latestVersion }} +
+
+ diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts index 2f6ed061a..d7c722355 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.ts +++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts @@ -45,7 +45,7 @@ export class UserNotificationsComponent implements OnInit { } loadNotifications (reset?: boolean) { - this.userNotificationService.listMyNotifications({ + const options = { pagination: this.componentPagination, ignoreLoadingBar: this.ignoreLoadingBar, 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). order: this.sortField === 'createdAt' ? -1 : 1 } - }) + } + + this.userNotificationService.listMyNotifications(options) .subscribe( result => { this.notifications = reset ? result.data : this.notifications.concat(result.data) diff --git a/config/default.yaml b/config/default.yaml index a09d20b9d..d400e1067 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -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 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 diff --git a/config/production.yaml.example b/config/production.yaml.example index 31c0e6b96..895931e7c 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -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 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' ############################################################################### # diff --git a/config/test.yaml b/config/test.yaml index 33c11afc3..4f0a7e5d9 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -38,6 +38,10 @@ log: contact_form: enabled: true +peertube: + check_latest_version: + enabled: false + redundancy: videos: check_interval: '1 minute' diff --git a/server.ts b/server.ts index a8bd25088..f44202c9a 100644 --- a/server.ts +++ b/server.ts @@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' 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' @@ -277,6 +278,7 @@ async function startApplication () { RemoveOldHistoryScheduler.Instance.enable() RemoveOldViewsScheduler.Instance.enable() PluginsCheckScheduler.Instance.enable() + PeerTubeVersionCheckScheduler.Instance.enable() AutoFollowIndexInstances.Instance.enable() // Redis initialization diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 5f5e4c5e6..0a9101a46 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts @@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re 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) diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index ceb6a341d..0bd84ffaa 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -251,6 +251,7 @@ function promisify2 (func: (arg1: T, arg2: U, cb: (err: any, result: A) } } +type SemVersion = { major: number, minor: number, patch: number } function parseSemVersion (s: string) { const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) @@ -258,7 +259,7 @@ function parseSemVersion (s: string) { major: parseInt(parsed[1]), minor: parseInt(parsed[2]), patch: parseInt(parsed[3]) - } + } as SemVersion } const randomBytesPromise = promisify1(randomBytes) diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 65a019ca6..e92cc4d2c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts @@ -37,6 +37,7 @@ function checkMissedConfig () { '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', diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c16b63c33..48e7f7397 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -163,6 +163,12 @@ const CONFIG = { CLEANUP_REMOTE_INTERACTIONS: config.get('federation.videos.cleanup_remote_interactions') } }, + PEERTUBE: { + CHECK_LATEST_VERSION: { + ENABLED: config.get('peertube.check_latest_version.enabled'), + URL: config.get('peertube.check_latest_version.url') + } + }, ADMIN: { get EMAIL () { return config.get('admin.email') } }, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index ea98e8a38..b37aeb622 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 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 @@ -763,6 +764,7 @@ if (isTestInstance() === true) { 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 } diff --git a/server/initializers/migrations/0610-views-index.ts b/server/initializers/migrations/0610-views-index copy.ts similarity index 100% rename from server/initializers/migrations/0610-views-index.ts rename to server/initializers/migrations/0610-views-index copy.ts diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts new file mode 100644 index 000000000..86bf56009 --- /dev/null +++ b/server/initializers/migrations/0615-latest-versions-notification-settings.ts @@ -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 { + { + 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 +} diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts new file mode 100644 index 000000000..a689b18fc --- /dev/null +++ b/server/initializers/migrations/0620-latest-versions-application.ts @@ -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 { + + { + 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 +} diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts new file mode 100644 index 000000000..77f395ce4 --- /dev/null +++ b/server/initializers/migrations/0625-latest-versions-notification.ts @@ -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 { + + { + 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 +} diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 969eae77b..187d4e86d 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -12,7 +12,7 @@ import { isTestInstance, root } from '../helpers/core-utils' 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' @@ -403,7 +403,7 @@ class Emailer { } 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() @@ -417,7 +417,7 @@ class Emailer { videoName: videoBlacklist.Video.name, action: { text: 'Review autoblacklist', - url: VIDEO_AUTO_BLACKLIST_URL + url: videoAutoBlacklistUrl } } } @@ -472,6 +472,42 @@ class Emailer { 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', diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug new file mode 100644 index 000000000..2f4d9399d --- /dev/null +++ b/server/lib/emails/peertube-version-new/html.pug @@ -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]. diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug new file mode 100644 index 000000000..86d3d87e8 --- /dev/null +++ b/server/lib/emails/plugin-version-new/html.pug @@ -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]. diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 740c274d7..da7f7cc05 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config' 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' @@ -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) { // List all followers that are users const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) @@ -667,6 +681,64 @@ class Notifier { 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({ + 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({ + 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 (options: { users: T[] notificationCreator: (user: T) => Promise diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts new file mode 100644 index 000000000..c8960465c --- /dev/null +++ b/server/lib/schedulers/peertube-version-check-scheduler.ts @@ -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(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()) + } +} diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts index 014993e94..9a1ae3ec5 100644 --- a/server/lib/schedulers/plugins-check-scheduler.ts +++ b/server/lib/schedulers/plugins-check-scheduler.ts @@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin' 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 { @@ -53,6 +54,11 @@ 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) } } diff --git a/server/lib/user.ts b/server/lib/user.ts index e1892f22c..9b0a0a2f1 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | 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 }) diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index ebab8b6d2..de1501299 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts @@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model { @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 @@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model { newInstanceFollower: this.newInstanceFollower, autoInstanceFollowing: this.autoInstanceFollowing, abuseNewMessage: this.abuseNewMessage, - abuseStateChange: this.abuseStateChange + abuseStateChange: this.abuseStateChange, + newPeerTubeVersion: this.newPeerTubeVersion, + newPluginVersion: this.newPluginVersion } } } diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index add129644..25c523203 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -9,7 +9,9 @@ import { VideoAbuseModel } from '../abuse/video-abuse' 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' @@ -96,7 +98,7 @@ function buildAccountInclude (required: boolean, withActor = false) { attributes: [ 'id' ], model: VideoAbuseModel.unscoped(), required: false, - include: [ buildVideoInclude(true) ] + include: [ buildVideoInclude(false) ] }, { attributes: [ 'id' ], @@ -106,12 +108,12 @@ function buildAccountInclude (required: boolean, withActor = false) { { attributes: [ 'id', 'originCommentId' ], model: VideoCommentModel.unscoped(), - required: true, + required: false, include: [ { attributes: [ 'id', 'name', 'uuid' ], model: VideoModel.unscoped(), - required: true + required: false } ] } @@ -120,7 +122,7 @@ function buildAccountInclude (required: boolean, withActor = false) { { model: AccountModel, as: 'FlaggedAccount', - required: true, + required: false, include: [ buildActorWithAvatarInclude() ] } ] @@ -140,6 +142,18 @@ function buildAccountInclude (required: boolean, withActor = 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' ], model: ActorFollowModel.unscoped(), @@ -251,6 +265,22 @@ function buildAccountInclude (required: boolean, withActor = false) { [Op.ne]: null } } + }, + { + fields: [ 'pluginId' ], + where: { + pluginId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'applicationId' ], + where: { + applicationId: { + [Op.ne]: null + } + } } ] as (ModelIndexesOptions & { where?: WhereOptions })[] }) @@ -370,6 +400,30 @@ export class UserNotificationModel extends Model { }) 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 } @@ -524,6 +578,18 @@ export class UserNotificationModel extends Model { } : 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, @@ -535,6 +601,8 @@ export class UserNotificationModel extends Model { videoBlacklist, account, actorFollow, + plugin, + peertube, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString() } @@ -553,17 +621,19 @@ export class UserNotificationModel extends Model { ? { 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, diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 909569de1..21f8b1cbc 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts @@ -32,6 +32,10 @@ export class ApplicationModel extends Model { @Column migrationVersion: number + @AllowNull(true) + @Column + latestPeerTubeVersion: string + @HasOne(() => AccountModel, { foreignKey: { allowNull: true diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts index 05a78b0ad..26d4423f9 100644 --- a/server/tests/api/check-params/user-notifications.ts +++ b/server/tests/api/check-params/user-notifications.ts @@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () { 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 () { diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts new file mode 100644 index 000000000..e07327d74 --- /dev/null +++ b/server/tests/api/notifications/admin-notifications.ts @@ -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 ]) + }) +}) diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts index bd07a339e..8caa30a3d 100644 --- a/server/tests/api/notifications/index.ts +++ b/server/tests/api/notifications/index.ts @@ -1,3 +1,4 @@ +import './admin-notifications' import './comments-notifications' import './moderation-notifications' import './notifications-api' diff --git a/server/types/models/application/application.ts b/server/types/models/application/application.ts new file mode 100644 index 000000000..9afb9ad70 --- /dev/null +++ b/server/types/models/application/application.ts @@ -0,0 +1,5 @@ +import { ApplicationModel } from '@server/models/application/application' + +// ############################################################################ + +export type MApplication = Omit diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts new file mode 100644 index 000000000..26e4b031f --- /dev/null +++ b/server/types/models/application/index.ts @@ -0,0 +1 @@ +export * from './application' diff --git a/server/types/models/index.ts b/server/types/models/index.ts index affa17425..b4fdb1ff3 100644 --- a/server/types/models/index.ts +++ b/server/types/models/index.ts @@ -1,4 +1,5 @@ export * from './account' +export * from './application' export * from './moderation' export * from './oauth' export * from './server' diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 58764a748..6988086f1 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts @@ -1,5 +1,7 @@ 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' @@ -85,13 +87,19 @@ export module UserNotificationIncludes { Pick & PickWith & PickWith + + export type PluginInclude = + Pick + + export type ApplicationInclude = + Pick } // ############################################################################ export type MUserNotification = Omit + 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> // ############################################################################ @@ -103,4 +111,6 @@ export type UserNotificationModelForApi = Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & + Use<'Plugin', UserNotificationIncludes.PluginInclude> & + Use<'Application', UserNotificationIncludes.ApplicationInclude> & Use<'Account', UserNotificationIncludes.AccountIncludeActor> diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index 5c95a1b3e..898a92d43 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts @@ -1,7 +1,7 @@ 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' diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts index 740f0c2d6..345e5bc16 100644 --- a/shared/extra-utils/miscs/sql.ts +++ b/shared/extra-utils/miscs/sql.ts @@ -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 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) { @@ -128,6 +136,7 @@ export { setActorField, countVideoViewsOf, setPluginVersion, + setPluginLatestVersion, selectQuery, deleteAll, updateQuery, diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/joinpeertube-versions.ts new file mode 100644 index 000000000..d7d5b2c49 --- /dev/null +++ b/shared/extra-utils/mock-servers/joinpeertube-versions.ts @@ -0,0 +1,31 @@ +import * as express from 'express' + +export class MockJoinPeerTubeVersions { + private latestVersion: string + + initialize () { + return new Promise(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 + } +} diff --git a/shared/extra-utils/instances-index/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts similarity index 100% rename from shared/extra-utils/instances-index/mock-instances-index.ts rename to shared/extra-utils/mock-servers/mock-instances-index.ts diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts index 467a3d959..249e82925 100644 --- a/shared/extra-utils/users/user-notifications.ts +++ b/shared/extra-utils/users/user-notifications.ts @@ -2,7 +2,8 @@ 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' @@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers' 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, @@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo ( 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, @@ -644,11 +696,13 @@ function getAllNotificationsSettings () { 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[] = [] @@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) { limit: 20 } } - const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) + const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) await setAccessTokensToServers(servers) @@ -749,5 +803,7 @@ export { checkNewInstanceFollower, prepareNotificationsTest, checkNewCommentAbuseForModerators, - checkNewAccountAbuseForModerators + checkNewAccountAbuseForModerators, + checkNewPeerTubeVersion, + checkNewPluginVersion } diff --git a/shared/models/index.ts b/shared/models/index.ts index 2214f7ca3..f105303f4 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -7,6 +7,7 @@ export * from './redundancy' export * from './users' export * from './videos' export * from './feeds' +export * from './joinpeertube' export * from './overviews' export * from './plugins' export * from './search' diff --git a/shared/models/joinpeertube/index.ts b/shared/models/joinpeertube/index.ts new file mode 100644 index 000000000..9681c35ad --- /dev/null +++ b/shared/models/joinpeertube/index.ts @@ -0,0 +1 @@ +export * from './versions.model' diff --git a/shared/models/joinpeertube/versions.model.ts b/shared/models/joinpeertube/versions.model.ts new file mode 100644 index 000000000..60a769150 --- /dev/null +++ b/shared/models/joinpeertube/versions.model.ts @@ -0,0 +1,5 @@ +export interface JoinPeerTubeVersions { + peertube: { + latestVersion: string + } +} diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts index 473148062..977e6b985 100644 --- a/shared/models/users/user-notification-setting.model.ts +++ b/shared/models/users/user-notification-setting.model.ts @@ -24,4 +24,7 @@ export interface UserNotificationSetting { abuseStateChange: UserNotificationSettingValue abuseNewMessage: UserNotificationSettingValue + + newPeerTubeVersion: UserNotificationSettingValue + newPluginVersion: UserNotificationSettingValue } diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index b04619685..8b33e3fbd 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts @@ -1,5 +1,6 @@ import { FollowState } from '../actors' import { AbuseState } from '../moderation' +import { PluginType } from '../plugins' export const enum UserNotificationType { NEW_VIDEO_FROM_SUBSCRIPTION = 1, @@ -26,7 +27,10 @@ export const enum UserNotificationType { ABUSE_STATE_CHANGE = 15, - ABUSE_NEW_MESSAGE = 16 + ABUSE_NEW_MESSAGE = 16, + + NEW_PLUGIN_VERSION = 17, + NEW_PEERTUBE_VERSION = 18 } export interface VideoInfo { @@ -108,6 +112,16 @@ export interface UserNotification { } } + plugin?: { + name: string + type: PluginType + latestVersion: string + } + + peertube?: { + latestVersion: string + } + createdAt: string updatedAt: string }