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