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