Browse Source

Add auto follow back support for instances

Chocobozzz 2 months ago
parent
commit
8424c4026a
44 changed files with 651 additions and 156 deletions
  1. 1
    1
      client/src/app/shared/users/user-notification.model.ts
  2. 16
    0
      config/default.yaml
  3. 15
    0
      config/production.yaml.example
  4. 5
    1
      package.json
  5. 2
    0
      server.ts
  6. 12
    0
      server/controllers/api/config.ts
  7. 3
    0
      server/controllers/api/server/follows.ts
  8. 2
    1
      server/controllers/api/users/my-notifications.ts
  9. 17
    0
      server/initializers/config.ts
  10. 36
    0
      server/lib/activitypub/follow.ts
  11. 1
    1
      server/lib/activitypub/process/process-accept.ts
  12. 12
    8
      server/lib/activitypub/process/process-follow.ts
  13. 0
    1
      server/lib/activitypub/send/send-follow.ts
  14. 28
    5
      server/lib/emailer.ts
  15. 13
    11
      server/lib/job-queue/handlers/activitypub-follow.ts
  16. 4
    1
      server/lib/job-queue/handlers/video-import.ts
  17. 109
    75
      server/lib/notifier.ts
  18. 2
    1
      server/lib/user.ts
  19. 5
    3
      server/lib/video-blacklist.ts
  20. 2
    0
      server/middlewares/validators/user-notifications.ts
  21. 1
    1
      server/models/account/account.ts
  22. 11
    1
      server/models/account/user-notification-setting.ts
  23. 15
    4
      server/models/account/user-notification.ts
  24. 2
    10
      server/models/activitypub/actor.ts
  25. 10
    0
      server/models/server/server.ts
  26. 1
    1
      server/models/video/video-channel.ts
  27. 21
    2
      server/tests/api/check-params/config.ts
  28. 2
    1
      server/tests/api/check-params/user-notifications.ts
  29. 36
    4
      server/tests/api/notifications/user-notifications.ts
  30. 148
    0
      server/tests/api/server/auto-follows.ts
  31. 19
    0
      server/tests/api/server/config.ts
  32. 1
    0
      server/tests/api/server/index.ts
  33. 3
    7
      server/typings/models/account/actor-follow.ts
  34. 1
    1
      server/typings/models/account/actor.ts
  35. 6
    5
      server/typings/models/user/user-notification.ts
  36. 3
    0
      server/typings/models/video/video-blacklist.ts
  37. 9
    0
      server/typings/utils.ts
  38. 15
    2
      shared/extra-utils/server/config.ts
  39. 37
    4
      shared/extra-utils/users/user-notifications.ts
  40. 12
    0
      shared/models/server/custom-config.model.ts
  41. 1
    0
      shared/models/users/user-notification-setting.model.ts
  42. 6
    2
      shared/models/users/user-notification.model.ts
  43. 1
    2
      tsconfig.json
  44. 5
    0
      yarn.lock

+ 1
- 1
client/src/app/shared/users/user-notification.model.ts View File

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

+ 16
- 0
config/default.yaml View File

@@ -273,5 +273,21 @@ followers:
273 273
     # Whether or not an administrator must manually validate a new follower
274 274
     manual_approval: false
275 275
 
276
+followings:
277
+  instance:
278
+    # If you want to automatically follow back new instance followers
279
+    # Only follows accepted followers (in case you enabled manual followers approbation)
280
+    # If this option is enabled, use the mute feature instead of deleting followings
281
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
282
+    auto_follow_back:
283
+      enabled: false
284
+
285
+    # If you want to automatically follow instances of the public index
286
+    # If this option is enabled, use the mute feature instead of deleting followings
287
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
288
+    auto_follow_index:
289
+      enabled: false
290
+      index_url: 'https://instances.joinpeertube.org'
291
+
276 292
 theme:
277 293
   default: 'default'

+ 15
- 0
config/production.yaml.example View File

@@ -288,5 +288,20 @@ followers:
288 288
     # Whether or not an administrator must manually validate a new follower
289 289
     manual_approval: false
290 290
 
291
+followings:
292
+  instance:
293
+    # If you want to automatically follow back new instance followers
294
+    # If this option is enabled, use the mute feature instead of deleting followings
295
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
296
+    auto_follow_back:
297
+      enabled: false
298
+
299
+    # If you want to automatically follow instances of the public index
300
+    # If this option is enabled, use the mute feature instead of deleting followings
301
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
302
+    auto_follow_index:
303
+      enabled: false
304
+      index_url: 'https://instances.joinpeertube.org'
305
+
291 306
 theme:
292 307
   default: 'default'

+ 5
- 1
package.json View File

@@ -132,6 +132,7 @@
132 132
     "lru-cache": "^5.1.1",
133 133
     "magnet-uri": "^5.1.4",
134 134
     "memoizee": "^0.4.14",
135
+    "module-alias": "^2.2.1",
135 136
     "morgan": "^1.5.3",
136 137
     "multer": "^1.1.0",
137 138
     "nodemailer": "^6.0.0",
@@ -224,5 +225,8 @@
224 225
   "scripty": {
225 226
     "silent": true
226 227
   },
227
-  "sasslintConfig": "client/.sass-lint.yml"
228
+  "sasslintConfig": "client/.sass-lint.yml",
229
+  "_moduleAliases": {
230
+    "@server": "dist/server"
231
+  }
228 232
 }

+ 2
- 0
server.ts View File

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

+ 12
- 0
server/controllers/api/config.ts View File

@@ -300,6 +300,18 @@ function customConfig (): CustomConfig {
300 300
         enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
301 301
         manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
302 302
       }
303
+    },
304
+    followings: {
305
+      instance: {
306
+        autoFollowBack: {
307
+          enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
308
+        },
309
+
310
+        autoFollowIndex: {
311
+          enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
312
+          indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
313
+        }
314
+      }
303 315
     }
304 316
   }
305 317
 }

+ 3
- 0
server/controllers/api/server/follows.ts View File

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

+ 2
- 1
server/controllers/api/users/my-notifications.ts View File

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

+ 17
- 0
server/initializers/config.ts View File

@@ -232,6 +232,23 @@ const CONFIG = {
232 232
       get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
233 233
     }
234 234
   },
235
+  FOLLOWINGS: {
236
+    INSTANCE: {
237
+      AUTO_FOLLOW_BACK: {
238
+        get ENABLED () {
239
+          return config.get<boolean>('followings.instance.auto_follow_back.enabled')
240
+        }
241
+      },
242
+      AUTO_FOLLOW_INDEX: {
243
+        get ENABLED () {
244
+          return config.get<boolean>('followings.instance.auto_follow_index.enabled')
245
+        },
246
+        get INDEX_URL () {
247
+          return config.get<string>('followings.instance.auto_follow_index.index_url')
248
+        }
249
+      }
250
+    }
251
+  },
235 252
   THEME: {
236 253
     get DEFAULT () { return config.get<string>('theme.default') }
237 254
   }

+ 36
- 0
server/lib/activitypub/follow.ts View File

