aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts8
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts33
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html16
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts6
-rw-r--r--config/default.yaml7
-rw-r--r--config/production.yaml.example6
-rw-r--r--config/test.yaml4
-rw-r--r--server.ts2
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/helpers/core-utils.ts3
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/config.ts6
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/initializers/migrations/0610-views-index copy.ts (renamed from server/initializers/migrations/0610-views-index.ts)0
-rw-r--r--server/initializers/migrations/0615-latest-versions-notification-settings.ts44
-rw-r--r--server/initializers/migrations/0620-latest-versions-application.ts27
-rw-r--r--server/initializers/migrations/0625-latest-versions-notification.ts26
-rw-r--r--server/lib/emailer.ts42
-rw-r--r--server/lib/emails/peertube-version-new/html.pug9
-rw-r--r--server/lib/emails/plugin-version-new/html.pug9
-rw-r--r--server/lib/notifier.ts74
-rw-r--r--server/lib/schedulers/peertube-version-check-scheduler.ts55
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts6
-rw-r--r--server/lib/user.ts4
-rw-r--r--server/models/account/user-notification-setting.ts22
-rw-r--r--server/models/account/user-notification.ts90
-rw-r--r--server/models/application/application.ts4
-rw-r--r--server/tests/api/check-params/user-notifications.ts4
-rw-r--r--server/tests/api/notifications/admin-notifications.ts165
-rw-r--r--server/tests/api/notifications/index.ts1
-rw-r--r--server/types/models/application/application.ts5
-rw-r--r--server/types/models/application/index.ts1
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts12
-rw-r--r--shared/extra-utils/index.ts2
-rw-r--r--shared/extra-utils/miscs/sql.ts13
-rw-r--r--shared/extra-utils/mock-servers/joinpeertube-versions.ts31
-rw-r--r--shared/extra-utils/mock-servers/mock-instances-index.ts (renamed from shared/extra-utils/instances-index/mock-instances-index.ts)0
-rw-r--r--shared/extra-utils/users/user-notifications.ts72
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/joinpeertube/index.ts1
-rw-r--r--shared/models/joinpeertube/versions.model.ts5
-rw-r--r--shared/models/users/user-notification-setting.model.ts3
-rw-r--r--shared/models/users/user-notification.model.ts16
44 files changed, 808 insertions, 37 deletions
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 {
42 newInstanceFollower: $localize`Your instance has a new follower`, 42 newInstanceFollower: $localize`Your instance has a new follower`,
43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`, 43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
44 abuseNewMessage: $localize`An abuse report received a new message`, 44 abuseNewMessage: $localize`An abuse report received a new message`,
45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators` 45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
46 newPeerTubeVersion: $localize`A new PeerTube version is available`,
47 newPluginVersion: $localize`One of your plugin/theme has a new available version`
46 } 48 }
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 50
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
52 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
54 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION 56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
57 newPeerTubeVersion: UserRight.MANAGE_DEBUG,
58 newPluginVersion: UserRight.MANAGE_DEBUG
55 } 59 }
56 } 60 }
57 61
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 {
6 AbuseState, 6 AbuseState,
7 ActorInfo, 7 ActorInfo,
8 FollowState, 8 FollowState,
9 PluginType,
9 UserNotification as UserNotificationServer, 10 UserNotification as UserNotificationServer,
10 UserNotificationType, 11 UserNotificationType,
11 UserRight, 12 UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
74 } 75 }
75 } 76 }
76 77
78 plugin?: {
79 name: string
80 type: PluginType
81 latestVersion: string
82 }
83
84 peertube?: {
85 latestVersion: string
86 }
87
77 createdAt: string 88 createdAt: string
78 updatedAt: string 89 updatedAt: string
79 90
80 // Additional fields 91 // Additional fields
81 videoUrl?: string 92 videoUrl?: string
82 commentUrl?: any[] 93 commentUrl?: any[]
94
83 abuseUrl?: string 95 abuseUrl?: string
84 abuseQueryParams?: { [id: string]: string } = {} 96 abuseQueryParams?: { [id: string]: string } = {}
97
85 videoAutoBlacklistUrl?: string 98 videoAutoBlacklistUrl?: string
99
86 accountUrl?: string 100 accountUrl?: string
101
87 videoImportIdentifier?: string 102 videoImportIdentifier?: string
88 videoImportUrl?: string 103 videoImportUrl?: string
104
89 instanceFollowUrl?: string 105 instanceFollowUrl?: string
90 106
107 peertubeVersionLink?: string
108
109 pluginUrl?: string
110 pluginQueryParams?: { [id: string]: string } = {}
111
91 constructor (hash: UserNotificationServer, user: AuthUser) { 112 constructor (hash: UserNotificationServer, user: AuthUser) {
92 this.id = hash.id 113 this.id = hash.id
93 this.type = hash.type 114 this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
114 this.actorFollow = hash.actorFollow 135 this.actorFollow = hash.actorFollow
115 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) 136 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
116 137
138 this.plugin = hash.plugin
139 this.peertube = hash.peertube
140
117 this.createdAt = hash.createdAt 141 this.createdAt = hash.createdAt
118 this.updatedAt = hash.updatedAt 142 this.updatedAt = hash.updatedAt
119 143
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
197 case UserNotificationType.AUTO_INSTANCE_FOLLOWING: 221 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
198 this.instanceFollowUrl = '/admin/follows/following-list' 222 this.instanceFollowUrl = '/admin/follows/following-list'
199 break 223 break
224
225 case UserNotificationType.NEW_PEERTUBE_VERSION:
226 this.peertubeVersionLink = 'https://joinpeertube.org/news'
227 break
228
229 case UserNotificationType.NEW_PLUGIN_VERSION:
230 this.pluginUrl = `/admin/plugins/list-installed`
231 this.pluginQueryParams.pluginType = this.plugin.type + ''
232 break
200 } 233 }
201 } catch (err) { 234 } catch (err) {
202 this.type = null 235 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 @@
191 </div> 191 </div>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
195 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
196
197 <div class="message" i18n>
198 <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 }}
199 </div>
200 </ng-container>
201
202 <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204
205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
194 <ng-container *ngSwitchDefault> 210 <ng-container *ngSwitchDefault>
195 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 211 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
196 212
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 {
45 } 45 }
46 46
47 loadNotifications (reset?: boolean) { 47 loadNotifications (reset?: boolean) {
48 this.userNotificationService.listMyNotifications({ 48 const options = {
49 pagination: this.componentPagination, 49 pagination: this.componentPagination,
50 ignoreLoadingBar: this.ignoreLoadingBar, 50 ignoreLoadingBar: this.ignoreLoadingBar,
51 sort: { 51 sort: {
@@ -53,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
53 // if we order by creation date, we want DESC. all other fields are ASC (like unread). 53 // if we order by creation date, we want DESC. all other fields are ASC (like unread).
54 order: this.sortField === 'createdAt' ? -1 : 1 54 order: this.sortField === 'createdAt' ? -1 : 1
55 } 55 }
56 }) 56 }
57
58 this.userNotificationService.listMyNotifications(options)
57 .subscribe( 59 .subscribe(
58 result => { 60 result => {
59 this.notifications = reset ? result.data : this.notifications.concat(result.data) 61 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:
198 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes 198 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
199 cleanup_remote_interactions: false 199 cleanup_remote_interactions: false
200 200
201peertube:
202 check_latest_version:
203 # Check and notify admins of new PeerTube versions
204 enabled: true
205 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
206 url: 'https://joinpeertube.org/api/v1/versions.json'
207
201cache: 208cache:
202 previews: 209 previews:
203 size: 500 # Max number of previews you want to cache 210 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:
196 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes 196 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
197 cleanup_remote_interactions: false 197 cleanup_remote_interactions: false
198 198
199peertube:
200 check_latest_version:
201 # Check and notify admins of new PeerTube versions
202 enabled: true
203 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
204 url: 'https://joinpeertube.org/api/v1/versions.json'
199 205
200############################################################################### 206###############################################################################
201# 207#
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:
38contact_form: 38contact_form:
39 enabled: true 39 enabled: true
40 40
41peertube:
42 check_latest_version:
43 enabled: false
44
41redundancy: 45redundancy:
42 videos: 46 videos:
43 check_interval: '1 minute' 47 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'
120import { PeerTubeSocket } from './server/lib/peertube-socket' 120import { PeerTubeSocket } from './server/lib/peertube-socket'
121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' 121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
122import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler' 122import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler'
123import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-version-check-scheduler'
123import { Hooks } from './server/lib/plugins/hooks' 124import { Hooks } from './server/lib/plugins/hooks'
124import { PluginManager } from './server/lib/plugins/plugin-manager' 125import { PluginManager } from './server/lib/plugins/plugin-manager'
125import { LiveManager } from './server/lib/live-manager' 126import { LiveManager } from './server/lib/live-manager'
@@ -277,6 +278,7 @@ async function startApplication () {
277 RemoveOldHistoryScheduler.Instance.enable() 278 RemoveOldHistoryScheduler.Instance.enable()
278 RemoveOldViewsScheduler.Instance.enable() 279 RemoveOldViewsScheduler.Instance.enable()
279 PluginsCheckScheduler.Instance.enable() 280 PluginsCheckScheduler.Instance.enable()
281 PeerTubeVersionCheckScheduler.Instance.enable()
280 AutoFollowIndexInstances.Instance.enable() 282 AutoFollowIndexInstances.Instance.enable()
281 283
282 // Redis initialization 284 // 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
80 newInstanceFollower: body.newInstanceFollower, 80 newInstanceFollower: body.newInstanceFollower,
81 autoInstanceFollowing: body.autoInstanceFollowing, 81 autoInstanceFollowing: body.autoInstanceFollowing,
82 abuseNewMessage: body.abuseNewMessage, 82 abuseNewMessage: body.abuseNewMessage,
83 abuseStateChange: body.abuseStateChange 83 abuseStateChange: body.abuseStateChange,
84 newPeerTubeVersion: body.newPeerTubeVersion,
85 newPluginVersion: body.newPluginVersion
84 } 86 }
85 87
86 await UserNotificationSettingModel.update(values, query) 88 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<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
251 } 251 }
252} 252}
253 253
254type SemVersion = { major: number, minor: number, patch: number }
254function parseSemVersion (s: string) { 255function parseSemVersion (s: string) {
255 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) 256 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
256 257
@@ -258,7 +259,7 @@ function parseSemVersion (s: string) {
258 major: parseInt(parsed[1]), 259 major: parseInt(parsed[1]),
259 minor: parseInt(parsed[2]), 260 minor: parseInt(parsed[2]),
260 patch: parseInt(parsed[3]) 261 patch: parseInt(parsed[3])
261 } 262 } as SemVersion
262} 263}
263 264
264const randomBytesPromise = promisify1<number, Buffer>(randomBytes) 265const randomBytesPromise = promisify1<number, Buffer>(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 () {
37 'theme.default', 37 'theme.default',
38 'remote_redundancy.videos.accept_from', 38 'remote_redundancy.videos.accept_from',
39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
40 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
40 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', 41 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
41 'search.search_index.disable_local_search', 'search.search_index.is_default_search', 42 'search.search_index.disable_local_search', 'search.search_index.is_default_search',
42 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', 43 '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 = {
163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') 163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
164 } 164 }
165 }, 165 },
166 PEERTUBE: {
167 CHECK_LATEST_VERSION: {
168 ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
169 URL: config.get<string>('peertube.check_latest_version.url')
170 }
171 },
166 ADMIN: { 172 ADMIN: {
167 get EMAIL () { return config.get<string>('admin.email') } 173 get EMAIL () { return config.get<string>('admin.email') }
168 }, 174 },
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'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 610 27const LAST_MIGRATION_VERSION = 625
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
207 updateVideos: 60000, // 1 minute 207 updateVideos: 60000, // 1 minute
208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day 208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, 209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
210 checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
210 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day 211 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
211 removeOldViews: 60000 * 60 * 24, // 1 day 212 removeOldViews: 60000 * 60 * 24, // 1 day
212 removeOldHistory: 60000 * 60 * 24, // 1 day 213 removeOldHistory: 60000 * 60 * 24, // 1 day
@@ -763,6 +764,7 @@ if (isTestInstance() === true) {
763 SCHEDULER_INTERVALS_MS.updateVideos = 5000 764 SCHEDULER_INTERVALS_MS.updateVideos = 5000
764 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 765 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
765 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 766 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
767 SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
766 REPEAT_JOBS['videos-views'] = { every: 5000 } 768 REPEAT_JOBS['videos-views'] = { every: 5000 }
767 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } 769 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
768 770
diff --git a/server/initializers/migrations/0610-views-index.ts b/server/initializers/migrations/0610-views-index copy.ts
index 02ee21172..02ee21172 100644
--- a/server/initializers/migrations/0610-views-index.ts
+++ b/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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
11
12 for (const column of notificationSettingColumns) {
13 const data = {
14 type: Sequelize.INTEGER,
15 defaultValue: null,
16 allowNull: true
17 }
18 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
19 }
20
21 {
22 const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
23 await utils.sequelize.query(query)
24 }
25
26 for (const column of notificationSettingColumns) {
27 const data = {
28 type: Sequelize.INTEGER,
29 defaultValue: null,
30 allowNull: false
31 }
32 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
33 }
34 }
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`
12 ALTER TABLE "userNotification"
13 ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
15 `)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
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'
12import { bunyanLogger, logger } from '../helpers/logger' 12import { bunyanLogger, logger } from '../helpers/logger'
13import { CONFIG, isEmailEnabled } from '../initializers/config' 13import { CONFIG, isEmailEnabled } from '../initializers/config'
14import { WEBSERVER } from '../initializers/constants' 14import { WEBSERVER } from '../initializers/constants'
15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue' 17import { JobQueue } from './job-queue'
18 18
@@ -403,7 +403,7 @@ class Emailer {
403 } 403 }
404 404
405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
406 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 406 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() 408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
409 409
@@ -417,7 +417,7 @@ class Emailer {
417 videoName: videoBlacklist.Video.name, 417 videoName: videoBlacklist.Video.name,
418 action: { 418 action: {
419 text: 'Review autoblacklist', 419 text: 'Review autoblacklist',
420 url: VIDEO_AUTO_BLACKLIST_URL 420 url: videoAutoBlacklistUrl
421 } 421 }
422 } 422 }
423 } 423 }
@@ -472,6 +472,42 @@ class Emailer {
472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
473 } 473 }
474 474
475 addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
476 const subject = `A new PeerTube version is available: ${latestVersion}`
477
478 const emailPayload: EmailPayload = {
479 to,
480 template: 'peertube-version-new',
481 subject,
482 text: subject,
483 locals: {
484 latestVersion
485 }
486 }
487
488 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
489 }
490
491 addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
492 const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
493
494 const subject = `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`
495
496 const emailPayload: EmailPayload = {
497 to,
498 template: 'plugin-version-new',
499 subject,
500 text: subject,
501 locals: {
502 pluginName: plugin.name,
503 latestVersion: plugin.latestVersion,
504 pluginUrl
505 }
506 }
507
508 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
509 }
510
475 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { 511 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
476 const emailPayload: EmailPayload = { 512 const emailPayload: EmailPayload = {
477 template: 'password-reset', 513 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 @@
1extends ../common/greetings
2
3block title
4 | New PeerTube version available
5
6block content
7 p
8 | A new version of PeerTube is available: #{latestVersion}.
9 | 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 @@
1extends ../common/greetings
2
3block title
4 | New plugin version available
5
6block content
7 p
8 | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
9 | 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'
19import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
21import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { Emailer } from './emailer'
@@ -144,6 +144,20 @@ class Notifier {
144 }) 144 })
145 } 145 }
146 146
147 notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
148 this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
149 .catch(err => {
150 logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
151 })
152 }
153
154 notifyOfNewPluginVersion (plugin: MPlugin) {
155 this.notifyAdminsOfNewPluginVersion(plugin)
156 .catch(err => {
157 logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
158 })
159 }
160
147 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { 161 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
148 // List all followers that are users 162 // List all followers that are users
149 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 163 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -667,6 +681,64 @@ class Notifier {
667 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 681 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
668 } 682 }
669 683
684 private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
685 // Use the debug right to know who is an administrator
686 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
687 if (admins.length === 0) return
688
689 logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
690
691 function settingGetter (user: MUserWithNotificationSetting) {
692 return user.NotificationSetting.newPeerTubeVersion
693 }
694
695 async function notificationCreator (user: MUserWithNotificationSetting) {
696 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
697 type: UserNotificationType.NEW_PEERTUBE_VERSION,
698 userId: user.id,
699 applicationId: application.id
700 })
701 notification.Application = application
702
703 return notification
704 }
705
706 function emailSender (emails: string[]) {
707 return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
708 }
709
710 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
711 }
712
713 private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
714 // Use the debug right to know who is an administrator
715 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
716 if (admins.length === 0) return
717
718 logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
719
720 function settingGetter (user: MUserWithNotificationSetting) {
721 return user.NotificationSetting.newPluginVersion
722 }
723
724 async function notificationCreator (user: MUserWithNotificationSetting) {
725 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
726 type: UserNotificationType.NEW_PLUGIN_VERSION,
727 userId: user.id,
728 pluginId: plugin.id
729 })
730 notification.Plugin = plugin
731
732 return notification
733 }
734
735 function emailSender (emails: string[]) {
736 return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
737 }
738
739 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
740 }
741
670 private async notify<T extends MUserWithNotificationSetting> (options: { 742 private async notify<T extends MUserWithNotificationSetting> (options: {
671 users: T[] 743 users: T[]
672 notificationCreator: (user: T) => Promise<UserNotificationModelForApi> 744 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
index 000000000..c8960465c
--- /dev/null
+++ b/server/lib/schedulers/peertube-version-check-scheduler.ts
@@ -0,0 +1,55 @@
1
2import { doJSONRequest } from '@server/helpers/requests'
3import { ApplicationModel } from '@server/models/application/application'
4import { compareSemVer } from '@shared/core-utils'
5import { JoinPeerTubeVersions } from '@shared/models'
6import { logger } from '../../helpers/logger'
7import { CONFIG } from '../../initializers/config'
8import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
9import { Notifier } from '../notifier'
10import { AbstractScheduler } from './abstract-scheduler'
11
12export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
13
14 private static instance: AbstractScheduler
15
16 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
17
18 private constructor () {
19 super()
20 }
21
22 protected async internalExecute () {
23 return this.checkLatestVersion()
24 }
25
26 private async checkLatestVersion () {
27 if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
28
29 logger.info('Checking latest PeerTube version.')
30
31 const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
32
33 if (!body?.peertube?.latestVersion) {
34 logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
35 return
36 }
37
38 const latestVersion = body.peertube.latestVersion
39 const application = await ApplicationModel.load()
40
41 // Already checked this version
42 if (application.latestPeerTubeVersion === latestVersion) return
43
44 if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
45 application.latestPeerTubeVersion = latestVersion
46 await application.save()
47
48 Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
49 }
50 }
51
52 static get Instance () {
53 return this.instance || (this.instance = new this())
54 }
55}
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'
6import { chunk } from 'lodash' 6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index' 7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' 8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier'
9 10
10export class PluginsCheckScheduler extends AbstractScheduler { 11export class PluginsCheckScheduler extends AbstractScheduler {
11 12
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
53 plugin.latestVersion = result.latestVersion 54 plugin.latestVersion = result.latestVersion
54 await plugin.save() 55 await plugin.save()
55 56
57 // Notify if there is an higher plugin version available
58 if (compareSemVer(plugin.version, result.latestVersion) < 0) {
59 Notifier.Instance.notifyOfNewPluginVersion(plugin)
60 }
61
56 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) 62 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
57 } 63 }
58 } 64 }
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 |
193 newInstanceFollower: UserNotificationSettingValue.WEB, 193 newInstanceFollower: UserNotificationSettingValue.WEB,
194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
196 autoInstanceFollowing: UserNotificationSettingValue.WEB 196 autoInstanceFollowing: UserNotificationSettingValue.WEB,
197 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
198 newPluginVersion: UserNotificationSettingValue.WEB
197 } 199 }
198 200
199 return UserNotificationSettingModel.create(values, { transaction: t }) 201 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 {
156 @Column 156 @Column
157 abuseNewMessage: UserNotificationSettingValue 157 abuseNewMessage: UserNotificationSettingValue
158 158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
159 @ForeignKey(() => UserModel) 177 @ForeignKey(() => UserModel)
160 @Column 178 @Column
161 userId: number 179 userId: number
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
195 newInstanceFollower: this.newInstanceFollower, 213 newInstanceFollower: this.newInstanceFollower,
196 autoInstanceFollowing: this.autoInstanceFollowing, 214 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage, 215 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange 216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
199 } 219 }
200 } 220 }
201} 221}
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'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
11import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
12import { ApplicationModel } from '../application/application'
12import { AvatarModel } from '../avatar/avatar' 13import { AvatarModel } from '../avatar/avatar'
14import { PluginModel } from '../server/plugin'
13import { ServerModel } from '../server/server' 15import { ServerModel } from '../server/server'
14import { getSort, throwIfNotValid } from '../utils' 16import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video' 17import { VideoModel } from '../video/video'
@@ -96,7 +98,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
96 attributes: [ 'id' ], 98 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(), 99 model: VideoAbuseModel.unscoped(),
98 required: false, 100 required: false,
99 include: [ buildVideoInclude(true) ] 101 include: [ buildVideoInclude(false) ]
100 }, 102 },
101 { 103 {
102 attributes: [ 'id' ], 104 attributes: [ 'id' ],
@@ -106,12 +108,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
106 { 108 {
107 attributes: [ 'id', 'originCommentId' ], 109 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel.unscoped(), 110 model: VideoCommentModel.unscoped(),
109 required: true, 111 required: false,
110 include: [ 112 include: [
111 { 113 {
112 attributes: [ 'id', 'name', 'uuid' ], 114 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(), 115 model: VideoModel.unscoped(),
114 required: true 116 required: false
115 } 117 }
116 ] 118 ]
117 } 119 }
@@ -120,7 +122,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
120 { 122 {
121 model: AccountModel, 123 model: AccountModel,
122 as: 'FlaggedAccount', 124 as: 'FlaggedAccount',
123 required: true, 125 required: false,
124 include: [ buildActorWithAvatarInclude() ] 126 include: [ buildActorWithAvatarInclude() ]
125 } 127 }
126 ] 128 ]
@@ -141,6 +143,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
141 }, 143 },
142 144
143 { 145 {
146 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
147 model: PluginModel.unscoped(),
148 required: false
149 },
150
151 {
152 attributes: [ 'id', 'latestPeerTubeVersion' ],
153 model: ApplicationModel.unscoped(),
154 required: false
155 },
156
157 {
144 attributes: [ 'id', 'state' ], 158 attributes: [ 'id', 'state' ],
145 model: ActorFollowModel.unscoped(), 159 model: ActorFollowModel.unscoped(),
146 required: false, 160 required: false,
@@ -251,6 +265,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
251 [Op.ne]: null 265 [Op.ne]: null
252 } 266 }
253 } 267 }
268 },
269 {
270 fields: [ 'pluginId' ],
271 where: {
272 pluginId: {
273 [Op.ne]: null
274 }
275 }
276 },
277 {
278 fields: [ 'applicationId' ],
279 where: {
280 applicationId: {
281 [Op.ne]: null
282 }
283 }
254 } 284 }
255 ] as (ModelIndexesOptions & { where?: WhereOptions })[] 285 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
256}) 286})
@@ -370,6 +400,30 @@ export class UserNotificationModel extends Model {
370 }) 400 })
371 ActorFollow: ActorFollowModel 401 ActorFollow: ActorFollowModel
372 402
403 @ForeignKey(() => PluginModel)
404 @Column
405 pluginId: number
406
407 @BelongsTo(() => PluginModel, {
408 foreignKey: {
409 allowNull: true
410 },
411 onDelete: 'cascade'
412 })
413 Plugin: PluginModel
414
415 @ForeignKey(() => ApplicationModel)
416 @Column
417 applicationId: number
418
419 @BelongsTo(() => ApplicationModel, {
420 foreignKey: {
421 allowNull: true
422 },
423 onDelete: 'cascade'
424 })
425 Application: ApplicationModel
426
373 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { 427 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
374 const where = { userId } 428 const where = { userId }
375 429
@@ -524,6 +578,18 @@ export class UserNotificationModel extends Model {
524 } 578 }
525 : undefined 579 : undefined
526 580
581 const plugin = this.Plugin
582 ? {
583 name: this.Plugin.name,
584 type: this.Plugin.type,
585 latestVersion: this.Plugin.latestVersion
586 }
587 : undefined
588
589 const peertube = this.Application
590 ? { latestVersion: this.Application.latestPeerTubeVersion }
591 : undefined
592
527 return { 593 return {
528 id: this.id, 594 id: this.id,
529 type: this.type, 595 type: this.type,
@@ -535,6 +601,8 @@ export class UserNotificationModel extends Model {
535 videoBlacklist, 601 videoBlacklist,
536 account, 602 account,
537 actorFollow, 603 actorFollow,
604 plugin,
605 peertube,
538 createdAt: this.createdAt.toISOString(), 606 createdAt: this.createdAt.toISOString(),
539 updatedAt: this.updatedAt.toISOString() 607 updatedAt: this.updatedAt.toISOString()
540 } 608 }
@@ -553,17 +621,19 @@ export class UserNotificationModel extends Model {
553 ? { 621 ? {
554 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), 622 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
555 623
556 video: { 624 video: abuse.VideoCommentAbuse.VideoComment.Video
557 id: abuse.VideoCommentAbuse.VideoComment.Video.id, 625 ? {
558 name: abuse.VideoCommentAbuse.VideoComment.Video.name, 626 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
559 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid 627 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
560 } 628 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
629 }
630 : undefined
561 } 631 }
562 : undefined 632 : undefined
563 633
564 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined 634 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
565 635
566 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined 636 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
567 637
568 return { 638 return {
569 id: abuse.id, 639 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 {
32 @Column 32 @Column
33 migrationVersion: number 33 migrationVersion: number
34 34
35 @AllowNull(true)
36 @Column
37 latestPeerTubeVersion: string
38
35 @HasOne(() => AccountModel, { 39 @HasOne(() => AccountModel, {
36 foreignKey: { 40 foreignKey: {
37 allowNull: true 41 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 () {
176 newInstanceFollower: UserNotificationSettingValue.WEB, 176 newInstanceFollower: UserNotificationSettingValue.WEB,
177 autoInstanceFollowing: UserNotificationSettingValue.WEB, 177 autoInstanceFollowing: UserNotificationSettingValue.WEB,
178 abuseNewMessage: UserNotificationSettingValue.WEB, 178 abuseNewMessage: UserNotificationSettingValue.WEB,
179 abuseStateChange: UserNotificationSettingValue.WEB 179 abuseStateChange: UserNotificationSettingValue.WEB,
180 newPeerTubeVersion: UserNotificationSettingValue.WEB,
181 newPluginVersion: UserNotificationSettingValue.WEB
180 } 182 }
181 183
182 it('Should fail with missing fields', async function () { 184 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 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
6import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
7import { ServerInfo } from '../../../../shared/extra-utils/index'
8import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
9import {
10 CheckerBaseParams,
11 checkNewPeerTubeVersion,
12 checkNewPluginVersion,
13 prepareNotificationsTest
14} from '../../../../shared/extra-utils/users/user-notifications'
15import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
16import { PluginType } from '@shared/models'
17
18describe('Test admin notifications', function () {
19 let server: ServerInfo
20 let userNotifications: UserNotification[] = []
21 let adminNotifications: UserNotification[] = []
22 let emails: object[] = []
23 let baseParams: CheckerBaseParams
24 let joinPeerTubeServer: MockJoinPeerTubeVersions
25
26 before(async function () {
27 this.timeout(120000)
28
29 const config = {
30 peertube: {
31 check_latest_version: {
32 enabled: true,
33 url: 'http://localhost:42102/versions.json'
34 }
35 },
36 plugins: {
37 index: {
38 enabled: true,
39 check_latest_versions_interval: '5 seconds'
40 }
41 }
42 }
43
44 const res = await prepareNotificationsTest(1, config)
45 emails = res.emails
46 server = res.servers[0]
47
48 userNotifications = res.userNotifications
49 adminNotifications = res.adminNotifications
50
51 baseParams = {
52 server: server,
53 emails,
54 socketNotifications: adminNotifications,
55 token: server.accessToken
56 }
57
58 await installPlugin({
59 url: server.url,
60 accessToken: server.accessToken,
61 npmName: 'peertube-plugin-hello-world'
62 })
63
64 await installPlugin({
65 url: server.url,
66 accessToken: server.accessToken,
67 npmName: 'peertube-theme-background-red'
68 })
69
70 joinPeerTubeServer = new MockJoinPeerTubeVersions()
71 await joinPeerTubeServer.initialize()
72 })
73
74 describe('Latest PeerTube version notification', function () {
75
76 it('Should not send a notification to admins if there is not a new version', async function () {
77 this.timeout(30000)
78
79 joinPeerTubeServer.setLatestVersion('1.4.2')
80
81 await wait(3000)
82 await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
83 })
84
85 it('Should send a notification to admins on new plugin version', async function () {
86 this.timeout(30000)
87
88 joinPeerTubeServer.setLatestVersion('15.4.2')
89
90 await wait(3000)
91 await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
92 })
93
94 it('Should not send the same notification to admins', async function () {
95 this.timeout(30000)
96
97 await wait(3000)
98 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
99 })
100
101 it('Should not have sent a notification to users', async function () {
102 this.timeout(30000)
103
104 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
105 })
106
107 it('Should send a new notification after a new release', async function () {
108 this.timeout(30000)
109
110 joinPeerTubeServer.setLatestVersion('15.4.3')
111
112 await wait(3000)
113 await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
114 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
115 })
116 })
117
118 describe('Latest plugin version notification', function () {
119
120 it('Should not send a notification to admins if there is no new plugin version', async function () {
121 this.timeout(30000)
122
123 await wait(6000)
124 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
125 })
126
127 it('Should send a notification to admins on new plugin version', async function () {
128 this.timeout(30000)
129
130 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
131 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
132 await wait(6000)
133
134 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
135 })
136
137 it('Should not send the same notification to admins', async function () {
138 this.timeout(30000)
139
140 await wait(6000)
141
142 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
143 })
144
145 it('Should not have sent a notification to users', async function () {
146 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
147 })
148
149 it('Should send a new notification after a new plugin release', async function () {
150 this.timeout(30000)
151
152 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
153 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
154 await wait(6000)
155
156 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
157 })
158 })
159
160 after(async function () {
161 MockSmtpServer.Instance.kill()
162
163 await cleanupTests([ server ])
164 })
165})
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 @@
1import './admin-notifications'
1import './comments-notifications' 2import './comments-notifications'
2import './moderation-notifications' 3import './moderation-notifications'
3import './notifications-api' 4import './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 @@
1import { ApplicationModel } from '@server/models/application/application'
2
3// ############################################################################
4
5export 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
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 @@
1export * from './account' 1export * from './account'
2export * from './application'
2export * from './moderation' 3export * from './moderation'
3export * from './oauth' 4export * from './oauth'
4export * from './server' 5export * 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 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' 2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { ApplicationModel } from '@server/models/application/application'
4import { PluginModel } from '@server/models/server/plugin'
3import { PickWith, PickWithOpt } from '@shared/core-utils' 5import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse' 6import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
@@ -85,13 +87,19 @@ export module UserNotificationIncludes {
85 Pick<ActorFollowModel, 'id' | 'state'> & 87 Pick<ActorFollowModel, 'id' | 'state'> &
86 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & 88 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
87 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> 89 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
90
91 export type PluginInclude =
92 Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
93
94 export type ApplicationInclude =
95 Pick<ApplicationModel, 'latestPeerTubeVersion'>
88} 96}
89 97
90// ############################################################################ 98// ############################################################################
91 99
92export type MUserNotification = 100export type MUserNotification =
93 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | 101 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
94 'VideoImport' | 'Account' | 'ActorFollow'> 102 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
95 103
96// ############################################################################ 104// ############################################################################
97 105
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
103 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & 111 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
104 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & 112 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
105 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & 113 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
114 Use<'Plugin', UserNotificationIncludes.PluginInclude> &
115 Use<'Application', UserNotificationIncludes.ApplicationInclude> &
106 Use<'Account', UserNotificationIncludes.AccountIncludeActor> 116 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 @@
1export * from './bulk/bulk' 1export * from './bulk/bulk'
2export * from './cli/cli' 2export * from './cli/cli'
3export * from './feeds/feeds' 3export * from './feeds/feeds'
4export * from './instances-index/mock-instances-index' 4export * from './mock-servers/mock-instances-index'
5export * from './miscs/miscs' 5export * from './miscs/miscs'
6export * from './miscs/sql' 6export * from './miscs/sql'
7export * from './miscs/stubs' 7export * 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[]) {
106 } 106 }
107} 107}
108 108
109function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) { 109function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
110 const seq = getSequelize(internalServerNumber) 110 const seq = getSequelize(internalServerNumber)
111 111
112 const options = { type: QueryTypes.UPDATE } 112 const options = { type: QueryTypes.UPDATE }
113 113
114 return seq.query(`UPDATE "plugin" SET "version" = '${newVersion}' WHERE "name" = '${pluginName}'`, options) 114 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
115}
116
117function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
118 return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
119}
120
121function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
122 return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
115} 123}
116 124
117function setActorFollowScores (internalServerNumber: number, newScore: number) { 125function setActorFollowScores (internalServerNumber: number, newScore: number) {
@@ -128,6 +136,7 @@ export {
128 setActorField, 136 setActorField,
129 countVideoViewsOf, 137 countVideoViewsOf,
130 setPluginVersion, 138 setPluginVersion,
139 setPluginLatestVersion,
131 selectQuery, 140 selectQuery,
132 deleteAll, 141 deleteAll,
133 updateQuery, 142 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 @@
1import * as express from 'express'
2
3export class MockJoinPeerTubeVersions {
4 private latestVersion: string
5
6 initialize () {
7 return new Promise<void>(res => {
8 const app = express()
9
10 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
12
13 return next()
14 })
15
16 app.get('/versions.json', (req: express.Request, res: express.Response) => {
17 return res.json({
18 peertube: {
19 latestVersion: this.latestVersion
20 }
21 })
22 })
23
24 app.listen(42102, () => res())
25 })
26 }
27
28 setLatestVersion (latestVersion: string) {
29 this.latestVersion = latestVersion
30 }
31}
diff --git a/shared/extra-utils/instances-index/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts
index 2604eda03..2604eda03 100644
--- a/shared/extra-utils/instances-index/mock-instances-index.ts
+++ b/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 @@
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { inspect } from 'util' 4import { inspect } from 'util'
5import { AbuseState } from '@shared/models' 5import { AbuseState, PluginType } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' 7import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
7import { MockSmtpServer } from '../miscs/email' 8import { MockSmtpServer } from '../miscs/email'
8import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' 9import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
@@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
11import { getUserNotificationSocket } from '../socket/socket-io' 12import { getUserNotificationSocket } from '../socket/socket-io'
12import { setAccessTokensToServers, userLogin } from './login' 13import { setAccessTokensToServers, userLogin } from './login'
13import { createUser, getMyUserInformation } from './users' 14import { createUser, getMyUserInformation } from './users'
14import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
15 15
16function updateMyNotificationSettings ( 16function updateMyNotificationSettings (
17 url: string, 17 url: string,
@@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo (
629 await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') 629 await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
630} 630}
631 631
632function getAllNotificationsSettings () { 632async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) {
633 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
634
635 function notificationChecker (notification: UserNotification, type: CheckerType) {
636 if (type === 'presence') {
637 expect(notification).to.not.be.undefined
638 expect(notification.type).to.equal(notificationType)
639
640 expect(notification.peertube).to.exist
641 expect(notification.peertube.latestVersion).to.equal(latestVersion)
642 } else {
643 expect(notification).to.satisfy((n: UserNotification) => {
644 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
645 })
646 }
647 }
648
649 function emailNotificationFinder (email: object) {
650 const text = email['text']
651
652 return text.includes(latestVersion)
653 }
654
655 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
656}
657
658async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) {
659 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
660
661 function notificationChecker (notification: UserNotification, type: CheckerType) {
662 if (type === 'presence') {
663 expect(notification).to.not.be.undefined
664 expect(notification.type).to.equal(notificationType)
665
666 expect(notification.plugin.name).to.equal(pluginName)
667 expect(notification.plugin.type).to.equal(pluginType)
668 } else {
669 expect(notification).to.satisfy((n: UserNotification) => {
670 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
671 })
672 }
673 }
674
675 function emailNotificationFinder (email: object) {
676 const text = email['text']
677
678 return text.includes(pluginName)
679 }
680
681 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
682}
683
684function getAllNotificationsSettings (): UserNotificationSetting {
633 return { 685 return {
634 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 686 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
635 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 687 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
@@ -644,11 +696,13 @@ function getAllNotificationsSettings () {
644 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 696 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
645 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 697 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
646 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 698 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
647 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 699 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
648 } as UserNotificationSetting 700 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
701 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
702 }
649} 703}
650 704
651async function prepareNotificationsTest (serversCount = 3) { 705async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
652 const userNotifications: UserNotification[] = [] 706 const userNotifications: UserNotification[] = []
653 const adminNotifications: UserNotification[] = [] 707 const adminNotifications: UserNotification[] = []
654 const adminNotificationsServer2: UserNotification[] = [] 708 const adminNotificationsServer2: UserNotification[] = []
@@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) {
665 limit: 20 719 limit: 20
666 } 720 }
667 } 721 }
668 const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) 722 const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
669 723
670 await setAccessTokensToServers(servers) 724 await setAccessTokensToServers(servers)
671 725
@@ -749,5 +803,7 @@ export {
749 checkNewInstanceFollower, 803 checkNewInstanceFollower,
750 prepareNotificationsTest, 804 prepareNotificationsTest,
751 checkNewCommentAbuseForModerators, 805 checkNewCommentAbuseForModerators,
752 checkNewAccountAbuseForModerators 806 checkNewAccountAbuseForModerators,
807 checkNewPeerTubeVersion,
808 checkNewPluginVersion
753} 809}
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'
7export * from './users' 7export * from './users'
8export * from './videos' 8export * from './videos'
9export * from './feeds' 9export * from './feeds'
10export * from './joinpeertube'
10export * from './overviews' 11export * from './overviews'
11export * from './plugins' 12export * from './plugins'
12export * from './search' 13export * 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 @@
1export interface JoinPeerTubeVersions {
2 peertube: {
3 latestVersion: string
4 }
5}
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 {
24 24
25 abuseStateChange: UserNotificationSettingValue 25 abuseStateChange: UserNotificationSettingValue
26 abuseNewMessage: UserNotificationSettingValue 26 abuseNewMessage: UserNotificationSettingValue
27
28 newPeerTubeVersion: UserNotificationSettingValue
29 newPluginVersion: UserNotificationSettingValue
27} 30}
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 @@
1import { FollowState } from '../actors' 1import { FollowState } from '../actors'
2import { AbuseState } from '../moderation' 2import { AbuseState } from '../moderation'
3import { PluginType } from '../plugins'
3 4
4export const enum UserNotificationType { 5export const enum UserNotificationType {
5 NEW_VIDEO_FROM_SUBSCRIPTION = 1, 6 NEW_VIDEO_FROM_SUBSCRIPTION = 1,
@@ -26,7 +27,10 @@ export const enum UserNotificationType {
26 27
27 ABUSE_STATE_CHANGE = 15, 28 ABUSE_STATE_CHANGE = 15,
28 29
29 ABUSE_NEW_MESSAGE = 16 30 ABUSE_NEW_MESSAGE = 16,
31
32 NEW_PLUGIN_VERSION = 17,
33 NEW_PEERTUBE_VERSION = 18
30} 34}
31 35
32export interface VideoInfo { 36export interface VideoInfo {
@@ -108,6 +112,16 @@ export interface UserNotification {
108 } 112 }
109 } 113 }
110 114
115 plugin?: {
116 name: string
117 type: PluginType
118 latestVersion: string
119 }
120
121 peertube?: {
122 latestVersion: string
123 }
124
111 createdAt: string 125 createdAt: string
112 updatedAt: string 126 updatedAt: string
113} 127}