27643704e152275ed7cfb02af43390a1db207058
[oweals/peertube.git] / server / models / activitypub / actor-follow.ts
1 import * as Bluebird from 'bluebird'
2 import { difference, values } from 'lodash'
3 import {
4   AfterCreate,
5   AfterDestroy,
6   AfterUpdate,
7   AllowNull,
8   BelongsTo,
9   Column,
10   CreatedAt,
11   DataType,
12   Default,
13   ForeignKey,
14   IsInt,
15   Max,
16   Model,
17   Table,
18   UpdatedAt
19 } from 'sequelize-typescript'
20 import { FollowState } from '../../../shared/models/actors'
21 import { ActorFollow } from '../../../shared/models/actors/follow.model'
22 import { logger } from '../../helpers/logger'
23 import { getServerActor } from '../../helpers/utils'
24 import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
25 import { ServerModel } from '../server/server'
26 import { createSafeIn, getFollowsSort, getSort } from '../utils'
27 import { ActorModel, unusedActorAttributesForAPI } from './actor'
28 import { VideoChannelModel } from '../video/video-channel'
29 import { AccountModel } from '../account/account'
30 import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
31 import {
32   MActorFollowActorsDefault,
33   MActorFollowActorsDefaultSubscription,
34   MActorFollowFollowingHost,
35   MActorFollowFormattable,
36   MActorFollowSubscriptions
37 } from '@server/typings/models'
38 import { ActivityPubActorType } from '@shared/models'
39
40 @Table({
41   tableName: 'actorFollow',
42   indexes: [
43     {
44       fields: [ 'actorId' ]
45     },
46     {
47       fields: [ 'targetActorId' ]
48     },
49     {
50       fields: [ 'actorId', 'targetActorId' ],
51       unique: true
52     },
53     {
54       fields: [ 'score' ]
55     }
56   ]
57 })
58 export class ActorFollowModel extends Model<ActorFollowModel> {
59
60   @AllowNull(false)
61   @Column(DataType.ENUM(...values(FOLLOW_STATES)))
62   state: FollowState
63
64   @AllowNull(false)
65   @Default(ACTOR_FOLLOW_SCORE.BASE)
66   @IsInt
67   @Max(ACTOR_FOLLOW_SCORE.MAX)
68   @Column
69   score: number
70
71   @CreatedAt
72   createdAt: Date
73
74   @UpdatedAt
75   updatedAt: Date
76
77   @ForeignKey(() => ActorModel)
78   @Column
79   actorId: number
80
81   @BelongsTo(() => ActorModel, {
82     foreignKey: {
83       name: 'actorId',
84       allowNull: false
85     },
86     as: 'ActorFollower',
87     onDelete: 'CASCADE'
88   })
89   ActorFollower: ActorModel
90
91   @ForeignKey(() => ActorModel)
92   @Column
93   targetActorId: number
94
95   @BelongsTo(() => ActorModel, {
96     foreignKey: {
97       name: 'targetActorId',
98       allowNull: false
99     },
100     as: 'ActorFollowing',
101     onDelete: 'CASCADE'
102   })
103   ActorFollowing: ActorModel
104
105   @AfterCreate
106   @AfterUpdate
107   static incrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) {
108     if (instance.state !== 'accepted') return undefined
109
110     return Promise.all([
111       ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction),
112       ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction)
113     ])
114   }
115
116   @AfterDestroy
117   static decrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) {
118     return Promise.all([
119       ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction),
120       ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction)
121     ])
122   }
123
124   static removeFollowsOf (actorId: number, t?: Transaction) {
125     const query = {
126       where: {
127         [Op.or]: [
128           {
129             actorId
130           },
131           {
132             targetActorId: actorId
133           }
134         ]
135       },
136       transaction: t
137     }
138
139     return ActorFollowModel.destroy(query)
140   }
141
142   // Remove actor follows with a score of 0 (too many requests where they were unreachable)
143   static async removeBadActorFollows () {
144     const actorFollows = await ActorFollowModel.listBadActorFollows()
145
146     const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
147     await Promise.all(actorFollowsRemovePromises)
148
149     const numberOfActorFollowsRemoved = actorFollows.length
150
151     if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
152   }
153
154   static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
155     const query = {
156       where: {
157         actorId,
158         targetActorId: targetActorId
159       },
160       include: [
161         {
162           model: ActorModel,
163           required: true,
164           as: 'ActorFollower'
165         },
166         {
167           model: ActorModel,
168           required: true,
169           as: 'ActorFollowing'
170         }
171       ],
172       transaction: t
173     }
174
175     return ActorFollowModel.findOne(query)
176   }
177
178   static loadByActorAndTargetNameAndHostForAPI (
179     actorId: number,
180     targetName: string,
181     targetHost: string,
182     t?: Transaction
183   ): Bluebird<MActorFollowActorsDefaultSubscription> {
184     const actorFollowingPartInclude: IncludeOptions = {
185       model: ActorModel,
186       required: true,
187       as: 'ActorFollowing',
188       where: {
189         preferredUsername: targetName
190       },
191       include: [
192         {
193           model: VideoChannelModel.unscoped(),
194           required: false
195         }
196       ]
197     }
198
199     if (targetHost === null) {
200       actorFollowingPartInclude.where['serverId'] = null
201     } else {
202       actorFollowingPartInclude.include.push({
203         model: ServerModel,
204         required: true,
205         where: {
206           host: targetHost
207         }
208       })
209     }
210
211     const query = {
212       where: {
213         actorId
214       },
215       include: [
216         actorFollowingPartInclude,
217         {
218           model: ActorModel,
219           required: true,
220           as: 'ActorFollower'
221         }
222       ],
223       transaction: t
224     }
225
226     return ActorFollowModel.findOne(query)
227       .then(result => {
228         if (result?.ActorFollowing.VideoChannel) {
229           result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
230         }
231
232         return result
233       })
234   }
235
236   static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Bluebird<MActorFollowFollowingHost[]> {
237     const whereTab = targets
238       .map(t => {
239         if (t.host) {
240           return {
241             [Op.and]: [
242               {
243                 $preferredUsername$: t.name
244               },
245               {
246                 $host$: t.host
247               }
248             ]
249           }
250         }
251
252         return {
253           [Op.and]: [
254             {
255               $preferredUsername$: t.name
256             },
257             {
258               $serverId$: null
259             }
260           ]
261         }
262       })
263
264     const query = {
265       attributes: [],
266       where: {
267         [Op.and]: [
268           {
269             [Op.or]: whereTab
270           },
271           {
272             actorId
273           }
274         ]
275       },
276       include: [
277         {
278           attributes: [ 'preferredUsername' ],
279           model: ActorModel.unscoped(),
280           required: true,
281           as: 'ActorFollowing',
282           include: [
283             {
284               attributes: [ 'host' ],
285               model: ServerModel.unscoped(),
286               required: false
287             }
288           ]
289         }
290       ]
291     }
292
293     return ActorFollowModel.findAll(query)
294   }
295
296   static listFollowingForApi (options: {
297     id: number
298     start: number
299     count: number
300     sort: string
301     state?: FollowState
302     actorType?: ActivityPubActorType
303     search?: string
304   }) {
305     const { id, start, count, sort, search, state, actorType } = options
306
307     const followWhere = state ? { state } : {}
308     const followingWhere: WhereOptions = {}
309     const followingServerWhere: WhereOptions = {}
310
311     if (search) {
312       Object.assign(followingServerWhere, {
313         host: {
314           [Op.iLike]: '%' + search + '%'
315         }
316       })
317     }
318
319     if (actorType) {
320       Object.assign(followingWhere, { type: actorType })
321     }
322
323     const query = {
324       distinct: true,
325       offset: start,
326       limit: count,
327       order: getFollowsSort(sort),
328       where: followWhere,
329       include: [
330         {
331           model: ActorModel,
332           required: true,
333           as: 'ActorFollower',
334           where: {
335             id
336           }
337         },
338         {
339           model: ActorModel,
340           as: 'ActorFollowing',
341           required: true,
342           where: followingWhere,
343           include: [
344             {
345               model: ServerModel,
346               required: true,
347               where: followingServerWhere
348             }
349           ]
350         }
351       ]
352     }
353
354     return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
355       .then(({ rows, count }) => {
356         return {
357           data: rows,
358           total: count
359         }
360       })
361   }
362
363   static listFollowersForApi (options: {
364     actorId: number
365     start: number
366     count: number
367     sort: string
368     state?: FollowState
369     actorType?: ActivityPubActorType
370     search?: string
371   }) {
372     const { actorId, start, count, sort, search, state, actorType } = options
373
374     const followWhere = state ? { state } : {}
375     const followerWhere: WhereOptions = {}
376     const followerServerWhere: WhereOptions = {}
377
378     if (search) {
379       Object.assign(followerServerWhere, {
380         host: {
381           [Op.iLike]: '%' + search + '%'
382         }
383       })
384     }
385
386     if (actorType) {
387       Object.assign(followerWhere, { type: actorType })
388     }
389
390     const query = {
391       distinct: true,
392       offset: start,
393       limit: count,
394       order: getFollowsSort(sort),
395       where: followWhere,
396       include: [
397         {
398           model: ActorModel,
399           required: true,
400           as: 'ActorFollower',
401           where: followerWhere,
402           include: [
403             {
404               model: ServerModel,
405               required: true,
406               where: followerServerWhere
407             }
408           ]
409         },
410         {
411           model: ActorModel,
412           as: 'ActorFollowing',
413           required: true,
414           where: {
415             id: actorId
416           }
417         }
418       ]
419     }
420
421     return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
422                            .then(({ rows, count }) => {
423                              return {
424                                data: rows,
425                                total: count
426                              }
427                            })
428   }
429
430   static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
431     const query = {
432       attributes: [],
433       distinct: true,
434       offset: start,
435       limit: count,
436       order: getSort(sort),
437       where: {
438         actorId: actorId
439       },
440       include: [
441         {
442           attributes: [ 'id' ],
443           model: ActorModel.unscoped(),
444           as: 'ActorFollowing',
445           required: true,
446           include: [
447             {
448               model: VideoChannelModel.unscoped(),
449               required: true,
450               include: [
451                 {
452                   attributes: {
453                     exclude: unusedActorAttributesForAPI
454                   },
455                   model: ActorModel,
456                   required: true
457                 },
458                 {
459                   model: AccountModel.unscoped(),
460                   required: true,
461                   include: [
462                     {
463                       attributes: {
464                         exclude: unusedActorAttributesForAPI
465                       },
466                       model: ActorModel,
467                       required: true
468                     }
469                   ]
470                 }
471               ]
472             }
473           ]
474         }
475       ]
476     }
477
478     return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
479                            .then(({ rows, count }) => {
480                              return {
481                                data: rows.map(r => r.ActorFollowing.VideoChannel),
482                                total: count
483                              }
484                            })
485   }
486
487   static async keepUnfollowedInstance (hosts: string[]) {
488     const followerId = (await getServerActor()).id
489
490     const query = {
491       attributes: [ 'id' ],
492       where: {
493         actorId: followerId
494       },
495       include: [
496         {
497           attributes: [ 'id' ],
498           model: ActorModel.unscoped(),
499           required: true,
500           as: 'ActorFollowing',
501           where: {
502             preferredUsername: SERVER_ACTOR_NAME
503           },
504           include: [
505             {
506               attributes: [ 'host' ],
507               model: ServerModel.unscoped(),
508               required: true,
509               where: {
510                 host: {
511                   [Op.in]: hosts
512                 }
513               }
514             }
515           ]
516         }
517       ]
518     }
519
520     const res = await ActorFollowModel.findAll(query)
521     const followedHosts = res.map(row => row.ActorFollowing.Server.host)
522
523     return difference(hosts, followedHosts)
524   }
525
526   static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
527     return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
528   }
529
530   static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
531     return ActorFollowModel.createListAcceptedFollowForApiQuery(
532       'followers',
533       actorIds,
534       t,
535       undefined,
536       undefined,
537       'sharedInboxUrl',
538       true
539     )
540   }
541
542   static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
543     return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
544   }
545
546   static async getStats () {
547     const serverActor = await getServerActor()
548
549     const totalInstanceFollowing = await ActorFollowModel.count({
550       where: {
551         actorId: serverActor.id
552       }
553     })
554
555     const totalInstanceFollowers = await ActorFollowModel.count({
556       where: {
557         targetActorId: serverActor.id
558       }
559     })
560
561     return {
562       totalInstanceFollowing,
563       totalInstanceFollowers
564     }
565   }
566
567   static updateScore (inboxUrl: string, value: number, t?: Transaction) {
568     const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
569       'WHERE id IN (' +
570         'SELECT "actorFollow"."id" FROM "actorFollow" ' +
571         'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
572         `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
573       ')'
574
575     const options = {
576       type: QueryTypes.BULKUPDATE,
577       transaction: t
578     }
579
580     return ActorFollowModel.sequelize.query(query, options)
581   }
582
583   static async updateScoreByFollowingServers (serverIds: number[], value: number, t?: Transaction) {
584     if (serverIds.length === 0) return
585
586     const me = await getServerActor()
587     const serverIdsString = createSafeIn(ActorFollowModel, serverIds)
588
589     const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
590       'WHERE id IN (' +
591         'SELECT "actorFollow"."id" FROM "actorFollow" ' +
592         'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' +
593         `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` + // I'm the follower
594         `AND "actor"."serverId" IN (${serverIdsString})` + // Criteria on followings
595       ')'
596
597     const options = {
598       type: QueryTypes.BULKUPDATE,
599       transaction: t
600     }
601
602     return ActorFollowModel.sequelize.query(query, options)
603   }
604
605   private static async createListAcceptedFollowForApiQuery (
606     type: 'followers' | 'following',
607     actorIds: number[],
608     t: Transaction,
609     start?: number,
610     count?: number,
611     columnUrl = 'url',
612     distinct = false
613   ) {
614     let firstJoin: string
615     let secondJoin: string
616
617     if (type === 'followers') {
618       firstJoin = 'targetActorId'
619       secondJoin = 'actorId'
620     } else {
621       firstJoin = 'actorId'
622       secondJoin = 'targetActorId'
623     }
624
625     const selections: string[] = []
626     if (distinct === true) selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`)
627     else selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`)
628
629     selections.push('COUNT(*) AS "total"')
630
631     const tasks: Bluebird<any>[] = []
632
633     for (const selection of selections) {
634       let query = 'SELECT ' + selection + ' FROM "actor" ' +
635         'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
636         'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
637         `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL `
638
639       if (count !== undefined) query += 'LIMIT ' + count
640       if (start !== undefined) query += ' OFFSET ' + start
641
642       const options = {
643         bind: { actorIds },
644         type: QueryTypes.SELECT,
645         transaction: t
646       }
647       tasks.push(ActorFollowModel.sequelize.query(query, options))
648     }
649
650     const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
651     const urls: string[] = followers.map(f => f.selectionUrl)
652
653     return {
654       data: urls,
655       total: dataTotal ? parseInt(dataTotal.total, 10) : 0
656     }
657   }
658
659   private static listBadActorFollows () {
660     const query = {
661       where: {
662         score: {
663           [Op.lte]: 0
664         }
665       },
666       logging: false
667     }
668
669     return ActorFollowModel.findAll(query)
670   }
671
672   toFormattedJSON (this: MActorFollowFormattable): ActorFollow {
673     const follower = this.ActorFollower.toFormattedJSON()
674     const following = this.ActorFollowing.toFormattedJSON()
675
676     return {
677       id: this.id,
678       follower,
679       following,
680       score: this.score,
681       state: this.state,
682       createdAt: this.createdAt,
683       updatedAt: this.updatedAt
684     }
685   }
686 }