Add publishedAt field for video model.
[oweals/peertube.git] / server / models / activitypub / actor-follow.ts
1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
3 import * as Sequelize from 'sequelize'
4 import {
5   AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model,
6   Table, UpdatedAt
7 } from 'sequelize-typescript'
8 import { FollowState } from '../../../shared/models/actors'
9 import { AccountFollow } from '../../../shared/models/actors/follow.model'
10 import { logger } from '../../helpers/logger'
11 import { getServerActor } from '../../helpers/utils'
12 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
13 import { FOLLOW_STATES } from '../../initializers/constants'
14 import { ServerModel } from '../server/server'
15 import { getSort } from '../utils'
16 import { ActorModel } from './actor'
17
18 @Table({
19   tableName: 'actorFollow',
20   indexes: [
21     {
22       fields: [ 'actorId' ]
23     },
24     {
25       fields: [ 'targetActorId' ]
26     },
27     {
28       fields: [ 'actorId', 'targetActorId' ],
29       unique: true
30     },
31     {
32       fields: [ 'score' ]
33     }
34   ]
35 })
36 export class ActorFollowModel extends Model<ActorFollowModel> {
37
38   @AllowNull(false)
39   @Column(DataType.ENUM(values(FOLLOW_STATES)))
40   state: FollowState
41
42   @AllowNull(false)
43   @Default(ACTOR_FOLLOW_SCORE.BASE)
44   @IsInt
45   @Max(ACTOR_FOLLOW_SCORE.MAX)
46   @Column
47   score: number
48
49   @CreatedAt
50   createdAt: Date
51
52   @UpdatedAt
53   updatedAt: Date
54
55   @ForeignKey(() => ActorModel)
56   @Column
57   actorId: number
58
59   @BelongsTo(() => ActorModel, {
60     foreignKey: {
61       name: 'actorId',
62       allowNull: false
63     },
64     as: 'ActorFollower',
65     onDelete: 'CASCADE'
66   })
67   ActorFollower: ActorModel
68
69   @ForeignKey(() => ActorModel)
70   @Column
71   targetActorId: number
72
73   @BelongsTo(() => ActorModel, {
74     foreignKey: {
75       name: 'targetActorId',
76       allowNull: false
77     },
78     as: 'ActorFollowing',
79     onDelete: 'CASCADE'
80   })
81   ActorFollowing: ActorModel
82
83   @AfterCreate
84   @AfterUpdate
85   static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
86     if (instance.state !== 'accepted') return undefined
87
88     return Promise.all([
89       ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
90       ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
91     ])
92   }
93
94   @AfterDestroy
95   static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
96     return Promise.all([
97       ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
98       ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
99     ])
100   }
101
102   // Remove actor follows with a score of 0 (too many requests where they were unreachable)
103   static async removeBadActorFollows () {
104     const actorFollows = await ActorFollowModel.listBadActorFollows()
105
106     const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
107     await Promise.all(actorFollowsRemovePromises)
108
109     const numberOfActorFollowsRemoved = actorFollows.length
110
111     if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
112   }
113
114   static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) {
115     if (goodInboxes.length === 0 && badInboxes.length === 0) return
116
117     logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
118
119     if (goodInboxes.length !== 0) {
120       ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
121         .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
122     }
123
124     if (badInboxes.length !== 0) {
125       ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
126         .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
127     }
128   }
129
130   static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
131     const query = {
132       where: {
133         actorId,
134         targetActorId: targetActorId
135       },
136       include: [
137         {
138           model: ActorModel,
139           required: true,
140           as: 'ActorFollower'
141         },
142         {
143           model: ActorModel,
144           required: true,
145           as: 'ActorFollowing'
146         }
147       ],
148       transaction: t
149     }
150
151     return ActorFollowModel.findOne(query)
152   }
153
154   static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
155     const query = {
156       where: {
157         actorId
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           include: [
170             {
171               model: ServerModel,
172               required: true,
173               where: {
174                 host: targetHost
175               }
176             }
177           ]
178         }
179       ],
180       transaction: t
181     }
182
183     return ActorFollowModel.findOne(query)
184   }
185
186   static listFollowingForApi (id: number, start: number, count: number, sort: string) {
187     const query = {
188       distinct: true,
189       offset: start,
190       limit: count,
191       order: getSort(sort),
192       include: [
193         {
194           model: ActorModel,
195           required: true,
196           as: 'ActorFollower',
197           where: {
198             id
199           }
200         },
201         {
202           model: ActorModel,
203           as: 'ActorFollowing',
204           required: true,
205           include: [ ServerModel ]
206         }
207       ]
208     }
209
210     return ActorFollowModel.findAndCountAll(query)
211       .then(({ rows, count }) => {
212         return {
213           data: rows,
214           total: count
215         }
216       })
217   }
218
219   static listFollowersForApi (id: number, start: number, count: number, sort: string) {
220     const query = {
221       distinct: true,
222       offset: start,
223       limit: count,
224       order: getSort(sort),
225       include: [
226         {
227           model: ActorModel,
228           required: true,
229           as: 'ActorFollower',
230           include: [ ServerModel ]
231         },
232         {
233           model: ActorModel,
234           as: 'ActorFollowing',
235           required: true,
236           where: {
237             id
238           }
239         }
240       ]
241     }
242
243     return ActorFollowModel.findAndCountAll(query)
244       .then(({ rows, count }) => {
245         return {
246           data: rows,
247           total: count
248         }
249       })
250   }
251
252   static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
253     return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
254   }
255
256   static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
257     return ActorFollowModel.createListAcceptedFollowForApiQuery(
258       'followers',
259       actorIds,
260       t,
261       undefined,
262       undefined,
263       'sharedInboxUrl',
264       true
265     )
266   }
267
268   static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
269     return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
270   }
271
272   static async getStats () {
273     const serverActor = await getServerActor()
274
275     const totalInstanceFollowing = await ActorFollowModel.count({
276       where: {
277         actorId: serverActor.id
278       }
279     })
280
281     const totalInstanceFollowers = await ActorFollowModel.count({
282       where: {
283         targetActorId: serverActor.id
284       }
285     })
286
287     return {
288       totalInstanceFollowing,
289       totalInstanceFollowers
290     }
291   }
292
293   private static async createListAcceptedFollowForApiQuery (
294     type: 'followers' | 'following',
295     actorIds: number[],
296     t: Sequelize.Transaction,
297     start?: number,
298     count?: number,
299     columnUrl = 'url',
300     distinct = false
301   ) {
302     let firstJoin: string
303     let secondJoin: string
304
305     if (type === 'followers') {
306       firstJoin = 'targetActorId'
307       secondJoin = 'actorId'
308     } else {
309       firstJoin = 'actorId'
310       secondJoin = 'targetActorId'
311     }
312
313     const selections: string[] = []
314     if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
315     else selections.push('"Follows"."' + columnUrl + '" AS "url"')
316
317     selections.push('COUNT(*) AS "total"')
318
319     const tasks: Bluebird<any>[] = []
320
321     for (let selection of selections) {
322       let query = 'SELECT ' + selection + ' FROM "actor" ' +
323         'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
324         'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
325         'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
326
327       if (count !== undefined) query += 'LIMIT ' + count
328       if (start !== undefined) query += ' OFFSET ' + start
329
330       const options = {
331         bind: { actorIds },
332         type: Sequelize.QueryTypes.SELECT,
333         transaction: t
334       }
335       tasks.push(ActorFollowModel.sequelize.query(query, options))
336     }
337
338     const [ followers, [ { total } ] ] = await
339     Promise.all(tasks)
340     const urls: string[] = followers.map(f => f.url)
341
342     return {
343       data: urls,
344       total: parseInt(total, 10)
345     }
346   }
347
348   private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
349     const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
350
351     const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
352       'WHERE id IN (' +
353         'SELECT "actorFollow"."id" FROM "actorFollow" ' +
354         'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
355         'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
356       ')'
357
358     const options = {
359       type: Sequelize.QueryTypes.BULKUPDATE,
360       transaction: t
361     }
362
363     return ActorFollowModel.sequelize.query(query, options)
364   }
365
366   private static listBadActorFollows () {
367     const query = {
368       where: {
369         score: {
370           [Sequelize.Op.lte]: 0
371         }
372       },
373       logging: false
374     }
375
376     return ActorFollowModel.findAll(query)
377   }
378
379   toFormattedJSON (): AccountFollow {
380     const follower = this.ActorFollower.toFormattedJSON()
381     const following = this.ActorFollowing.toFormattedJSON()
382
383     return {
384       id: this.id,
385       follower,
386       following,
387       score: this.score,
388       state: this.state,
389       createdAt: this.createdAt,
390       updatedAt: this.updatedAt
391     }
392   }
393 }