@@ -0,0 +1,36 @@
1
+import { MActorFollowActors } from '../../typings/models'
2
+import { CONFIG } from '../../initializers/config'
3
+import { SERVER_ACTOR_NAME } from '../../initializers/constants'
4
+import { JobQueue } from '../job-queue'
5
+import { logger } from '../../helpers/logger'
6
+import { getServerActor } from '../../helpers/utils'
7
+import { ServerModel } from '@server/models/server/server'
8
+
9
+async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
10
+  if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
11
+
12
+  const follower = actorFollow.ActorFollower
13
+
14
+  if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) {
15
+    logger.info('Auto follow back %s.', follower.url)
16
+
17
+    const me = await getServerActor()
18
+
19
+    const server = await ServerModel.load(follower.serverId)
20
+    const host = server.host
21
+
22
+    const payload = {
23
+      host,
24
+      name: SERVER_ACTOR_NAME,
25
+      followerActorId: me.id,
26
+      isAutoFollow: true
27
+    }
28
+
29
+    JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
30
+            .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
31
+  }
32
+}
33
+
34
+export {
35
+  autoFollowBackIfNeeded
36
+}

+ 1
- 1
server/lib/activitypub/process/process-accept.ts View File

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

+ 12
- 8
server/lib/activitypub/process/process-follow.ts View File

@@ -10,7 +10,8 @@ import { getAPId } from '../../../helpers/activitypub'
10 10
 import { getServerActor } from '../../../helpers/utils'
11 11
 import { CONFIG } from '../../../initializers/config'
12 12
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
13
-import { MAccount, MActorFollowActors, MActorFollowFull, MActorSignature } from '../../../typings/models'
13
+import { MActorFollowActors, MActorSignature } from '../../../typings/models'
14
+import { autoFollowBackIfNeeded } from '../follow'
14 15
 
15 16
 async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
16 17
   const { activity, byActor } = options
