Add auto follow back support for instances

This commit is contained in:
Chocobozzz 2019-08-30 16:50:12 +02:00 committed by Chocobozzz
parent f69ec5f340
commit 8424c4026a
44 changed files with 652 additions and 157 deletions

View File

@ -112,7 +112,7 @@ export class UserNotification implements UserNotificationServer {
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
this.videoUrl = this.buildVideoUrl(this.video)
this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
break
case UserNotificationType.BLACKLIST_ON_MY_VIDEO:

View File

@ -273,5 +273,21 @@ followers:
# Whether or not an administrator must manually validate a new follower
manual_approval: false
followings:
instance:
# If you want to automatically follow back new instance followers
# Only follows accepted followers (in case you enabled manual followers approbation)
# If this option is enabled, use the mute feature instead of deleting followings
# /!\ Don't enable this if you don't have a reactive moderation team /!\
auto_follow_back:
enabled: false
# If you want to automatically follow instances of the public index
# If this option is enabled, use the mute feature instead of deleting followings
# /!\ Don't enable this if you don't have a reactive moderation team /!\
auto_follow_index:
enabled: false
index_url: 'https://instances.joinpeertube.org'
theme:
default: 'default'

View File

@ -288,5 +288,20 @@ followers:
# Whether or not an administrator must manually validate a new follower
manual_approval: false
followings:
instance:
# If you want to automatically follow back new instance followers
# If this option is enabled, use the mute feature instead of deleting followings
# /!\ Don't enable this if you don't have a reactive moderation team /!\
auto_follow_back:
enabled: false
# If you want to automatically follow instances of the public index
# If this option is enabled, use the mute feature instead of deleting followings
# /!\ Don't enable this if you don't have a reactive moderation team /!\
auto_follow_index:
enabled: false
index_url: 'https://instances.joinpeertube.org'
theme:
default: 'default'

View File

@ -132,6 +132,7 @@
"lru-cache": "^5.1.1",
"magnet-uri": "^5.1.4",
"memoizee": "^0.4.14",
"module-alias": "^2.2.1",
"morgan": "^1.5.3",
"multer": "^1.1.0",
"nodemailer": "^6.0.0",
@ -224,5 +225,8 @@
"scripty": {
"silent": true
},
"sasslintConfig": "client/.sass-lint.yml"
"sasslintConfig": "client/.sass-lint.yml",
"_moduleAliases": {
"@server": "dist/server"
}
}

View File

@ -1,3 +1,5 @@
require('module-alias/register')
// FIXME: https://github.com/nodejs/node/pull/16853
import { PluginManager } from './server/lib/plugins/plugin-manager'

View File

@ -300,6 +300,18 @@ function customConfig (): CustomConfig {
enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
}
},
followings: {
instance: {
autoFollowBack: {
enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
},
autoFollowIndex: {
enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
}
}
}
}
}

View File

@ -25,6 +25,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { JobQueue } from '../../../lib/job-queue'
import { removeRedundancyOf } from '../../../lib/redundancy'
import { sequelizeTypescript } from '../../../initializers/database'
import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
const serverFollowsRouter = express.Router()
serverFollowsRouter.get('/following',
@ -172,5 +173,7 @@ async function acceptFollower (req: express.Request, res: express.Response) {
follow.state = 'accepted'
await follow.save()
await autoFollowBackIfNeeded(follow)
return res.status(204).end()
}

View File

@ -76,7 +76,8 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
newFollow: body.newFollow,
newUserRegistration: body.newUserRegistration,
commentMention: body.commentMention,
newInstanceFollower: body.newInstanceFollower
newInstanceFollower: body.newInstanceFollower,
autoInstanceFollowing: body.autoInstanceFollowing
}
await UserNotificationSettingModel.update(values, query)

View File

@ -232,6 +232,23 @@ const CONFIG = {
get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
}
},
FOLLOWINGS: {
INSTANCE: {
AUTO_FOLLOW_BACK: {
get ENABLED () {
return config.get<boolean>('followings.instance.auto_follow_back.enabled')
}
},
AUTO_FOLLOW_INDEX: {
get ENABLED () {
return config.get<boolean>('followings.instance.auto_follow_index.enabled')
},
get INDEX_URL () {
return config.get<string>('followings.instance.auto_follow_index.index_url')
}
}
}
},
THEME: {
get DEFAULT () { return config.get<string>('theme.default') }
}

View File

@ -0,0 +1,36 @@
import { MActorFollowActors } from '../../typings/models'
import { CONFIG } from '../../initializers/config'
import { SERVER_ACTOR_NAME } from '../../initializers/constants'
import { JobQueue } from '../job-queue'
import { logger } from '../../helpers/logger'
import { getServerActor } from '../../helpers/utils'
import { ServerModel } from '@server/models/server/server'
async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
const follower = actorFollow.ActorFollower
if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) {
logger.info('Auto follow back %s.', follower.url)
const me = await getServerActor()
const server = await ServerModel.load(follower.serverId)
const host = server.host
const payload = {
host,
name: SERVER_ACTOR_NAME,
followerActorId: me.id,
isAutoFollow: true
}
JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
.catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
}
}
export {
autoFollowBackIfNeeded
}

