]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add new plugin/peertube version notifs
authorChocobozzz <me@florianbigard.com>
Thu, 11 Mar 2021 15:54:52 +0000 (16:54 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 24 Mar 2021 17:18:41 +0000 (18:18 +0100)
44 files changed:
client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
client/src/app/shared/shared-main/users/user-notification.model.ts
client/src/app/shared/shared-main/users/user-notifications.component.html
client/src/app/shared/shared-main/users/user-notifications.component.ts
config/default.yaml
config/production.yaml.example
config/test.yaml
server.ts
server/controllers/api/users/my-notifications.ts
server/helpers/core-utils.ts
server/initializers/checker-before-init.ts
server/initializers/config.ts
server/initializers/constants.ts
server/initializers/migrations/0610-views-index copy.ts [moved from server/initializers/migrations/0610-views-index.ts with 100% similarity]
server/initializers/migrations/0615-latest-versions-notification-settings.ts [new file with mode: 0644]
server/initializers/migrations/0620-latest-versions-application.ts [new file with mode: 0644]
server/initializers/migrations/0625-latest-versions-notification.ts [new file with mode: 0644]
server/lib/emailer.ts
server/lib/emails/peertube-version-new/html.pug [new file with mode: 0644]
server/lib/emails/plugin-version-new/html.pug [new file with mode: 0644]
server/lib/notifier.ts
server/lib/schedulers/peertube-version-check-scheduler.ts [new file with mode: 0644]
server/lib/schedulers/plugins-check-scheduler.ts
server/lib/user.ts
server/models/account/user-notification-setting.ts
server/models/account/user-notification.ts
server/models/application/application.ts
server/tests/api/check-params/user-notifications.ts
server/tests/api/notifications/admin-notifications.ts [new file with mode: 0644]
server/tests/api/notifications/index.ts
server/types/models/application/application.ts [new file with mode: 0644]
server/types/models/application/index.ts [new file with mode: 0644]
server/types/models/index.ts
server/types/models/user/user-notification.ts
shared/extra-utils/index.ts
shared/extra-utils/miscs/sql.ts
shared/extra-utils/mock-servers/joinpeertube-versions.ts [new file with mode: 0644]
shared/extra-utils/mock-servers/mock-instances-index.ts [moved from shared/extra-utils/instances-index/mock-instances-index.ts with 100% similarity]
shared/extra-utils/users/user-notifications.ts
shared/models/index.ts
shared/models/joinpeertube/index.ts [new file with mode: 0644]
shared/models/joinpeertube/versions.model.ts [new file with mode: 0644]
shared/models/users/user-notification-setting.model.ts
shared/models/users/user-notification.model.ts

index ad7497f45f644fe3ffdce6ae94600149ceddf436..c7e17303812825e4086fca013ca24baec3dff476 100644 (file)
@@ -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
     }
   }
 
index 1211995fd3a89e98ce3e02b43d9e9362f55d4df9..88a4811da0681ec350e267bd1a789f3dcc3ccd1e 100644 (file)
@@ -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
index 5e0e2f8e896fe386fb21b086090a5a0b9933b0e8..325f0eaae1bdcce6aef9eb5f00b3ea64681e6909 100644 (file)
         </div>
       </ng-container>
 
+      <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
+        <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+        <div class="message" i18n>
+          <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
+        </div>
+      </ng-container>
+
+      <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
+        <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+        <div class="message" i18n>
+            <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
+        </div>
+      </ng-container>
+
       <ng-container *ngSwitchDefault>
         <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
 
