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:
# 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'
# 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'
"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",
"scripty": {
"silent": true
},
- "sasslintConfig": "client/.sass-lint.yml"
+ "sasslintConfig": "client/.sass-lint.yml",
+ "_moduleAliases": {
+ "@server": "dist/server"
+ }
}
+require('module-alias/register')
+
// FIXME: https://github.com/nodejs/node/pull/16853
import { PluginManager } from './server/lib/plugins/plugin-manager'
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
+ }
+ }
}
}
}
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',
follow.state = 'accepted'
await follow.save()
+ await autoFollowBackIfNeeded(follow)
+
return res.status(204).end()
}
newFollow: body.newFollow,
newUserRegistration: body.newUserRegistration,
commentMention: body.commentMention,
- newInstanceFollower: body.newInstanceFollower
+ newInstanceFollower: body.newInstanceFollower,
+ autoInstanceFollowing: body.autoInstanceFollowing
}
await UserNotificationSettingModel.update(values, query)
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') }
}
--- /dev/null
+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
+}
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)
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
// ---------------------------------------------------------------------------
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')
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) {
+ const follower = await ActorModel.loadFull(byActor.id)
+ const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
+
if (isFollowingInstance) {
- Notifier.Instance.notifyOfNewInstanceFollow(actorFollow)
+ Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
} else {
- const actorFollowFull = actorFollow as MActorFollowFull
- actorFollowFull.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as MAccount
-
Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
}
}
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'
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 = {
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()
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()
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.` +
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) {
const fromActor = await ActorModel.load(payload.followerActorId)
- return retryTransactionWrapper(follow, fromActor, targetActor)
+ return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-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.')
}
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
- })
+ 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
}
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'
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)
}
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 {
.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 {
.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 }))
}
.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(
})
}
- 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
}
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
}
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
}
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?
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
}
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
}
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
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 })
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
}
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
}
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
}
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
}
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
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
}
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[] = []
newUserRegistration: UserNotificationSettingValue.WEB,
commentMention: UserNotificationSettingValue.WEB,
newFollow: UserNotificationSettingValue.WEB,
- newInstanceFollower: UserNotificationSettingValue.WEB
+ newInstanceFollower: UserNotificationSettingValue.WEB,
+ autoInstanceFollowing: UserNotificationSettingValue.WEB
}
return UserNotificationSettingModel.create(values, { transaction: t })
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,
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
},
})
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)
.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 })
}
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
@Column
newInstanceFollower: UserNotificationSettingValue
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingNewInstanceFollower',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
+ )
+ @Column
+ autoInstanceFollowing: UserNotificationSettingValue
+
@AllowNull(false)
@Default(null)
@Is(
newUserRegistration: this.newUserRegistration,
commentMention: this.commentMention,
newFollow: this.newFollow,
- newInstanceFollower: this.newInstanceFollower
+ newInstanceFollower: this.newInstanceFollower,
+ autoInstanceFollowing: this.autoInstanceFollowing
}
}
}
]
},
{
- 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
+ }
]
}
]
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,
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
MActorFormattable,
MActorFull,
MActorHost,
- MActorRedundancyAllowedOpt,
MActorServer,
MActorSummaryFormattable
} from '../../typings/models'
})
}
- 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) {
}
const json = {
- type: activityPubType,
+ type: this.type,
id: this.url,
following: this.getFollowingUrl(),
followers: this.getFollowersUrl(),
})
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: {
}
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,
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 () {
enabled: false,
manualApproval: true
}
+ },
+ followings: {
+ instance: {
+ autoFollowBack: {
+ enabled: true
+ },
+ autoFollowIndex: {
+ enabled: true,
+ indexUrl: 'https://index.example.com'
+ }
+ }
}
}
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 () {
immutableAssign,
registerUser,
removeVideoFromBlacklist,
- reportVideoAbuse,
- updateCustomConfig,
+ reportVideoAbuse, unfollow,
+ updateCustomConfig, updateCustomSubConfig,
updateMyUser,
updateVideo,
updateVideoChannel,
getUserNotifications,
markAsReadAllNotifications,
markAsReadNotifications,
- updateMyNotificationSettings
+ updateMyNotificationSettings,
+ checkAutoInstanceFollowing
} from '../../../../shared/extra-utils/users/user-notifications'
import {
User,
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 () {
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 () {
--- /dev/null
+/* 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)
+ })
+})
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) {
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 () {
enabled: false,
manualApproval: true
}
+ },
+ followings: {
+ instance: {
+ autoFollowBack: {
+ enabled: true
+ },
+ autoFollowIndex: {
+ enabled: true,
+ indexUrl: 'https://updated.example.com'
+ }
+ }
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
+import './auto-follows'
import './config'
import './contact-form'
import './email'
import {
MActor,
MActorAccount,
- MActorAccountChannel,
+ MActorDefaultAccountChannel,
MActorChannelAccountActor,
MActorDefault,
MActorFormattable,
Use<'ActorFollowing', MActorDefault>
export type MActorFollowFull = MActorFollow &
- Use<'ActorFollower', MActorAccountChannel> &
- Use<'ActorFollowing', MActorAccountChannel>
+ Use<'ActorFollower', MActorDefaultAccountChannel> &
+ Use<'ActorFollowing', MActorDefaultAccountChannel>
// ############################################################################
Use<'ActorFollower', MActorDefault> &
Use<'ActorFollowing', SubscriptionFollowing>
-export type MActorFollowFollowingFullFollowerAccount = MActorFollow &
- Use<'ActorFollower', MActorAccount> &
- Use<'ActorFollowing', MActorAccountChannel>
-
export type MActorFollowSubscriptions = MActorFollow &
Use<'ActorFollowing', MActorChannelAccountActor>
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>
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'
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> &
// ############################################################################
+export type MVideoBlacklistLightVideo = MVideoBlacklistLight &
+ Use<'Video', MVideo>
+
export type MVideoBlacklistVideo = MVideoBlacklist &
Use<'Video', MVideo>
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]>
+};
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'
})
}
-function updateCustomSubConfig (url: string, token: string, newConfig: any) {
+function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
const updateParams: CustomConfig = {
instance: {
name: 'PeerTube updated',
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)
}
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 ||
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,
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
markAsReadAllNotifications,
checkMyVideoImportIsFinished,
checkUserRegistered,
+ checkAutoInstanceFollowing,
checkVideoIsPublished,
checkNewVideoFromSubscription,
checkNewActorFollow,
}
}
+ followings: {
+ instance: {
+ autoFollowBack: {
+ enabled: boolean
+ }
+
+ autoFollowIndex: {
+ enabled: boolean
+ indexUrl: string
+ }
+ }
+ }
}
newFollow: UserNotificationSettingValue
commentMention: UserNotificationSettingValue
newInstanceFollower: UserNotificationSettingValue
+ autoInstanceFollowing: UserNotificationSettingValue
}
VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12,
- NEW_INSTANCE_FOLLOWER = 13
+ NEW_INSTANCE_FOLLOWER = 13,
+
+ AUTO_INSTANCE_FOLLOWING = 14
}
export interface VideoInfo {
id: number
follower: ActorInfo
state: FollowState
+
following: {
- type: 'account' | 'channel'
+ type: 'account' | 'channel' | 'instance'
name: string
displayName: string
+ host: string
}
}
"typeRoots": [ "node_modules/@types", "server/typings" ],
"baseUrl": "./",
"paths": {
- "@server/typings/*": [ "server/typings/*" ],
- "@server/models/*": [ "server/models/*" ]
+ "@server/*": [ "server/*" ]
}
},
"exclude": [
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"