View File

@ -24,7 +24,7 @@ async function processAccept (actor: MActorDefault, targetActor: MActorSignature
if (!follow) throw new Error('Cannot find associated follow.')
if (follow.state !== 'accepted') {
follow.set('state', 'accepted')
follow.state = 'accepted'
await follow.save()
await addFetchOutboxJob(targetActor)

View File

@ -10,7 +10,8 @@ import { getAPId } from '../../../helpers/activitypub'
import { getServerActor } from '../../../helpers/utils'
import { CONFIG } from '../../../initializers/config'
import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
import { MAccount, MActorFollowActors, MActorFollowFull, MActorSignature } from '../../../typings/models'
import { MActorFollowActors, MActorSignature } from '../../../typings/models'
import { autoFollowBackIfNeeded } from '../follow'
async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
const { activity, byActor } = options
@ -28,7 +29,7 @@ export {
// ---------------------------------------------------------------------------
async function processFollow (byActor: MActorSignature, targetActorURL: string) {
const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => {
const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => {
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
if (!targetActor) throw new Error('Unknown actor')
@ -67,21 +68,24 @@ async function processFollow (byActor: MActorSignature, targetActorURL: string)
actorFollow.ActorFollowing = targetActor
// Target sends to actor he accepted the follow request
if (actorFollow.state === 'accepted') await sendAccept(actorFollow)
if (actorFollow.state === 'accepted') {
await sendAccept(actorFollow)
await autoFollowBackIfNeeded(actorFollow)
}
return { actorFollow, created, isFollowingInstance }
return { actorFollow, created, isFollowingInstance, targetActor }
})
// Rejected
if (!actorFollow) return
if (created) {
if (isFollowingInstance) {
Notifier.Instance.notifyOfNewInstanceFollow(actorFollow)
} else {
const actorFollowFull = actorFollow as MActorFollowFull
actorFollowFull.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as MAccount
const follower = await ActorModel.loadFull(byActor.id)
const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
if (isFollowingInstance) {
Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
} else {
Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
}
}

View File

@ -1,5 +1,4 @@
import { ActivityFollow } from '../../../../shared/models/activitypub'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getActorFollowActivityPubUrl } from '../url'
import { unicastTo } from './utils'
import { logger } from '../../../helpers/logger'

View File

@ -6,8 +6,15 @@ import { JobQueue } from './job-queue'
import { EmailPayload } from './job-queue/handlers/email'
import { readFileSync } from 'fs-extra'
import { WEBSERVER } from '../initializers/constants'
import { MCommentOwnerVideo, MVideo, MVideoAbuseVideo, MVideoAccountLight, MVideoBlacklistVideo } from '../typings/models/video'
import { MActorFollowActors, MActorFollowFollowingFullFollowerAccount, MUser } from '../typings/models'
import {
MCommentOwnerVideo,
MVideo,
MVideoAbuseVideo,
MVideoAccountLight,
MVideoBlacklistLightVideo,
MVideoBlacklistVideo
} from '../typings/models/video'
import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
type SendEmailOptions = {
@ -107,7 +114,7 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addNewFollowNotification (to: string[], actorFollow: MActorFollowFollowingFullFollowerAccount, followType: 'account' | 'channel') {
addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
const followerName = actorFollow.ActorFollower.Account.getDisplayName()
const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
@ -144,6 +151,22 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
const text = `Hi dear admin,\n\n` +
`Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
`\n\n` +
`Cheers,\n` +
`${CONFIG.EMAIL.BODY.SIGNATURE}`
const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
text
}
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
myVideoPublishedNotification (to: string[], video: MVideo) {
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
@ -265,9 +288,9 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addVideoAutoBlacklistModeratorsNotification (to: string[], video: MVideo) {
addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
const text = `Hi,\n\n` +
`A recently added video was auto-blacklisted and requires moderator review before publishing.` +

View File

@ -10,12 +10,13 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { ActorModel } from '../../../models/activitypub/actor'
import { Notifier } from '../../notifier'
import { sequelizeTypescript } from '../../../initializers/database'
import { MAccount, MActor, MActorFollowActors, MActorFollowFull, MActorFull } from '../../../typings/models'
import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
export type ActivitypubFollowPayload = {
followerActorId: number
name: string
host: string
isAutoFollow?: boolean
}
async function processActivityPubFollow (job: Bull.Job) {
@ -35,7 +36,7 @@ async function processActivityPubFollow (job: Bull.Job) {
const fromActor = await ActorModel.load(payload.followerActorId)
return retryTransactionWrapper(follow, fromActor, targetActor)
return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
}
// ---------------------------------------------------------------------------
@ -45,7 +46,7 @@ export {
// ---------------------------------------------------------------------------
async function follow (fromActor: MActor, targetActor: MActorFull) {
async function follow (fromActor: MActor, targetActor: MActorFull, isAutoFollow = false) {
if (fromActor.id === targetActor.id) {
throw new Error('Follower is the same than target actor.')
}
@ -75,14 +76,15 @@ async function follow (fromActor: MActor, targetActor: MActorFull) {
return actorFollow
})
if (actorFollow.state === 'accepted') {
const followerFull = Object.assign(fromActor, { Account: await actorFollow.ActorFollower.$get('Account') as MAccount })
const followerFull = await ActorModel.loadFull(fromActor.id)
const actorFollowFull = Object.assign(actorFollow, {
ActorFollowing: targetActor,
ActorFollower: followerFull
})
Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
}
if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
if (isAutoFollow === true) Notifier.Instance.notifyOfAutoInstanceFollowing(actorFollowFull)
return actorFollow
}

View File

@ -21,6 +21,7 @@ import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumb
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { MThumbnail } from '../../../typings/models/video/thumbnail'
import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models'
type VideoImportYoutubeDLPayload = {
type: 'youtube-dl'
@ -204,7 +205,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
if (video.isBlacklisted()) {
Notifier.Instance.notifyOnVideoAutoBlacklist(video)
const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
} else {
Notifier.Instance.notifyOnNewVideoIfNeeded(video)
}

View File

@ -1,30 +1,30 @@
import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
import { logger } from '../helpers/logger'
import { VideoModel } from '../models/video/video'
import { Emailer } from './emailer'
import { UserNotificationModel } from '../models/account/user-notification'
import { VideoCommentModel } from '../models/video/video-comment'
import { UserModel } from '../models/account/user'
import { PeerTubeSocket } from './peertube-socket'
import { CONFIG } from '../initializers/config'
import { VideoPrivacy, VideoState } from '../../shared/models/videos'
import { VideoBlacklistModel } from '../models/video/video-blacklist'
import * as Bluebird from 'bluebird'
import { VideoImportModel } from '../models/video/video-import'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import {
MCommentOwnerVideo,
MVideo,
MVideoAbuseVideo,
MVideoAccountLight,
MVideoBlacklistLightVideo,
MVideoBlacklistVideo,
MVideoFullLight
} from '../typings/models/video'
import { MUser, MUserAccount, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/typings/models/user'
import { MActorFollowActors, MActorFollowFull, MActorFollowFollowingFullFollowerAccount } from '../typings/models'
import { ActorFollowModel } from '../models/activitypub/actor-follow'
import {
MUser,
MUserDefault,
MUserNotifSettingAccount,
MUserWithNotificationSetting,
UserNotificationModelForApi
} from '@server/typings/models/user'
import { MActorFollowFull } from '../typings/models'
import { MVideoImportVideo } from '@server/typings/models/video/video-import'
import { AccountModel } from '@server/models/account/account'
class Notifier {
@ -77,9 +77,9 @@ class Notifier {
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
}
notifyOnVideoAutoBlacklist (video: MVideo): void {
this.notifyModeratorsOfVideoAutoBlacklist(video)
.catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
.catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
}
notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
@ -87,7 +87,7 @@ class Notifier {
.catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
}
notifyOnVideoUnblacklist (video: MVideo): void {
notifyOnVideoUnblacklist (video: MVideoFullLight): void {
this.notifyVideoOwnerOfUnblacklist(video)
.catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
}
@ -97,12 +97,12 @@ class Notifier {
.catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
}
notifyOnNewUserRegistration (user: MUserAccount): void {
notifyOnNewUserRegistration (user: MUserDefault): void {
this.notifyModeratorsOfNewUserRegistration(user)
.catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
}
notifyOfNewUserFollow (actorFollow: MActorFollowFollowingFullFollowerAccount): void {
notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
this.notifyUserOfNewActorFollow(actorFollow)
.catch(err => {
logger.error(
@ -114,30 +114,37 @@ class Notifier {
})
}
notifyOfNewInstanceFollow (actorFollow: MActorFollowActors): void {
notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
this.notifyAdminsOfNewInstanceFollow(actorFollow)
.catch(err => {
logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
})
}
notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
.catch(err => {
logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
})
}
private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
// List all followers that are users
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
logger.info('Notifying %d users of new video %s.', users.length, video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.newVideoFromSubscription
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
userId: user.id,
videoId: video.id
})
notification.Video = video as VideoModel
notification.Video = video
return notification
}
@ -162,17 +169,17 @@ class Notifier {
logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.newCommentOnMyVideo
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
userId: user.id,
commentId: comment.id
})
notification.Comment = comment as VideoCommentModel
notification.Comment = comment
return notification
}
@ -207,19 +214,19 @@ class Notifier {
logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserNotifSettingAccount) {
if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE
return user.NotificationSetting.commentMention
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserNotifSettingAccount) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.COMMENT_MENTION,
userId: user.id,
commentId: comment.id
})
notification.Comment = comment as VideoCommentModel
notification.Comment = comment
return notification
}
@ -231,7 +238,7 @@ class Notifier {
return this.notify({ users, settingGetter, notificationCreator, emailSender })
}
private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFollowingFullFollowerAccount) {
private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
if (actorFollow.ActorFollowing.isOwned() === false) return
// Account follows one of our account?
@ -253,17 +260,17 @@ class Notifier {
logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.newFollow
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_FOLLOW,
userId: user.id,
actorFollowId: actorFollow.id
})
notification.ActorFollow = actorFollow as ActorFollowModel
notification.ActorFollow = actorFollow
return notification
}
@ -275,22 +282,22 @@ class Notifier {
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
}
private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowActors) {
private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.newInstanceFollower
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
userId: user.id,
actorFollowId: actorFollow.id
})
notification.ActorFollow = actorFollow as ActorFollowModel
notification.ActorFollow = actorFollow
return notification
}
@ -302,18 +309,45 @@ class Notifier {
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
}
private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.autoInstanceFollowing
}
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
userId: user.id,
actorFollowId: actorFollow.id
})
notification.ActorFollow = actorFollow
return notification
}
function emailSender (emails: string[]) {
return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
}
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
}
private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
if (moderators.length === 0) return
logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.videoAbuseAsModerator
}
async function notificationCreator (user: UserModel) {
const notification: UserNotificationModelForApi = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
userId: user.id,
videoAbuseId: videoAbuse.id
@ -330,29 +364,29 @@ class Notifier {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
}
private async notifyModeratorsOfVideoAutoBlacklist (video: MVideo) {
private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
if (moderators.length === 0) return
logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.videoAutoBlacklistAsModerator
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
userId: user.id,
videoId: video.id
videoBlacklistId: videoBlacklist.id
})
notification.Video = video as VideoModel
notification.VideoBlacklist = videoBlacklist
return notification
}
function emailSender (emails: string[]) {
return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
}
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
@ -364,17 +398,17 @@ class Notifier {
logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.blacklistOnMyVideo
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
userId: user.id,
videoBlacklistId: videoBlacklist.id
})
notification.VideoBlacklist = videoBlacklist as VideoBlacklistModel
notification.VideoBlacklist = videoBlacklist
return notification
}
@ -386,23 +420,23 @@ class Notifier {
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
}
private async notifyVideoOwnerOfUnblacklist (video: MVideo) {
private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
const user = await UserModel.loadByVideoId(video.id)
if (!user) return
logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.blacklistOnMyVideo
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
userId: user.id,
videoId: video.id
})
notification.Video = video as VideoModel
notification.Video = video
return notification
}
@ -420,17 +454,17 @@ class Notifier {
logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.myVideoPublished
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.MY_VIDEO_PUBLISHED,
userId: user.id,
videoId: video.id
})
notification.Video = video as VideoModel
notification.Video = video
return notification
}
@ -448,17 +482,17 @@ class Notifier {
logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.myVideoImportFinished
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
userId: user.id,
videoImportId: videoImport.id
})
notification.VideoImport = videoImport as VideoImportModel
notification.VideoImport = videoImport
return notification
}
@ -472,7 +506,7 @@ class Notifier {
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
}
private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserAccount) {
private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
if (moderators.length === 0) return
@ -481,17 +515,17 @@ class Notifier {
moderators.length, registeredUser.username
)
function settingGetter (user: UserModel) {
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.newUserRegistration
}
async function notificationCreator (user: UserModel) {
const notification = await UserNotificationModel.create({
async function notificationCreator (user: MUserWithNotificationSetting) {
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_USER_REGISTRATION,
userId: user.id,
accountId: registeredUser.Account.id
})
notification.Account = registeredUser.Account as AccountModel
notification.Account = registeredUser.Account
return notification
}
@ -503,11 +537,11 @@ class Notifier {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
}
private async notify (options: {
users: MUserWithNotificationSetting[],
notificationCreator: (user: MUserWithNotificationSetting) => Promise<UserNotificationModelForApi>,
private async notify <T extends MUserWithNotificationSetting> (options: {
users: T[],
notificationCreator: (user: T) => Promise<UserNotificationModelForApi>,
emailSender: (emails: string[]) => Promise<any> | Bluebird<any>,
settingGetter: (user: MUserWithNotificationSetting) => UserNotificationSettingValue
settingGetter: (user: T) => UserNotificationSettingValue
}) {
const emails: string[] = []

View File

@ -138,7 +138,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
newUserRegistration: UserNotificationSettingValue.WEB,
commentMention: UserNotificationSettingValue.WEB,
newFollow: UserNotificationSettingValue.WEB,
newInstanceFollower: UserNotificationSettingValue.WEB
newInstanceFollower: UserNotificationSettingValue.WEB,
autoInstanceFollowing: UserNotificationSettingValue.WEB
}
return UserNotificationSettingModel.create(values, { transaction: t })

View File

@ -6,7 +6,7 @@ import { logger } from '../helpers/logger'
import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
import { Hooks } from './plugins/hooks'
import { Notifier } from './notifier'
import { MUser, MVideoBlacklist, MVideoWithBlacklistLight } from '@server/typings/models'
import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models'
async function autoBlacklistVideoIfNeeded (parameters: {
video: MVideoWithBlacklistLight,
@ -31,7 +31,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
reason: 'Auto-blacklisted. Moderator review required.',
type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
}
const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklist>({
const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklistVideo>({
where: {
videoId: video.id
},
@ -40,7 +40,9 @@ async function autoBlacklistVideoIfNeeded (parameters: {
})
video.VideoBlacklist = videoBlacklist
if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(video)
videoBlacklist.Video = video
if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
logger.info('Video %s auto-blacklisted.', video.uuid)

View File

@ -43,6 +43,8 @@ const updateNotificationSettingsValidator = [
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'),
body('newInstanceFollower')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'),
body('autoInstanceFollowing')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance following notification setting'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body })

View File

@ -381,7 +381,7 @@ export class AccountModel extends Model<AccountModel> {
}
toActivityPubObject (this: MAccountAP) {
const obj = this.Actor.toActivityPubObject(this.name, 'Account')
const obj = this.Actor.toActivityPubObject(this.name)
return Object.assign(obj, {
summary: this.description

View File

@ -111,6 +111,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@Column
newInstanceFollower: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@Is(
'UserNotificationSettingNewInstanceFollower',
value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
)
@Column
autoInstanceFollowing: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@Is(
@ -165,7 +174,8 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
newUserRegistration: this.newUserRegistration,
commentMention: this.commentMention,
newFollow: this.newFollow,
newInstanceFollower: this.newInstanceFollower
newInstanceFollower: this.newInstanceFollower,
autoInstanceFollowing: this.autoInstanceFollowing
}
}
}

View File

@ -135,13 +135,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
]
},
{
attributes: [ 'preferredUsername' ],
attributes: [ 'preferredUsername', 'type' ],
model: ActorModel.unscoped(),
required: true,
as: 'ActorFollowing',
include: [
buildChannelInclude(false),
buildAccountInclude(false)
buildAccountInclude(false),
{
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
}
]
}
]
@ -404,6 +409,11 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
const account = this.Account ? this.formatActor(this.Account) : undefined
const actorFollowingType = {
Application: 'instance' as 'instance',
Group: 'channel' as 'channel',
Person: 'account' as 'account'
}
const actorFollow = this.ActorFollow ? {
id: this.ActorFollow.id,
state: this.ActorFollow.state,
@ -415,9 +425,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
host: this.ActorFollow.ActorFollower.getHost()
},
following: {
type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
name: this.ActorFollow.ActorFollowing.preferredUsername
name: this.ActorFollow.ActorFollowing.preferredUsername,
host: this.ActorFollow.ActorFollowing.getHost()
}
} : undefined

View File

@ -43,7 +43,6 @@ import {
MActorFormattable,
MActorFull,
MActorHost,
MActorRedundancyAllowedOpt,
MActorServer,
MActorSummaryFormattable
} from '../../typings/models'
@ -430,15 +429,8 @@ export class ActorModel extends Model<ActorModel> {
})
}
toActivityPubObject (this: MActorAP, name: string, type: 'Account' | 'Application' | 'VideoChannel') {
toActivityPubObject (this: MActorAP, name: string) {
let activityPubType
if (type === 'Account') {
activityPubType = 'Person' as 'Person'
} else if (type === 'Application') {
activityPubType = 'Application' as 'Application'
} else { // VideoChannel
activityPubType = 'Group' as 'Group'
}
let icon = undefined
if (this.avatarId) {
@ -451,7 +443,7 @@ export class ActorModel extends Model<ActorModel> {
}
const json = {
type: activityPubType,
type: this.type,
id: this.url,
following: this.getFollowingUrl(),
followers: this.getFollowersUrl(),

View File

@ -51,6 +51,16 @@ export class ServerModel extends Model<ServerModel> {
})
BlockedByAccounts: ServerBlocklistModel[]
static load (id: number): Bluebird<MServer> {
const query = {
where: {
id
}
}
return ServerModel.findOne(query)
}
static loadByHost (host: string): Bluebird<MServer> {
const query = {
where: {

View File

@ -517,7 +517,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
}
toActivityPubObject (this: MChannelAP): ActivityPubActor {
const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
const obj = this.Actor.toActivityPubObject(this.name)
return Object.assign(obj, {
summary: this.description,

View File

@ -5,8 +5,16 @@ import 'mocha'
import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo,
setAccessTokensToServers, userLogin, immutableAssign, cleanupTests
cleanupTests,
createUser,
flushAndRunServer,
immutableAssign,
makeDeleteRequest,
makeGetRequest,
makePutBodyRequest,
ServerInfo,
setAccessTokensToServers,
userLogin
} from '../../../../shared/extra-utils'
describe('Test config API validators', function () {
@ -98,6 +106,17 @@ describe('Test config API validators', function () {
enabled: false,
manualApproval: true
}
},
followings: {
instance: {
autoFollowBack: {
enabled: true
},
autoFollowIndex: {
enabled: true,
indexUrl: 'https://index.example.com'
}
}
}
}

View File

@ -172,7 +172,8 @@ describe('Test user notifications API validators', function () {
commentMention: UserNotificationSettingValue.WEB,
newFollow: UserNotificationSettingValue.WEB,
newUserRegistration: UserNotificationSettingValue.WEB,
newInstanceFollower: UserNotificationSettingValue.WEB
newInstanceFollower: UserNotificationSettingValue.WEB,
autoInstanceFollowing: UserNotificationSettingValue.WEB
}
it('Should fail with missing fields', async function () {

View File

@ -16,8 +16,8 @@ import {
immutableAssign,
registerUser,
removeVideoFromBlacklist,
reportVideoAbuse,
updateCustomConfig,
reportVideoAbuse, unfollow,
updateCustomConfig, updateCustomSubConfig,
updateMyUser,
updateVideo,
updateVideoChannel,
@ -45,7 +45,8 @@ import {
getUserNotifications,
markAsReadAllNotifications,
markAsReadNotifications,
updateMyNotificationSettings
updateMyNotificationSettings,
checkAutoInstanceFollowing
} from '../../../../shared/extra-utils/users/user-notifications'
import {
User,
@ -108,7 +109,8 @@ describe('Test users notifications', function () {
commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
}
before(async function () {
@ -897,6 +899,36 @@ describe('Test users notifications', function () {
const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
})
it('Should send a notification on auto follow back', async function () {
this.timeout(40000)
await unfollow(servers[2].url, servers[2].accessToken, servers[0])
await waitJobs(servers)
const config = {
followings: {
instance: {
autoFollowBack: { enabled: true }
}
}
}
await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
await waitJobs(servers)
const followerHost = servers[0].host
const followingHost = servers[2].host
await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence')
config.followings.instance.autoFollowBack.enabled = false
await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
})
})
describe('New actor follow', function () {

View File

@ -0,0 +1,148 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import {
acceptFollower,
cleanupTests,
flushAndRunMultipleServers,
ServerInfo,
setAccessTokensToServers,
unfollow,
updateCustomSubConfig
} from '../../../../shared/extra-utils/index'
import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { ActorFollow } from '../../../../shared/models/actors'
const expect = chai.expect
async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) {
{
const res = await getFollowersListPaginationAndSort(following.url, 0, 5, '-createdAt')
const follows = res.body.data as ActorFollow[]
if (exists === true) {
expect(res.body.total).to.equal(1)
expect(follows[ 0 ].follower.host).to.equal(follower.host)
expect(follows[ 0 ].state).to.equal('accepted')
} else {
expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0)
}
}
{
const res = await getFollowingListPaginationAndSort(follower.url, 0, 5, '-createdAt')
const follows = res.body.data as ActorFollow[]
if (exists === true) {
expect(res.body.total).to.equal(1)
expect(follows[ 0 ].following.host).to.equal(following.host)
expect(follows[ 0 ].state).to.equal('accepted')
} else {
expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0)
}
}
}
async function server1Follows2 (servers: ServerInfo[]) {
await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken)
await waitJobs(servers)
}
async function resetFollows (servers: ServerInfo[]) {
try {
await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ])
await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ])
} catch { /* empty */ }
await waitJobs(servers)
await checkFollow(servers[0], servers[1], false)
await checkFollow(servers[1], servers[0], false)
}
describe('Test auto follows', function () {
let servers: ServerInfo[] = []
before(async function () {
this.timeout(30000)
servers = await flushAndRunMultipleServers(2)
// Get the access tokens
await setAccessTokensToServers(servers)
})
describe('Auto follow back', function () {
it('Should not auto follow back if the option is not enabled', async function () {
this.timeout(15000)
await server1Follows2(servers)
await checkFollow(servers[0], servers[1], true)
await checkFollow(servers[1], servers[0], false)
await resetFollows(servers)
})
it('Should auto follow back on auto accept if the option is enabled', async function () {
this.timeout(15000)
const config = {
followings: {
instance: {
autoFollowBack: { enabled: true }
}
}
}
await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
await server1Follows2(servers)
await checkFollow(servers[0], servers[1], true)
await checkFollow(servers[1], servers[0], true)
await resetFollows(servers)
})
it('Should wait the acceptation before auto follow back', async function () {
this.timeout(30000)
const config = {
followings: {
instance: {
autoFollowBack: { enabled: true }
}
},
followers: {
instance: {
manualApproval: true
}
}
}
await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
await server1Follows2(servers)
await checkFollow(servers[0], servers[1], false)
await checkFollow(servers[1], servers[0], false)
await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host)
await waitJobs(servers)
await checkFollow(servers[0], servers[1], true)
await checkFollow(servers[1], servers[0], true)
await resetFollows(servers)
})
})
after(async function () {
await cleanupTests(servers)
})
})

View File

@ -68,6 +68,10 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
expect(data.followers.instance.enabled).to.be.true
expect(data.followers.instance.manualApproval).to.be.false
expect(data.followings.instance.autoFollowBack.enabled).to.be.false
expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://instances.joinpeertube.org')
}
function checkUpdatedConfig (data: CustomConfig) {
@ -119,6 +123,10 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.followers.instance.enabled).to.be.false
expect(data.followers.instance.manualApproval).to.be.true
expect(data.followings.instance.autoFollowBack.enabled).to.be.true
expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
}
describe('Test config', function () {
@ -261,6 +269,17 @@ describe('Test config', function () {
enabled: false,
manualApproval: true
}
},
followings: {
instance: {
autoFollowBack: {
enabled: true
},
autoFollowIndex: {
enabled: true,
indexUrl: 'https://updated.example.com'
}
}
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)

View File

@ -1,3 +1,4 @@
import './auto-follows'
import './config'
import './contact-form'
import './email'

View File

@ -2,7 +2,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import {
MActor,
MActorAccount,
MActorAccountChannel,
MActorDefaultAccountChannel,
MActorChannelAccountActor,
MActorDefault,
MActorFormattable,
@ -37,8 +37,8 @@ export type MActorFollowActorsDefault = MActorFollow &
Use<'ActorFollowing', MActorDefault>
export type MActorFollowFull = MActorFollow &
Use<'ActorFollower', MActorAccountChannel> &
Use<'ActorFollowing', MActorAccountChannel>
Use<'ActorFollower', MActorDefaultAccountChannel> &
Use<'ActorFollowing', MActorDefaultAccountChannel>
// ############################################################################
@ -51,10 +51,6 @@ export type MActorFollowActorsDefaultSubscription = MActorFollow &
Use<'ActorFollower', MActorDefault> &
Use<'ActorFollowing', SubscriptionFollowing>
export type MActorFollowFollowingFullFollowerAccount = MActorFollow &
Use<'ActorFollower', MActorAccount> &
Use<'ActorFollowing', MActorAccountChannel>
export type MActorFollowSubscriptions = MActorFollow &
Use<'ActorFollowing', MActorChannelAccountActor>

View File

@ -58,7 +58,7 @@ export type MActorAccount = MActor &
export type MActorChannel = MActor &
Use<'VideoChannel', MChannel>
export type MActorAccountChannel = MActorAccount & MActorChannel
export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel
export type MActorServer = MActor &
Use<'Server', MServer>

View File

@ -1,5 +1,5 @@
import { UserNotificationModel } from '../../../models/account/user-notification'
import { PickWith } from '../../utils'
import { PickWith, PickWithOpt } from '../../utils'
import { VideoModel } from '../../../models/video/video'
import { ActorModel } from '../../../models/activitypub/actor'
import { ServerModel } from '../../../models/server/server'
@ -48,12 +48,13 @@ export namespace UserNotificationIncludes {
export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
PickWith<ActorModel, 'Account', AccountInclude> &
PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
export type ActorFollowing = Pick<ActorModel, 'preferredUsername'> &
export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> &
PickWith<ActorModel, 'Account', AccountInclude>
PickWith<ActorModel, 'Account', AccountInclude> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> &
PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &

View File

@ -13,6 +13,9 @@ export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'>
// ############################################################################
export type MVideoBlacklistLightVideo = MVideoBlacklistLight &
Use<'Video', MVideo>
export type MVideoBlacklistVideo = MVideoBlacklist &
Use<'Video', MVideo>

View File

@ -11,3 +11,12 @@ export type PickWith<T, KT extends keyof T, V> = {
export type PickWithOpt<T, KT extends keyof T, V> = {
[P in KT]?: T[P] extends V ? V : never
}
// https://github.com/krzkaczor/ts-essentials Rocks!
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: DeepPartial<T[P]>
};

View File

@ -1,5 +1,7 @@
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
import { CustomConfig } from '../../models/server/custom-config.model'
import { DeepPartial } from '@server/typings/utils'
import { merge } from 'lodash'
function getConfig (url: string) {
const path = '/api/v1/config'
@ -44,7 +46,7 @@ function updateCustomConfig (url: string, token: string, newCustomConfig: Custom
})
}
function updateCustomSubConfig (url: string, token: string, newConfig: any) {
function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
const updateParams: CustomConfig = {
instance: {
name: 'PeerTube updated',
@ -130,10 +132,21 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
enabled: true,
manualApproval: false
}
},
followings: {
instance: {
autoFollowBack: {
enabled: false
},
autoFollowIndex: {
indexUrl: 'https://instances.joinpeertube.org',
enabled: false
}
}
}
}
Object.assign(updateParams, newConfig)
merge(updateParams, newConfig)
return updateCustomConfig(url, token, updateParams)
}

View File

@ -279,8 +279,9 @@ async function checkNewActorFollow (
expect(notification.actorFollow.follower.name).to.equal(followerName)
expect(notification.actorFollow.follower.host).to.not.be.undefined
expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
expect(notification.actorFollow.following.type).to.equal(followType)
const following = notification.actorFollow.following
expect(following.displayName).to.equal(followingDisplayName)
expect(following.type).to.equal(followType)
} else {
expect(notification).to.satisfy(n => {
return n.type !== notificationType ||
@ -327,6 +328,37 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
await checkNotification(base, notificationChecker, emailFinder, type)
}
async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
function notificationChecker (notification: UserNotification, type: CheckerType) {
if (type === 'presence') {
expect(notification).to.not.be.undefined
expect(notification.type).to.equal(notificationType)
const following = notification.actorFollow.following
checkActor(following)
expect(following.name).to.equal('peertube')
expect(following.host).to.equal(followingHost)
expect(notification.actorFollow.follower.name).to.equal('peertube')
expect(notification.actorFollow.follower.host).to.equal(followerHost)
} else {
expect(notification).to.satisfy(n => {
return n.type !== notificationType || n.actorFollow.following.host !== followingHost
})
}
}
function emailFinder (email: object) {
const text: string = email[ 'text' ]
return text.includes(' automatically followed a new instance') && text.includes(followingHost)
}
await checkNotification(base, notificationChecker, emailFinder, type)
}
async function checkCommentMention (
base: CheckerBaseParams,
uuid: string,
@ -427,8 +459,8 @@ async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, vi
expect(notification).to.not.be.undefined
expect(notification.type).to.equal(notificationType)
expect(notification.video.id).to.be.a('number')
checkVideo(notification.video, videoName, videoUUID)
expect(notification.videoBlacklist.video.id).to.be.a('number')
checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
} else {
expect(notification).to.satisfy((n: UserNotification) => {
return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
@ -480,6 +512,7 @@ export {
markAsReadAllNotifications,
checkMyVideoImportIsFinished,
checkUserRegistered,
checkAutoInstanceFollowing,
checkVideoIsPublished,
checkNewVideoFromSubscription,
checkNewActorFollow,

View File

@ -99,4 +99,16 @@ export interface CustomConfig {
}
}
followings: {
instance: {
autoFollowBack: {
enabled: boolean
}
autoFollowIndex: {
enabled: boolean
indexUrl: string
}
}
}
}

View File

@ -16,4 +16,5 @@ export interface UserNotificationSetting {
newFollow: UserNotificationSettingValue
commentMention: UserNotificationSettingValue
newInstanceFollower: UserNotificationSettingValue
autoInstanceFollowing: UserNotificationSettingValue
}

View File

@ -19,7 +19,9 @@ export enum UserNotificationType {
VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12,
NEW_INSTANCE_FOLLOWER = 13
NEW_INSTANCE_FOLLOWER = 13,
AUTO_INSTANCE_FOLLOWING = 14
}
export interface VideoInfo {
@ -78,10 +80,12 @@ export interface UserNotification {
id: number
follower: ActorInfo
state: FollowState
following: {
type: 'account' | 'channel'
type: 'account' | 'channel' | 'instance'
name: string
displayName: string
host: string
}
}

View File

@ -17,8 +17,7 @@
"typeRoots": [ "node_modules/@types", "server/typings" ],
"baseUrl": "./",
"paths": {
"@server/typings/*": [ "server/typings/*" ],
"@server/models/*": [ "server/models/*" ]
"@server/*": [ "server/*" ]
}
},
"exclude": [

View File

@ -4610,6 +4610,11 @@ mocha@^6.0.0:
yargs-parser "13.0.0"
yargs-unparser "1.5.0"
module-alias@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.1.tgz#553aea9dc7f99cd45fd75e34a574960dc46550da"
integrity sha512-LTez0Eo+YtfUhgzhu/LqxkUzOpD+k5C0wXBLun0L1qE2BhHf6l09dqam8e7BnoMYA6mAlP0vSsGFQ8QHhGN/aQ==
moment-timezone@^0.5.21, moment-timezone@^0.5.25:
version "0.5.26"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772"