index 2f6ed061a20eb104dd821965b6a504d8647ad23e..d7c72235528ddef83bea36ac3ac9b82de4b096d3 100644 (file)
@@ -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)
index a09d20b9d6d9e61a3188b2057e79db2659893877..d400e10670b5f2448fe0255694d400a31ec1bb3b 100644 (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
     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
index 31c0e6b9618f4262d4d29d912eb2740f05b57b89..895931e7cb62ef4271a975a651ffd62dd61e04e7 100644 (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
     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'
 
 ###############################################################################
 #
index 33c11afc30b78099e0921eea63e966300df80790..4f0a7e5d9cd8028a63c7c5f2225e04dae1a40a78 100644 (file)
@@ -38,6 +38,10 @@ log:
 contact_form:
   enabled: true
 
+peertube:
+  check_latest_version:
+    enabled: false
+
 redundancy:
   videos:
     check_interval: '1 minute'
index a8bd250881ec35014e6ff54194350fe5dbf07aaf..f44202c9af982bb294dd5563049c40046aa50809 100644 (file)
--- 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
index 5f5e4c5e6be8e1e91e075a2dfd99ba8338ddc539..0a9101a46df3340a995a7b8758f7bb3df61c4f78 100644 (file)
@@ -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)
index ceb6a341dd8ec719c31630ca5b1fc0bdde0f8a89..0bd84ffaa88f2f6c59578210e7b654186673a673 100644 (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) {
   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<number, Buffer>(randomBytes)
index 65a019ca686cb8b9e5721cc938e40e9d940855be..e92cc4d2cfd30e00ba01bec7686dcf23140fcf05 100644 (file)
@@ -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',
index c16b63c33903ec1952a1aaa733a681dedc3d200b..48e7f7397be50e4d21f7e73a939b96e5b26bde9c 100644 (file)
@@ -163,6 +163,12 @@ const CONFIG = {
       CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
     }
   },
+  PEERTUBE: {
+    CHECK_LATEST_VERSION: {
+      ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
+      URL: config.get<string>('peertube.check_latest_version.url')
+    }
+  },
   ADMIN: {
     get EMAIL () { return config.get<string>('admin.email') }
   },
index ea98e8a3825923a8b730de6060c7544fe67223fb..b37aeb6223c8e3c09bbf8e6a041ee556735e6965 100644 (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
   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/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
new file mode 100644 (file)
index 0000000..86bf560
--- /dev/null
@@ -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
+}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
new file mode 100644 (file)
index 0000000..a689b18
--- /dev/null
@@ -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
+}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
new file mode 100644 (file)
index 0000000..77f395c
--- /dev/null
@@ -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
+}
index 969eae77b81ee948a10ea23043f4f7d58773d0b4..187d4e86d5fae8467329b645be9476567a620d9b 100644 (file)
@@ -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 (file)
index 0000000..2f4d939
--- /dev/null
@@ -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 (file)
index 0000000..86d3d87
--- /dev/null
@@ -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].
index 740c274d72116cd1b4edf0c310bffffbb03c8be6..da7f7cc0506dc094814fd4e378a20f238415717a 100644 (file)
@@ -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<UserNotificationModelForApi>({
+        type: UserNotificationType.NEW_PEERTUBE_VERSION,
+        userId: user.id,
+        applicationId: application.id
+      })
+      notification.Application = application
+
+      return notification
+    }
+
+    function emailSender (emails: string[]) {
+      return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
+    }
+
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+  }
+
+  private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
+    // Use the debug right to know who is an administrator
+    const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
+    if (admins.length === 0) return
+
+    logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
+
+    function settingGetter (user: MUserWithNotificationSetting) {
+      return user.NotificationSetting.newPluginVersion
+    }
+
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+        type: UserNotificationType.NEW_PLUGIN_VERSION,
+        userId: user.id,
+        pluginId: plugin.id
+      })
+      notification.Plugin = plugin
+
+      return notification
+    }
+
+    function emailSender (emails: string[]) {
+      return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
+    }
+
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+  }
+
   private async notify<T extends MUserWithNotificationSetting> (options: {
     users: T[]
     notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts
new file mode 100644 (file)
index 0000000..c896046
--- /dev/null
@@ -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())
+  }
+}
index 014993e94cee04568588ca21445911f2db4c966b..9a1ae3ec50861dc9e26695dd6ca6680e7f9dc0b8 100644 (file)
@@ -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)
           }
         }
index e1892f22c89646b08a68f32ddb6b52985d7f1894..9b0a0a2f111cd46b64a834ac6bf8ba61c28f2e1b 100644 (file)
@@ -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 })
index ebab8b6d2c58c2fcae57c340934796c8d7967309..de150129922ca57e0dced2524d2a9c222030d331 100644 (file)
@@ -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
     }
   }
 }