@@ -28,7 +29,7 @@ export {
28 29
 // ---------------------------------------------------------------------------
29 30
 
30 31
 async function processFollow (byActor: MActorSignature, targetActorURL: string) {
31
-  const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => {
32
+  const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => {
32 33
     const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
33 34
 
34 35
     if (!targetActor) throw new Error('Unknown actor')
@@ -67,21 +68,24 @@ async function processFollow (byActor: MActorSignature, targetActorURL: string)
67 68
     actorFollow.ActorFollowing = targetActor
68 69
 
69 70
     // Target sends to actor he accepted the follow request
70
-    if (actorFollow.state === 'accepted') await sendAccept(actorFollow)
71
+    if (actorFollow.state === 'accepted') {
72
+      await sendAccept(actorFollow)
73
+      await autoFollowBackIfNeeded(actorFollow)
74
+    }
71 75
 
72
-    return { actorFollow, created, isFollowingInstance }
76
+    return { actorFollow, created, isFollowingInstance, targetActor }
73 77
   })
74 78
 
75 79
   // Rejected
76 80
   if (!actorFollow) return
77 81
 
78 82
   if (created) {
83
+    const follower = await ActorModel.loadFull(byActor.id)
84
+    const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
85
+
79 86
     if (isFollowingInstance) {
80
-      Notifier.Instance.notifyOfNewInstanceFollow(actorFollow)
87
+      Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
81 88
     } else {
82
-      const actorFollowFull = actorFollow as MActorFollowFull
83
-      actorFollowFull.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as MAccount
84
-
85 89
       Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
86 90
     }
87 91
   }

+ 0
- 1
server/lib/activitypub/send/send-follow.ts View File

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

+ 28
- 5
server/lib/emailer.ts View File

@@ -6,8 +6,15 @@ import { JobQueue } from './job-queue'
6 6
 import { EmailPayload } from './job-queue/handlers/email'
7 7
 import { readFileSync } from 'fs-extra'
8 8
 import { WEBSERVER } from '../initializers/constants'
9
-import { MCommentOwnerVideo, MVideo, MVideoAbuseVideo, MVideoAccountLight, MVideoBlacklistVideo } from '../typings/models/video'
10
-import { MActorFollowActors, MActorFollowFollowingFullFollowerAccount, MUser } from '../typings/models'
9
+import {
10
+  MCommentOwnerVideo,
11
+  MVideo,
12
+  MVideoAbuseVideo,
13
+  MVideoAccountLight,
14
+  MVideoBlacklistLightVideo,
15
+  MVideoBlacklistVideo
16
+} from '../typings/models/video'
17
+import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
11 18
 import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
12 19
 
13 20
 type SendEmailOptions = {
@@ -107,7 +114,7 @@ class Emailer {
107 114
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
108 115
   }
109 116
 
110
-  addNewFollowNotification (to: string[], actorFollow: MActorFollowFollowingFullFollowerAccount, followType: 'account' | 'channel') {
117
+  addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
111 118
     const followerName = actorFollow.ActorFollower.Account.getDisplayName()
112 119
     const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
113 120
 
@@ -144,6 +151,22 @@ class Emailer {
144 151
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
145 152
   }
146 153
 
154
+  addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155
+    const text = `Hi dear admin,\n\n` +
156
+      `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
157
+      `\n\n` +
158
+      `Cheers,\n` +
159
+      `${CONFIG.EMAIL.BODY.SIGNATURE}`
160
+
161
+    const emailPayload: EmailPayload = {
162
+      to,
163
+      subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
164
+      text
165
+    }
166
+
167
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
168
+  }
169
+
147 170
   myVideoPublishedNotification (to: string[], video: MVideo) {
148 171
     const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
149 172
 
@@ -265,9 +288,9 @@ class Emailer {
265 288
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
266 289
   }
267 290
 
268
-  addVideoAutoBlacklistModeratorsNotification (to: string[], video: MVideo) {
291
+  addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
269 292
     const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
270
-    const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
293
+    const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
271 294
 
272 295
     const text = `Hi,\n\n` +
273 296
       `A recently added video was auto-blacklisted and requires moderator review before publishing.` +

+ 13
- 11
server/lib/job-queue/handlers/activitypub-follow.ts View File

@@ -10,12 +10,13 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
10 10
 import { ActorModel } from '../../../models/activitypub/actor'
11 11
 import { Notifier } from '../../notifier'
12 12
 import { sequelizeTypescript } from '../../../initializers/database'
13
-import { MAccount, MActor, MActorFollowActors, MActorFollowFull, MActorFull } from '../../../typings/models'
13
+import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
14 14
 
15 15
 export type ActivitypubFollowPayload = {
16 16
   followerActorId: number
17 17
   name: string
18 18
   host: string
19
+  isAutoFollow?: boolean
19 20
 }
20 21
 
21 22
 async function processActivityPubFollow (job: Bull.Job) {
@@ -35,7 +36,7 @@ async function processActivityPubFollow (job: Bull.Job) {
35 36
 
36 37
   const fromActor = await ActorModel.load(payload.followerActorId)
37 38
 
38
-  return retryTransactionWrapper(follow, fromActor, targetActor)
39
+  return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
39 40
 }
40 41
 // ---------------------------------------------------------------------------
41 42
 
@@ -45,7 +46,7 @@ export {
45 46
 
46 47
 // ---------------------------------------------------------------------------
47 48
 
48
-async function follow (fromActor: MActor, targetActor: MActorFull) {
49
+async function follow (fromActor: MActor, targetActor: MActorFull, isAutoFollow = false) {
49 50
   if (fromActor.id === targetActor.id) {
50 51
     throw new Error('Follower is the same than target actor.')
51 52
   }
@@ -75,14 +76,15 @@ async function follow (fromActor: MActor, targetActor: MActorFull) {
75 76
     return actorFollow
76 77
   })
77 78
 
78
-  if (actorFollow.state === 'accepted') {
79
-    const followerFull = Object.assign(fromActor, { Account: await actorFollow.ActorFollower.$get('Account') as MAccount })
79
+  const followerFull = await ActorModel.loadFull(fromActor.id)
80 80
 
81
-    const actorFollowFull = Object.assign(actorFollow, {
82
-      ActorFollowing: targetActor,
83
-      ActorFollower: followerFull
84
-    })
81
+  const actorFollowFull = Object.assign(actorFollow, {
82
+    ActorFollowing: targetActor,
83
+    ActorFollower: followerFull
84
+  })
85 85
 
86
-    Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
87
-  }
86
+  if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
87
+  if (isAutoFollow === true) Notifier.Instance.notifyOfAutoInstanceFollowing(actorFollowFull)
88
+
89
+  return actorFollow
88 90
 }

+ 4
- 1
server/lib/job-queue/handlers/video-import.ts View File

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

+ 109
- 75
server/lib/notifier.ts View File

@@ -1,30 +1,30 @@
1 1
 import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
2 2
 import { logger } from '../helpers/logger'
3
-import { VideoModel } from '../models/video/video'
4 3
 import { Emailer } from './emailer'
5 4
 import { UserNotificationModel } from '../models/account/user-notification'
6
-import { VideoCommentModel } from '../models/video/video-comment'
7 5
 import { UserModel } from '../models/account/user'
8 6
 import { PeerTubeSocket } from './peertube-socket'
9 7
 import { CONFIG } from '../initializers/config'
10 8
 import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11
-import { VideoBlacklistModel } from '../models/video/video-blacklist'
12 9
 import * as Bluebird from 'bluebird'
13
-import { VideoImportModel } from '../models/video/video-import'
14 10
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
15 11
 import {
16 12
   MCommentOwnerVideo,
17
-  MVideo,
18 13
   MVideoAbuseVideo,
19 14
   MVideoAccountLight,
15
+  MVideoBlacklistLightVideo,
20 16
   MVideoBlacklistVideo,
21 17
   MVideoFullLight
22 18
 } from '../typings/models/video'
23
-import { MUser, MUserAccount, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/typings/models/user'
24
-import { MActorFollowActors, MActorFollowFull, MActorFollowFollowingFullFollowerAccount } from '../typings/models'
25
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
19
+import {
20
+  MUser,
21
+  MUserDefault,
22
+  MUserNotifSettingAccount,
23
+  MUserWithNotificationSetting,
24
+  UserNotificationModelForApi
25
+} from '@server/typings/models/user'
26
+import { MActorFollowFull } from '../typings/models'
26 27
 import { MVideoImportVideo } from '@server/typings/models/video/video-import'
27
-import { AccountModel } from '@server/models/account/account'
28 28
 
29 29
 class Notifier {
30 30
 
@@ -77,9 +77,9 @@ class Notifier {
77 77
       .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
78 78
   }
79 79
 
80
-  notifyOnVideoAutoBlacklist (video: MVideo): void {
81
-    this.notifyModeratorsOfVideoAutoBlacklist(video)
82
-      .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
80
+  notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
81
+    this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
82
+      .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
83 83
   }
84 84
 
85 85
   notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
@@ -87,7 +87,7 @@ class Notifier {
87 87
       .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
88 88
   }
89 89
 
90
-  notifyOnVideoUnblacklist (video: MVideo): void {
90
+  notifyOnVideoUnblacklist (video: MVideoFullLight): void {
91 91
     this.notifyVideoOwnerOfUnblacklist(video)
92 92
         .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
93 93
   }
@@ -97,12 +97,12 @@ class Notifier {
97 97
       .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
98 98
   }
99 99
 
100
-  notifyOnNewUserRegistration (user: MUserAccount): void {
100
+  notifyOnNewUserRegistration (user: MUserDefault): void {
101 101
     this.notifyModeratorsOfNewUserRegistration(user)
102 102
         .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
103 103
   }
104 104
 
105
-  notifyOfNewUserFollow (actorFollow: MActorFollowFollowingFullFollowerAccount): void {
105
+  notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
106 106
     this.notifyUserOfNewActorFollow(actorFollow)
107 107
       .catch(err => {
108 108
         logger.error(
@@ -114,30 +114,37 @@ class Notifier {
114 114
       })
115 115
   }
116 116
 
117
-  notifyOfNewInstanceFollow (actorFollow: MActorFollowActors): void {
117
+  notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
118 118
     this.notifyAdminsOfNewInstanceFollow(actorFollow)
119 119
         .catch(err => {
120 120
           logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
121 121
         })
122 122
   }
123 123
 
124
+  notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
125
+    this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
126
+        .catch(err => {
127
+          logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
128
+        })
129
+  }
130
+
124 131
   private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
125 132
     // List all followers that are users
126 133
     const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
127 134
 
128 135
     logger.info('Notifying %d users of new video %s.', users.length, video.url)
129 136
 
130
-    function settingGetter (user: UserModel) {
137
+    function settingGetter (user: MUserWithNotificationSetting) {
131 138
       return user.NotificationSetting.newVideoFromSubscription
132 139
     }
133 140
 
134
-    async function notificationCreator (user: UserModel) {
135
-      const notification = await UserNotificationModel.create({
141
+    async function notificationCreator (user: MUserWithNotificationSetting) {
142
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
136 143
         type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
137 144
         userId: user.id,
138 145
         videoId: video.id
139 146
       })
140
-      notification.Video = video as VideoModel
147
+      notification.Video = video
141 148
 
142 149
       return notification
143 150
     }
@@ -162,17 +169,17 @@ class Notifier {
162 169
 
163 170
     logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
164 171
 
165
-    function settingGetter (user: UserModel) {
172
+    function settingGetter (user: MUserWithNotificationSetting) {
166 173
       return user.NotificationSetting.newCommentOnMyVideo
167 174
     }
168 175
 
169
-    async function notificationCreator (user: UserModel) {
170
-      const notification = await UserNotificationModel.create({
176
+    async function notificationCreator (user: MUserWithNotificationSetting) {
177
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
171 178
         type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
172 179
         userId: user.id,
173 180
         commentId: comment.id
174 181
       })
175
-      notification.Comment = comment as VideoCommentModel
182
+      notification.Comment = comment
176 183
 
177 184
       return notification
178 185
     }
@@ -207,19 +214,19 @@ class Notifier {
207 214
 
208 215
     logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
209 216
 
210
-    function settingGetter (user: UserModel) {
217
+    function settingGetter (user: MUserNotifSettingAccount) {
211 218
       if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE
212 219
 
213 220
       return user.NotificationSetting.commentMention
214 221
     }
215 222
 
216
-    async function notificationCreator (user: UserModel) {
217
-      const notification = await UserNotificationModel.create({
223
+    async function notificationCreator (user: MUserNotifSettingAccount) {
224
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
218 225
         type: UserNotificationType.COMMENT_MENTION,
219 226
         userId: user.id,
220 227
         commentId: comment.id
221 228
       })
222
-      notification.Comment = comment as VideoCommentModel
229
+      notification.Comment = comment
223 230
 
224 231
       return notification
225 232
     }
@@ -231,7 +238,7 @@ class Notifier {
231 238
     return this.notify({ users, settingGetter, notificationCreator, emailSender })
232 239
   }
233 240
 
234
-  private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFollowingFullFollowerAccount) {
241
+  private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
235 242
     if (actorFollow.ActorFollowing.isOwned() === false) return
236 243
 
237 244
     // Account follows one of our account?
@@ -253,17 +260,17 @@ class Notifier {
253 260
 
254 261
     logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
255 262
 
256
-    function settingGetter (user: UserModel) {
263
+    function settingGetter (user: MUserWithNotificationSetting) {
257 264
       return user.NotificationSetting.newFollow
258 265
     }
259 266
 
260
-    async function notificationCreator (user: UserModel) {
261
-      const notification = await UserNotificationModel.create({
267
+    async function notificationCreator (user: MUserWithNotificationSetting) {
268
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
262 269
         type: UserNotificationType.NEW_FOLLOW,
263 270
         userId: user.id,
264 271
         actorFollowId: actorFollow.id
265 272
       })
266
-      notification.ActorFollow = actorFollow as ActorFollowModel
273
+      notification.ActorFollow = actorFollow
267 274
 
268 275
       return notification
269 276
     }
@@ -275,22 +282,22 @@ class Notifier {
275 282
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
276 283
   }
277 284
 
278
-  private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowActors) {
285
+  private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
279 286
     const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
280 287
 
281 288
     logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
282 289
 
283
-    function settingGetter (user: UserModel) {
290
+    function settingGetter (user: MUserWithNotificationSetting) {
284 291
       return user.NotificationSetting.newInstanceFollower
285 292
     }
286 293
 
287
-    async function notificationCreator (user: UserModel) {
288
-      const notification = await UserNotificationModel.create({
294
+    async function notificationCreator (user: MUserWithNotificationSetting) {
295
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
289 296
         type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
290 297
         userId: user.id,
291 298
         actorFollowId: actorFollow.id
292 299
       })
293
-      notification.ActorFollow = actorFollow as ActorFollowModel
300
+      notification.ActorFollow = actorFollow
294 301
 
295 302
       return notification
296 303
     }
@@ -302,18 +309,45 @@ class Notifier {
302 309
     return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
303 310
   }
304 311
 
312
+  private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
313
+    const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
314
+
315
+    logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
316
+
317
+    function settingGetter (user: MUserWithNotificationSetting) {
318
+      return user.NotificationSetting.autoInstanceFollowing
319
+    }
320
+
321
+    async function notificationCreator (user: MUserWithNotificationSetting) {
322
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
323
+        type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
324
+        userId: user.id,
325
+        actorFollowId: actorFollow.id
326
+      })
327
+      notification.ActorFollow = actorFollow
328
+
329
+      return notification
330
+    }
331
+
332
+    function emailSender (emails: string[]) {
333
+      return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
334
+    }
335
+
336
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
337
+  }
338
+
305 339
   private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) {
306 340
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
307 341
     if (moderators.length === 0) return
308 342
 
309 343
     logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url)
310 344
 
311
-    function settingGetter (user: UserModel) {
345
+    function settingGetter (user: MUserWithNotificationSetting) {
312 346
       return user.NotificationSetting.videoAbuseAsModerator
313 347
     }
314 348
 
315
-    async function notificationCreator (user: UserModel) {
316
-      const notification: UserNotificationModelForApi = await UserNotificationModel.create({
349
+    async function notificationCreator (user: MUserWithNotificationSetting) {
350
+      const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
317 351
         type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
318 352
         userId: user.id,
319 353
         videoAbuseId: videoAbuse.id
@@ -330,29 +364,29 @@ class Notifier {
330 364
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
331 365
   }
332 366
 
333
-  private async notifyModeratorsOfVideoAutoBlacklist (video: MVideo) {
367
+  private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
334 368
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
335 369
     if (moderators.length === 0) return
336 370
 
337
-    logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
371
+    logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
338 372
 
339
-    function settingGetter (user: UserModel) {
373
+    function settingGetter (user: MUserWithNotificationSetting) {
340 374
       return user.NotificationSetting.videoAutoBlacklistAsModerator
341 375
     }
342
-    async function notificationCreator (user: UserModel) {
343 376
 
344
-      const notification = await UserNotificationModel.create({
377
+    async function notificationCreator (user: MUserWithNotificationSetting) {
378
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
345 379
         type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
346 380
         userId: user.id,
347
-        videoId: video.id
381
+        videoBlacklistId: videoBlacklist.id
348 382
       })
349
-      notification.Video = video as VideoModel
383
+      notification.VideoBlacklist = videoBlacklist
350 384
 
351 385
       return notification
352 386
     }
353 387
 
354 388
     function emailSender (emails: string[]) {
355
-      return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
389
+      return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
356 390
     }
357 391
 
358 392
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
@@ -364,17 +398,17 @@ class Notifier {
364 398
 
365 399
     logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
366 400
 
367
-    function settingGetter (user: UserModel) {
401
+    function settingGetter (user: MUserWithNotificationSetting) {
368 402
       return user.NotificationSetting.blacklistOnMyVideo
369 403
     }
370 404
 
371
-    async function notificationCreator (user: UserModel) {
372
-      const notification = await UserNotificationModel.create({
405
+    async function notificationCreator (user: MUserWithNotificationSetting) {
406
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
373 407
         type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
374 408
         userId: user.id,
375 409
         videoBlacklistId: videoBlacklist.id
376 410
       })
377
-      notification.VideoBlacklist = videoBlacklist as VideoBlacklistModel
411
+      notification.VideoBlacklist = videoBlacklist
378 412
 
379 413
       return notification
380 414
     }
@@ -386,23 +420,23 @@ class Notifier {
386 420
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
387 421
   }
388 422
 
389
-  private async notifyVideoOwnerOfUnblacklist (video: MVideo) {
423
+  private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
390 424
     const user = await UserModel.loadByVideoId(video.id)
391 425
     if (!user) return
392 426
 
393 427
     logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
394 428
 
395
-    function settingGetter (user: UserModel) {
429
+    function settingGetter (user: MUserWithNotificationSetting) {
396 430
       return user.NotificationSetting.blacklistOnMyVideo
397 431
     }
398 432
 
399
-    async function notificationCreator (user: UserModel) {
400
-      const notification = await UserNotificationModel.create({
433
+    async function notificationCreator (user: MUserWithNotificationSetting) {
434
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
401 435
         type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
402 436
         userId: user.id,
403 437
         videoId: video.id
404 438
       })
405
-      notification.Video = video as VideoModel
439
+      notification.Video = video
406 440
 
407 441
       return notification
408 442
     }
@@ -420,17 +454,17 @@ class Notifier {
420 454
 
421 455
     logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
422 456
 
423
-    function settingGetter (user: UserModel) {
457
+    function settingGetter (user: MUserWithNotificationSetting) {
424 458
       return user.NotificationSetting.myVideoPublished
425 459
     }
426 460
 
427
-    async function notificationCreator (user: UserModel) {
428
-      const notification = await UserNotificationModel.create({
461
+    async function notificationCreator (user: MUserWithNotificationSetting) {
462
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
429 463
         type: UserNotificationType.MY_VIDEO_PUBLISHED,
430 464
         userId: user.id,
431 465
         videoId: video.id
432 466
       })
433
-      notification.Video = video as VideoModel
467
+      notification.Video = video
434 468
 
435 469
       return notification
436 470
     }
@@ -448,17 +482,17 @@ class Notifier {
448 482
 
449 483
     logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
450 484
 
451
-    function settingGetter (user: UserModel) {
485
+    function settingGetter (user: MUserWithNotificationSetting) {
452 486
       return user.NotificationSetting.myVideoImportFinished
453 487
     }
454 488
 
455
-    async function notificationCreator (user: UserModel) {
456
-      const notification = await UserNotificationModel.create({
489
+    async function notificationCreator (user: MUserWithNotificationSetting) {
490
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
457 491
         type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
458 492
         userId: user.id,
459 493
         videoImportId: videoImport.id
460 494
       })
461
-      notification.VideoImport = videoImport as VideoImportModel
495
+      notification.VideoImport = videoImport
462 496
 
463 497
       return notification
464 498
     }
@@ -472,7 +506,7 @@ class Notifier {
472 506
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
473 507
   }
474 508
 
475
-  private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserAccount) {
509
+  private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
476 510
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
477 511
     if (moderators.length === 0) return
478 512
 
@@ -481,17 +515,17 @@ class Notifier {
481 515
       moderators.length, registeredUser.username
482 516
     )
483 517
 
484
-    function settingGetter (user: UserModel) {
518
+    function settingGetter (user: MUserWithNotificationSetting) {
485 519
       return user.NotificationSetting.newUserRegistration
486 520
     }
487 521
 
488
-    async function notificationCreator (user: UserModel) {
489
-      const notification = await UserNotificationModel.create({
522
+    async function notificationCreator (user: MUserWithNotificationSetting) {
523
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
490 524
         type: UserNotificationType.NEW_USER_REGISTRATION,
491 525
         userId: user.id,
492 526
         accountId: registeredUser.Account.id
493 527
       })
494
-      notification.Account = registeredUser.Account as AccountModel
528
+      notification.Account = registeredUser.Account
495 529
 
496 530
       return notification
497 531
     }
@@ -503,11 +537,11 @@ class Notifier {
503 537
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
504 538
   }
505 539
 
506
-  private async notify (options: {
507
-    users: MUserWithNotificationSetting[],
508
-    notificationCreator: (user: MUserWithNotificationSetting) => Promise<UserNotificationModelForApi>,
540
+  private async notify <T extends MUserWithNotificationSetting> (options: {
541
+    users: T[],
542
+    notificationCreator: (user: T) => Promise<UserNotificationModelForApi>,
509 543
     emailSender: (emails: string[]) => Promise<any> | Bluebird<any>,
510
-    settingGetter: (user: MUserWithNotificationSetting) => UserNotificationSettingValue
544
+    settingGetter: (user: T) => UserNotificationSettingValue
511 545
   }) {
512 546
     const emails: string[] = []
513 547
 

+ 2
- 1
server/lib/user.ts View File

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

+ 5
- 3
server/lib/video-blacklist.ts View File

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

+ 2
- 0
server/middlewares/validators/user-notifications.ts View File

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

+ 1
- 1
server/models/account/account.ts View File

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

+ 11
- 1
server/models/account/user-notification-setting.ts View File

@@ -111,6 +111,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
111 111
   @Column
112 112
   newInstanceFollower: UserNotificationSettingValue
113 113
 
114
+  @AllowNull(false)
115
+  @Default(null)
116
+  @Is(
117
+    'UserNotificationSettingNewInstanceFollower',
118
+    value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
119
+  )
120
+  @Column
121
+  autoInstanceFollowing: UserNotificationSettingValue
122
+
114 123
   @AllowNull(false)
115 124
   @Default(null)
116 125
   @Is(
@@ -165,7 +174,8 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
165 174
       newUserRegistration: this.newUserRegistration,
166 175
       commentMention: this.commentMention,
167 176
       newFollow: this.newFollow,
168
-      newInstanceFollower: this.newInstanceFollower
177
+      newInstanceFollower: this.newInstanceFollower,
178
+      autoInstanceFollowing: this.autoInstanceFollowing
169 179
     }
170 180
   }
171 181
 }

+ 15
- 4
server/models/account/user-notification.ts View File

@@ -135,13 +135,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
135 135
             ]
136 136
           },
137 137
           {
138
-            attributes: [ 'preferredUsername' ],
138
+            attributes: [ 'preferredUsername', 'type' ],
139 139
             model: ActorModel.unscoped(),
140 140
             required: true,
141 141
             as: 'ActorFollowing',
142 142
             include: [
143 143
               buildChannelInclude(false),
144
-              buildAccountInclude(false)
144
+              buildAccountInclude(false),
145
+              {
146
+                attributes: [ 'host' ],
147
+                model: ServerModel.unscoped(),
148
+                required: false
149
+              }
145 150
             ]
146 151
           }
147 152
         ]
@@ -404,6 +409,11 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
404 409
 
405 410
     const account = this.Account ? this.formatActor(this.Account) : undefined
406 411
 
412
+    const actorFollowingType = {
413
+      Application: 'instance' as 'instance',
414
+      Group: 'channel' as 'channel',
415
+      Person: 'account' as 'account'
416
+    }
407 417
     const actorFollow = this.ActorFollow ? {
408 418
       id: this.ActorFollow.id,
409 419
       state: this.ActorFollow.state,
@@ -415,9 +425,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
415 425
         host: this.ActorFollow.ActorFollower.getHost()
416 426
       },
417 427
       following: {
418
-        type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
428
+        type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
419 429
         displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
420
-        name: this.ActorFollow.ActorFollowing.preferredUsername
430
+        name: this.ActorFollow.ActorFollowing.preferredUsername,
431
+        host: this.ActorFollow.ActorFollowing.getHost()
421 432
       }
422 433
     } : undefined
423 434
 

+ 2
- 10
server/models/activitypub/actor.ts View File

@@ -43,7 +43,6 @@ import {
43 43
   MActorFormattable,
44 44
   MActorFull,
45 45
   MActorHost,
46
-  MActorRedundancyAllowedOpt,
47 46
   MActorServer,
48 47
   MActorSummaryFormattable
49 48
 } from '../../typings/models'
@@ -430,15 +429,8 @@ export class ActorModel extends Model<ActorModel> {
430 429
     })
431 430
   }
432 431
 
433
-  toActivityPubObject (this: MActorAP, name: string, type: 'Account' | 'Application' | 'VideoChannel') {
432
+  toActivityPubObject (this: MActorAP, name: string) {
434 433
     let activityPubType
435
-    if (type === 'Account') {
436
-      activityPubType = 'Person' as 'Person'
437
-    } else if (type === 'Application') {
438
-      activityPubType = 'Application' as 'Application'
439
-    } else { // VideoChannel
440
-      activityPubType = 'Group' as 'Group'
441
-    }
442 434
 
443 435
     let icon = undefined
444 436
     if (this.avatarId) {
@@ -451,7 +443,7 @@ export class ActorModel extends Model<ActorModel> {
451 443
     }
452 444
 
453 445
     const json = {
454
-      type: activityPubType,
446
+      type: this.type,
455 447
       id: this.url,
456 448
       following: this.getFollowingUrl(),
457 449
       followers: this.getFollowersUrl(),

+ 10
- 0
server/models/server/server.ts View File

@@ -51,6 +51,16 @@ export class ServerModel extends Model<ServerModel> {
51 51
   })
52 52
   BlockedByAccounts: ServerBlocklistModel[]
53 53
 
54
+  static load (id: number): Bluebird<MServer> {
55
+    const query = {
56
+      where: {
57
+        id
58
+      }
59
+    }
60
+
61
+    return ServerModel.findOne(query)
62
+  }
63
+
54 64
   static loadByHost (host: string): Bluebird<MServer> {
55 65
     const query = {
56 66
       where: {

+ 1
- 1
server/models/video/video-channel.ts View File

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

+ 21
- 2
server/tests/api/check-params/config.ts View File

@@ -5,8 +5,16 @@ import 'mocha'
5 5
 import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
6 6
 
7 7
 import {
8
-  createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo,
9
-  setAccessTokensToServers, userLogin, immutableAssign, cleanupTests
8
+  cleanupTests,
9
+  createUser,
10
+  flushAndRunServer,
11
+  immutableAssign,
12
+  makeDeleteRequest,
13
+  makeGetRequest,
14
+  makePutBodyRequest,
15
+  ServerInfo,
16
+  setAccessTokensToServers,
17
+  userLogin
10 18
 } from '../../../../shared/extra-utils'
11 19
 
12 20
 describe('Test config API validators', function () {
@@ -98,6 +106,17 @@ describe('Test config API validators', function () {
98 106
         enabled: false,
99 107
         manualApproval: true
100 108
       }
109
+    },
110
+    followings: {
111
+      instance: {
112
+        autoFollowBack: {
113
+          enabled: true
114
+        },
115
+        autoFollowIndex: {
116
+          enabled: true,
117
+          indexUrl: 'https://index.example.com'
118
+        }
119
+      }
101 120
     }
102 121
   }
103 122
 

+ 2
- 1
server/tests/api/check-params/user-notifications.ts View File

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

+ 36
- 4
server/tests/api/notifications/user-notifications.ts View File

@@ -16,8 +16,8 @@ import {
16 16
   immutableAssign,
17 17
   registerUser,
18 18
   removeVideoFromBlacklist,
19
-  reportVideoAbuse,
20
-  updateCustomConfig,
19
+  reportVideoAbuse, unfollow,
20
+  updateCustomConfig, updateCustomSubConfig,
21 21
   updateMyUser,
22 22
   updateVideo,
23 23
   updateVideoChannel,
@@ -45,7 +45,8 @@ import {
45 45
   getUserNotifications,
46 46
   markAsReadAllNotifications,
47 47
   markAsReadNotifications,
48
-  updateMyNotificationSettings
48
+  updateMyNotificationSettings,
49
+  checkAutoInstanceFollowing
49 50
 } from '../../../../shared/extra-utils/users/user-notifications'
50 51
 import {
51 52
   User,
@@ -108,7 +109,8 @@ describe('Test users notifications', function () {
108 109
     commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 110
     newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
110 111
     newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
111
-    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
112
+    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
113
+    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
112 114
   }
113 115
 
114 116
   before(async function () {
@@ -897,6 +899,36 @@ describe('Test users notifications', function () {
897 899
       const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
898 900
       await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
899 901
     })
902
+
903
+    it('Should send a notification on auto follow back', async function () {
904
+      this.timeout(40000)
905
+
906
+      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
907
+      await waitJobs(servers)
908
+
909
+      const config = {
910
+        followings: {
911
+          instance: {
912
+            autoFollowBack: { enabled: true }
913
+          }
914
+        }
915
+      }
916
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
917
+
918
+      await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
919
+
920
+      await waitJobs(servers)
921
+
922
+      const followerHost = servers[0].host
923
+      const followingHost = servers[2].host
924
+      await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
925
+
926
+      const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
927
+      await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence')
928
+
929
+      config.followings.instance.autoFollowBack.enabled = false
930
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
931
+    })
900 932
   })
901 933
 
902 934
   describe('New actor follow', function () {

+ 148
- 0
server/tests/api/server/auto-follows.ts View File

@@ -0,0 +1,148 @@
1
+/* tslint:disable:no-unused-expression */
2
+
3
+import * as chai from 'chai'
4
+import 'mocha'
5
+import {
6
+  acceptFollower,
7
+  cleanupTests,
8
+  flushAndRunMultipleServers,
9
+  ServerInfo,
10
+  setAccessTokensToServers,
11
+  unfollow,
12
+  updateCustomSubConfig
13
+} from '../../../../shared/extra-utils/index'
14
+import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
15
+import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
16
+import { ActorFollow } from '../../../../shared/models/actors'
17
+
18
+const expect = chai.expect
19
+
20
+async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) {
21
+  {
22
+    const res = await getFollowersListPaginationAndSort(following.url, 0, 5, '-createdAt')
23
+    const follows = res.body.data as ActorFollow[]
24
+
25
+    if (exists === true) {
26
+      expect(res.body.total).to.equal(1)
27
+
28
+      expect(follows[ 0 ].follower.host).to.equal(follower.host)
29
+      expect(follows[ 0 ].state).to.equal('accepted')
30
+    } else {
31
+      expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0)
32
+    }
33
+  }
34
+
35
+  {
36
+    const res = await getFollowingListPaginationAndSort(follower.url, 0, 5, '-createdAt')
37
+    const follows = res.body.data as ActorFollow[]
38
+
39
+    if (exists === true) {
40
+      expect(res.body.total).to.equal(1)
41
+
42
+      expect(follows[ 0 ].following.host).to.equal(following.host)
43
+      expect(follows[ 0 ].state).to.equal('accepted')
44
+    } else {
45
+      expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0)
46
+    }
47
+  }
48
+}
49
+
50
+async function server1Follows2 (servers: ServerInfo[]) {
51
+  await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken)
52
+
53
+  await waitJobs(servers)
54
+}
55
+
56
+async function resetFollows (servers: ServerInfo[]) {
57
+  try {
58
+    await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ])
59
+    await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ])
60
+  } catch { /* empty */ }
61
+
62
+  await waitJobs(servers)
63
+
64
+  await checkFollow(servers[0], servers[1], false)
65
+  await checkFollow(servers[1], servers[0], false)
66
+}
67
+
68
+describe('Test auto follows', function () {
69
+  let servers: ServerInfo[] = []
70
+
71
+  before(async function () {
72
+    this.timeout(30000)
73
+
74
+    servers = await flushAndRunMultipleServers(2)
75
+
76
+    // Get the access tokens
77
+    await setAccessTokensToServers(servers)
78
+  })
79
+
80
+  describe('Auto follow back', function () {
81
+
82
+    it('Should not auto follow back if the option is not enabled', async function () {
83
+      this.timeout(15000)
84
+
85
+      await server1Follows2(servers)
86
+
87
+      await checkFollow(servers[0], servers[1], true)
88
+      await checkFollow(servers[1], servers[0], false)
89
+
90
+      await resetFollows(servers)
91
+    })
92
+
93
+    it('Should auto follow back on auto accept if the option is enabled', async function () {
94
+      this.timeout(15000)
95
+
96
+      const config = {
97
+        followings: {
98
+          instance: {
99
+            autoFollowBack: { enabled: true }
100
+          }
101
+        }
102
+      }
103
+      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
104
+
105
+      await server1Follows2(servers)
106
+
107
+      await checkFollow(servers[0], servers[1], true)
108
+      await checkFollow(servers[1], servers[0], true)
109
+
110
+      await resetFollows(servers)
111
+    })
112
+
113
+    it('Should wait the acceptation before auto follow back', async function () {
114
+      this.timeout(30000)
115
+
116
+      const config = {
117
+        followings: {
118
+          instance: {
119
+            autoFollowBack: { enabled: true }
120
+          }
121
+        },
122
+        followers: {
123
+          instance: {
124
+            manualApproval: true
125
+          }
126
+        }
127
+      }
128
+      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
129
+
130
+      await server1Follows2(servers)
131
+
132
+      await checkFollow(servers[0], servers[1], false)
133
+      await checkFollow(servers[1], servers[0], false)
134
+
135
+      await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host)
136
+      await waitJobs(servers)
137
+
138
+      await checkFollow(servers[0], servers[1], true)
139
+      await checkFollow(servers[1], servers[0], true)
140
+
141
+      await resetFollows(servers)
142
+    })
143
+  })
144
+
145
+  after(async function () {
146
+    await cleanupTests(servers)
147
+  })
148
+})

+ 19
- 0
server/tests/api/server/config.ts View File

@@ -68,6 +68,10 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
68 68
 
69 69
   expect(data.followers.instance.enabled).to.be.true
70 70
   expect(data.followers.instance.manualApproval).to.be.false
71
+
72
+  expect(data.followings.instance.autoFollowBack.enabled).to.be.false
73
+  expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
74
+  expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://instances.joinpeertube.org')
71 75
 }
72 76
 
73 77
 function checkUpdatedConfig (data: CustomConfig) {
@@ -119,6 +123,10 @@ function checkUpdatedConfig (data: CustomConfig) {
119 123
 
120 124
   expect(data.followers.instance.enabled).to.be.false
121 125
   expect(data.followers.instance.manualApproval).to.be.true
126
+
127
+  expect(data.followings.instance.autoFollowBack.enabled).to.be.true
128
+  expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
129
+  expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
122 130
 }
123 131
 
124 132
 describe('Test config', function () {
@@ -261,6 +269,17 @@ describe('Test config', function () {
261 269
           enabled: false,
262 270
           manualApproval: true
263 271
         }
272
+      },
273
+      followings: {
274
+        instance: {
275
+          autoFollowBack: {
276
+            enabled: true
277
+          },
278
+          autoFollowIndex: {
279
+            enabled: true,
280
+            indexUrl: 'https://updated.example.com'
281
+          }
282
+        }
264 283
       }
265 284
     }
266 285
     await updateCustomConfig(server.url, server.accessToken, newCustomConfig)

+ 1
- 0
server/tests/api/server/index.ts View File

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

+ 3
- 7
server/typings/models/account/actor-follow.ts View File

@@ -2,7 +2,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
2 2
 import {
3 3
   MActor,
4 4
   MActorAccount,
5
-  MActorAccountChannel,
5
+  MActorDefaultAccountChannel,
6 6
   MActorChannelAccountActor,
7 7
   MActorDefault,
8 8
   MActorFormattable,
@@ -37,8 +37,8 @@ export type MActorFollowActorsDefault = MActorFollow &
37 37
   Use<'ActorFollowing', MActorDefault>
38 38
 
39 39
 export type MActorFollowFull = MActorFollow &
40
-  Use<'ActorFollower', MActorAccountChannel> &
41
-  Use<'ActorFollowing', MActorAccountChannel>
40
+  Use<'ActorFollower', MActorDefaultAccountChannel> &
41
+  Use<'ActorFollowing', MActorDefaultAccountChannel>
42 42
 
43 43
 // ############################################################################
44 44
 
@@ -51,10 +51,6 @@ export type MActorFollowActorsDefaultSubscription = MActorFollow &
51 51
   Use<'ActorFollower', MActorDefault> &
52 52
   Use<'ActorFollowing', SubscriptionFollowing>
53 53
 
54
-export type MActorFollowFollowingFullFollowerAccount = MActorFollow &
55
-  Use<'ActorFollower', MActorAccount> &
56
-  Use<'ActorFollowing', MActorAccountChannel>
57
-
58 54
 export type MActorFollowSubscriptions = MActorFollow &
59 55
   Use<'ActorFollowing', MActorChannelAccountActor>
60 56
 

+ 1
- 1
server/typings/models/account/actor.ts View File

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

+ 6
- 5
server/typings/models/user/user-notification.ts View File

@@ -1,5 +1,5 @@
1 1
 import { UserNotificationModel } from '../../../models/account/user-notification'
2
-import { PickWith } from '../../utils'
2
+import { PickWith, PickWithOpt } from '../../utils'
3 3
 import { VideoModel } from '../../../models/video/video'
4 4
 import { ActorModel } from '../../../models/activitypub/actor'
5 5
 import { ServerModel } from '../../../models/server/server'
@@ -48,12 +48,13 @@ export namespace UserNotificationIncludes {
48 48
 
49 49
   export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
50 50
     PickWith<ActorModel, 'Account', AccountInclude> &
51
-    PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
52
-    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
51
+    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
52
+    PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
53 53
 
54
-  export type ActorFollowing = Pick<ActorModel, 'preferredUsername'> &
54
+  export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
55 55
     PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> &
56
-    PickWith<ActorModel, 'Account', AccountInclude>
56
+    PickWith<ActorModel, 'Account', AccountInclude> &
57
+    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
57 58
 
58 59
   export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> &
59 60
     PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &

+ 3
- 0
server/typings/models/video/video-blacklist.ts View File

@@ -13,6 +13,9 @@ export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'>
13 13
 
14 14
 // ############################################################################
15 15
 
16
+export type MVideoBlacklistLightVideo = MVideoBlacklistLight &
17
+  Use<'Video', MVideo>
18
+
16 19
 export type MVideoBlacklistVideo = MVideoBlacklist &
17 20
   Use<'Video', MVideo>
18 21
 

+ 9
- 0
server/typings/utils.ts View File

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

+ 15
- 2
shared/extra-utils/server/config.ts View File

@@ -1,5 +1,7 @@
1 1
 import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2 2
 import { CustomConfig } from '../../models/server/custom-config.model'
3
+import { DeepPartial } from '@server/typings/utils'
4
+import { merge } from 'lodash'
3 5
 
4 6
 function getConfig (url: string) {
5 7
   const path = '/api/v1/config'
@@ -44,7 +46,7 @@ function updateCustomConfig (url: string, token: string, newCustomConfig: Custom
44 46
   })
45 47
 }
46 48
 
47
-function updateCustomSubConfig (url: string, token: string, newConfig: any) {
49
+function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
48 50
   const updateParams: CustomConfig = {
49 51
     instance: {
50 52
       name: 'PeerTube updated',
@@ -130,10 +132,21 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
130 132
         enabled: true,
131 133
         manualApproval: false
132 134
       }
135
+    },
136
+    followings: {
137
+      instance: {
138
+        autoFollowBack: {
139
+          enabled: false
140
+        },
141
+        autoFollowIndex: {
142
+          indexUrl: 'https://instances.joinpeertube.org',
143
+          enabled: false
144
+        }
145
+      }
133 146
     }
134 147
   }
135 148
 
136
-  Object.assign(updateParams, newConfig)
149
+  merge(updateParams, newConfig)
137 150
 
138 151
   return updateCustomConfig(url, token, updateParams)
139 152
 }

+ 37
- 4
shared/extra-utils/users/user-notifications.ts View File

@@ -279,8 +279,9 @@ async function checkNewActorFollow (
279 279
       expect(notification.actorFollow.follower.name).to.equal(followerName)
280 280
       expect(notification.actorFollow.follower.host).to.not.be.undefined
281 281
 
282
-      expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
283
-      expect(notification.actorFollow.following.type).to.equal(followType)
282
+      const following = notification.actorFollow.following
283
+      expect(following.displayName).to.equal(followingDisplayName)
284
+      expect(following.type).to.equal(followType)
284 285
     } else {
285 286
       expect(notification).to.satisfy(n => {
286 287
         return n.type !== notificationType ||
@@ -327,6 +328,37 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
327 328
   await checkNotification(base, notificationChecker, emailFinder, type)
328 329
 }
329 330
 
331
+async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
332
+  const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
333
+
334
+  function notificationChecker (notification: UserNotification, type: CheckerType) {
335
+    if (type === 'presence') {
336
+      expect(notification).to.not.be.undefined
337
+      expect(notification.type).to.equal(notificationType)
338
+
339
+      const following = notification.actorFollow.following
340
+      checkActor(following)
341
+      expect(following.name).to.equal('peertube')
342
+      expect(following.host).to.equal(followingHost)
343
+
344
+      expect(notification.actorFollow.follower.name).to.equal('peertube')
345
+      expect(notification.actorFollow.follower.host).to.equal(followerHost)
346
+    } else {
347
+      expect(notification).to.satisfy(n => {
348
+        return n.type !== notificationType || n.actorFollow.following.host !== followingHost
349
+      })
350
+    }
351
+  }
352
+
353
+  function emailFinder (email: object) {
354
+    const text: string = email[ 'text' ]
355
+
356
+    return text.includes(' automatically followed a new instance') && text.includes(followingHost)
357
+  }
358
+
359
+  await checkNotification(base, notificationChecker, emailFinder, type)
360
+}
361
+
330 362
 async function checkCommentMention (
331 363
   base: CheckerBaseParams,
332 364
   uuid: string,
@@ -427,8 +459,8 @@ async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, vi
427 459
       expect(notification).to.not.be.undefined
428 460
       expect(notification.type).to.equal(notificationType)
429 461
 
430
-      expect(notification.video.id).to.be.a('number')
431
-      checkVideo(notification.video, videoName, videoUUID)
462
+      expect(notification.videoBlacklist.video.id).to.be.a('number')
463
+      checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
432 464
     } else {
433 465
       expect(notification).to.satisfy((n: UserNotification) => {
434 466
         return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
@@ -480,6 +512,7 @@ export {
480 512
   markAsReadAllNotifications,
481 513
   checkMyVideoImportIsFinished,
482 514
   checkUserRegistered,
515
+  checkAutoInstanceFollowing,
483 516
   checkVideoIsPublished,
484 517
   checkNewVideoFromSubscription,
485 518
   checkNewActorFollow,

+ 12
- 0
shared/models/server/custom-config.model.ts View File

@@ -99,4 +99,16 @@ export interface CustomConfig {
99 99
     }
100 100
   }
101 101
 
102
+  followings: {
103
+    instance: {
104
+      autoFollowBack: {
105
+        enabled: boolean
106
+      }
107
+
108
+      autoFollowIndex: {
109
+        enabled: boolean
110
+        indexUrl: string
111
+      }
112
+    }
113
+  }
102 114
 }

+ 1
- 0
shared/models/users/user-notification-setting.model.ts View File

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

+ 6
- 2
shared/models/users/user-notification.model.ts View File

@@ -19,7 +19,9 @@ export enum UserNotificationType {
19 19
 
20 20
   VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12,
21 21
 
22
-  NEW_INSTANCE_FOLLOWER = 13
22
+  NEW_INSTANCE_FOLLOWER = 13,
23
+
24
+  AUTO_INSTANCE_FOLLOWING = 14
23 25
 }
24 26
 
25 27
 export interface VideoInfo {
@@ -78,10 +80,12 @@ export interface UserNotification {
78 80
     id: number
79 81
     follower: ActorInfo
80 82
     state: FollowState
83
+
81 84
     following: {
82
-      type: 'account' | 'channel'
85
+      type: 'account' | 'channel' | 'instance'
83 86
       name: string
84 87
       displayName: string
88
+      host: string
85 89
     }
86 90
   }
87 91
 

+ 1
- 2
tsconfig.json View File

@@ -17,8 +17,7 @@
17 17
     "typeRoots": [ "node_modules/@types", "server/typings" ],
18 18
     "baseUrl": "./",
19 19
     "paths": {
20
-      "@server/typings/*": [ "server/typings/*" ],
21
-      "@server/models/*": [ "server/models/*" ]
20
+      "@server/*": [ "server/*" ]
22 21
     }
23 22
   },
24 23
   "exclude": [

+ 5
- 0
yarn.lock View File

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

Loading…
Cancel
Save