Add new plugin/peertube version notifs

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

View File

@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
newInstanceFollower: $localize`Your instance has a new follower`,
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
}
}

View 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

View File

@ -191,6 +191,22 @@
</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>

View 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)

View File

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

View File

@ -196,6 +196,12 @@ federation:
# We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
cleanup_remote_interactions: false
peertube:
check_latest_version:
# Check and notify admins of new PeerTube versions
enabled: true
# You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
url: 'https://joinpeertube.org/api/v1/versions.json'
###############################################################################
#

View File

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

View File

@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
import { PeerTubeSocket } from './server/lib/peertube-socket'
import { 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

View 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)

View File

@ -251,6 +251,7 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
}
}
type SemVersion = { major: number, minor: number, patch: number }
function parseSemVersion (s: string) {
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)

View 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',

View 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') }
},

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 610
const LAST_MIGRATION_VERSION = 625
// ---------------------------------------------------------------------------
@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
updateVideos: 60000, // 1 minute
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 }

View File

@ -0,0 +1,44 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
{
const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
for (const column of notificationSettingColumns) {
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('userNotificationSetting', column, data)
}
{
const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
await utils.sequelize.query(query)
}
for (const column of notificationSettingColumns) {
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: false
}
await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
}
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -0,0 +1,27 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
{
const data = {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -0,0 +1,26 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
{
await utils.sequelize.query(`
ALTER TABLE "userNotification"
ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
`)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -12,7 +12,7 @@ import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger'
import { 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',

View File

@ -0,0 +1,9 @@
extends ../common/greetings
block title
| New PeerTube version available
block content
p
| A new version of PeerTube is available: #{latestVersion}.
| You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].

View File

@ -0,0 +1,9 @@
extends ../common/greetings
block title
| New plugin version available
block content
p
| A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
| You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].

View File

@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { 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>

View File

@ -0,0 +1,55 @@
import { doJSONRequest } from '@server/helpers/requests'
import { ApplicationModel } from '@server/models/application/application'
import { compareSemVer } from '@shared/core-utils'
import { JoinPeerTubeVersions } from '@shared/models'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
import { Notifier } from '../notifier'
import { AbstractScheduler } from './abstract-scheduler'
export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
private constructor () {
super()
}
protected async internalExecute () {
return this.checkLatestVersion()
}
private async checkLatestVersion () {
if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
logger.info('Checking latest PeerTube version.')
const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
if (!body?.peertube?.latestVersion) {
logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
return
}
const latestVersion = body.peertube.latestVersion
const application = await ApplicationModel.load()
// Already checked this version
if (application.latestPeerTubeVersion === latestVersion) return
if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
application.latestPeerTubeVersion = latestVersion
await application.save()
Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
}
}
static get Instance () {
return this.instance || (this.instance = new this())
}
}

View File

@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
import { chunk } from 'lodash'
import { 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)
}
}

View 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 })

View 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
}
}
}

View 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,

View File

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

View 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 () {

View File

@ -0,0 +1,165 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
import { expect } from 'chai'
import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
import { ServerInfo } from '../../../../shared/extra-utils/index'
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
import {
CheckerBaseParams,
checkNewPeerTubeVersion,
checkNewPluginVersion,
prepareNotificationsTest
} from '../../../../shared/extra-utils/users/user-notifications'
import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
import { PluginType } from '@shared/models'
describe('Test admin notifications', function () {
let server: ServerInfo
let userNotifications: UserNotification[] = []
let adminNotifications: UserNotification[] = []
let emails: object[] = []
let baseParams: CheckerBaseParams
let joinPeerTubeServer: MockJoinPeerTubeVersions
before(async function () {
this.timeout(120000)
const config = {
peertube: {
check_latest_version: {
enabled: true,
url: 'http://localhost:42102/versions.json'
}
},
plugins: {
index: {
enabled: true,
check_latest_versions_interval: '5 seconds'
}
}
}
const res = await prepareNotificationsTest(1, config)
emails = res.emails
server = res.servers[0]
userNotifications = res.userNotifications
adminNotifications = res.adminNotifications
baseParams = {
server: server,
emails,
socketNotifications: adminNotifications,
token: server.accessToken
}
await installPlugin({
url: server.url,
accessToken: server.accessToken,
npmName: 'peertube-plugin-hello-world'
})
await installPlugin({
url: server.url,
accessToken: server.accessToken,
npmName: 'peertube-theme-background-red'
})
joinPeerTubeServer = new MockJoinPeerTubeVersions()
await joinPeerTubeServer.initialize()
})
describe('Latest PeerTube version notification', function () {
it('Should not send a notification to admins if there is not a new version', async function () {
this.timeout(30000)
joinPeerTubeServer.setLatestVersion('1.4.2')
await wait(3000)
await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
})
it('Should send a notification to admins on new plugin version', async function () {
this.timeout(30000)
joinPeerTubeServer.setLatestVersion('15.4.2')
await wait(3000)
await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
})
it('Should not send the same notification to admins', async function () {
this.timeout(30000)
await wait(3000)
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
})
it('Should not have sent a notification to users', async function () {
this.timeout(30000)
expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
})
it('Should send a new notification after a new release', async function () {
this.timeout(30000)
joinPeerTubeServer.setLatestVersion('15.4.3')
await wait(3000)
await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
})
})
describe('Latest plugin version notification', function () {
it('Should not send a notification to admins if there is no new plugin version', async function () {
this.timeout(30000)
await wait(6000)
await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
})
it('Should send a notification to admins on new plugin version', async function () {
this.timeout(30000)
await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
await wait(6000)
await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
})
it('Should not send the same notification to admins', async function () {
this.timeout(30000)
await wait(6000)
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
})
it('Should not have sent a notification to users', async function () {
expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
})
it('Should send a new notification after a new plugin release', async function () {
this.timeout(30000)
await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
await wait(6000)
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
})
})
after(async function () {
MockSmtpServer.Instance.kill()
await cleanupTests([ server ])
})
})

View File

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

View File

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

View File

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

View File

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

View 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>

View 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'

View File

@ -106,12 +106,20 @@ async function closeAllSequelize (servers: ServerInfo[]) {
}
}
function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
const seq = getSequelize(internalServerNumber)
const 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,

View File

@ -0,0 +1,31 @@
import * as express from 'express'
export class MockJoinPeerTubeVersions {
private latestVersion: string
initialize () {
return new Promise<void>(res => {
const app = express()
app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
return next()
})
app.get('/versions.json', (req: express.Request, res: express.Response) => {
return res.json({
peertube: {
latestVersion: this.latestVersion
}
})
})
app.listen(42102, () => res())
})
}
setLatestVersion (latestVersion: string) {
this.latestVersion = latestVersion
}
}

View File

@ -2,7 +2,8 @@
import { expect } from 'chai'
import { 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
}

View 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'

View File

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

View File

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

View File

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

View 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
}