index add129644d577292b98dc72ce515e093f04a7073..25c5232032132ea977217697b4553456b0c49d3a 100644 (file)
@@ -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,
index 909569de1117e157f5f97ad10cc81635c0f71596..21f8b1cbc70acc65c96e24c829ee65fdca1bffad 100644 (file)
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
   @Column
   migrationVersion: number
 
+  @AllowNull(true)
+  @Column
+  latestPeerTubeVersion: string
+
   @HasOne(() => AccountModel, {
     foreignKey: {
       allowNull: true
index 05a78b0adc320d49940fe44788e7455ac42ce5fb..26d4423f9313d3a545e4a94f9b916a5d69212ca5 100644 (file)
@@ -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 (file)
index 0000000..e07327d
--- /dev/null
@@ -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 ])
+  })
+})
index bd07a339e0368b6661fd9078f0a38e903fe4dd9e..8caa30a3d2c9304603c46aa6fc0c937e6f4a12c0 100644 (file)
@@ -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 (file)
index 0000000..9afb9ad
--- /dev/null
@@ -0,0 +1,5 @@
+import { ApplicationModel } from '@server/models/application/application'
+
+// ############################################################################
+
+export type MApplication = Omit<ApplicationModel, 'Account'>
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts
new file mode 100644 (file)
index 0000000..26e4b03
--- /dev/null
@@ -0,0 +1 @@
+export * from './application'
index affa17425d0afe4d182003b18fb8db3fd90da0da..b4fdb1ff339d5a332980a2803e200ed944ce3b0c 100644 (file)
@@ -1,4 +1,5 @@
 export * from './account'
+export * from './application'
 export * from './moderation'
 export * from './oauth'
 export * from './server'
index 58764a74842a6207563d380c094ac5f972dc3813..6988086f13d53c96eb99f2f6d02f5c5fd7270d61 100644 (file)
@@ -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<ActorFollowModel, 'id' | 'state'> &
     PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
     PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
+
+  export type PluginInclude =
+    Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
+
+  export type ApplicationInclude =
+    Pick<ApplicationModel, 'latestPeerTubeVersion'>
 }
 
 // ############################################################################
 
 export type MUserNotification =
   Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
-  'VideoImport' | 'Account' | 'ActorFollow'>
+  'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
 
 // ############################################################################
 
@@ -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>
index 5c95a1b3e446e22974229ca256e583588c840dad..898a92d43198a95c8ca9f39b75cc14bddb6d47fa 100644 (file)
@@ -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'
index 740f0c2d6d817c2dbe78c9d4f1bdd8adeab5596b..345e5bc1647cf9f1ee256470433f3014a62a7b3b 100644 (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 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 (file)
index 0000000..d7d5b2c
--- /dev/null
@@ -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
+  }
+}
index 467a3d95910b034d05fe47ae7ee7177001e88638..249e82925c0c7a241c17af2c99fa79e697f67dda 100644 (file)
@@ -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
 }
index 2214f7ca3c5c669849e3a282e82c9c2e80600f02..f105303f4617765854b7838aa3266d00e0df6f9d 100644 (file)
@@ -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 (file)
index 0000000..9681c35
--- /dev/null
@@ -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 (file)
index 0000000..60a7691
--- /dev/null
@@ -0,0 +1,5 @@
+export interface JoinPeerTubeVersions {
+  peertube: {
+    latestVersion: string
+  }
+}
index 473148062b36e1e0b9deb537dc4737c803c1c002..977e6b9858e18f34cc79af692657a16dbd2c6563 100644 (file)
@@ -24,4 +24,7 @@ export interface UserNotificationSetting {
 
   abuseStateChange: UserNotificationSettingValue
   abuseNewMessage: UserNotificationSettingValue
+
+  newPeerTubeVersion: UserNotificationSettingValue
+  newPluginVersion: UserNotificationSettingValue
 }
index b04619685cb5711e1a6092199b8403444d23043a..8b33e3fbdab30d66a90719ba315a0ea06ff68bc5 100644 (file)
@@ -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
 }