]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/activitypub/actor-follow.ts
Update follower/following counts
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor-follow.ts
CommitLineData
50d6de9c
C
1import * as Bluebird from 'bluebird'
2import { values } from 'lodash'
3import * as Sequelize from 'sequelize'
60650c77 4import {
32b2b43c 5 AfterCreate, AfterDestroy, AfterUpdate,
60650c77
C
6 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, Table,
7 UpdatedAt
8} from 'sequelize-typescript'
50d6de9c 9import { FollowState } from '../../../shared/models/actors'
60650c77
C
10import { AccountFollow } from '../../../shared/models/actors/follow.model'
11import { logger } from '../../helpers/logger'
12import { ACTOR_FOLLOW_SCORE } from '../../initializers'
50d6de9c
C
13import { FOLLOW_STATES } from '../../initializers/constants'
14import { ServerModel } from '../server/server'
15import { getSort } from '../utils'
16import { 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
60650c77
C
30 },
31 {
32 fields: [ 'score' ]
50d6de9c
C
33 }
34 ]
35})
36export class ActorFollowModel extends Model<ActorFollowModel> {
37
38 @AllowNull(false)
39 @Column(DataType.ENUM(values(FOLLOW_STATES)))
40 state: FollowState
41
60650c77
C
42 @AllowNull(false)
43 @Default(ACTOR_FOLLOW_SCORE.BASE)
44 @IsInt
45 @Max(ACTOR_FOLLOW_SCORE.MAX)
46 @Column
47 score: number
48
50d6de9c
C
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
32b2b43c
C
83 @AfterCreate
84 @AfterUpdate
85 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
86 if (instance.state !== 'accepted') return
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
60650c77
C
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 updateActorFollowsScoreAndRemoveBadOnes (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
50d6de9c
C
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
6502c3d4
C
183 return ActorFollowModel.findOne(query)
184 }
185
186 static loadByFollowerInbox (url: string, t?: Sequelize.Transaction) {
187 const query = {
188 where: {
189 state: 'accepted'
190 },
191 include: [
192 {
193 model: ActorModel,
194 required: true,
195 as: 'ActorFollower',
196 where: {
197 [Sequelize.Op.or]: [
198 {
199 inboxUrl: url
200 },
201 {
202 sharedInboxUrl: url
203 }
204 ]
205 }
206 }
207 ],
208 transaction: t
209 } as any // FIXME: typings does not work
210
50d6de9c
C
211 return ActorFollowModel.findOne(query)
212 }
213
214 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
215 const query = {
216 distinct: true,
217 offset: start,
218 limit: count,
219 order: [ getSort(sort) ],
220 include: [
221 {
222 model: ActorModel,
223 required: true,
224 as: 'ActorFollower',
225 where: {
226 id
227 }
228 },
229 {
230 model: ActorModel,
231 as: 'ActorFollowing',
232 required: true,
233 include: [ ServerModel ]
234 }
235 ]
236 }
237
238 return ActorFollowModel.findAndCountAll(query)
239 .then(({ rows, count }) => {
240 return {
241 data: rows,
242 total: count
243 }
244 })
245 }
246
247 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
248 const query = {
249 distinct: true,
250 offset: start,
251 limit: count,
252 order: [ getSort(sort) ],
253 include: [
254 {
255 model: ActorModel,
256 required: true,
257 as: 'ActorFollower',
258 include: [ ServerModel ]
259 },
260 {
261 model: ActorModel,
262 as: 'ActorFollowing',
263 required: true,
264 where: {
265 id
266 }
267 }
268 ]
269 }
270
271 return ActorFollowModel.findAndCountAll(query)
272 .then(({ rows, count }) => {
273 return {
274 data: rows,
275 total: count
276 }
277 })
278 }
279
280 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
281 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
282 }
283
284 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
ca309a9f 285 return ActorFollowModel.createListAcceptedFollowForApiQuery(
759f8a29 286 'followers',
ca309a9f
C
287 actorIds,
288 t,
289 undefined,
290 undefined,
759f8a29
C
291 'sharedInboxUrl',
292 true
ca309a9f 293 )
50d6de9c
C
294 }
295
296 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
297 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
298 }
299
759f8a29
C
300 private static async createListAcceptedFollowForApiQuery (
301 type: 'followers' | 'following',
302 actorIds: number[],
303 t: Sequelize.Transaction,
304 start?: number,
305 count?: number,
306 columnUrl = 'url',
307 distinct = false
308 ) {
50d6de9c
C
309 let firstJoin: string
310 let secondJoin: string
311
312 if (type === 'followers') {
313 firstJoin = 'targetActorId'
314 secondJoin = 'actorId'
315 } else {
316 firstJoin = 'actorId'
317 secondJoin = 'targetActorId'
318 }
319
759f8a29
C
320 const selections: string[] = []
321 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
322 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
323
324 selections.push('COUNT(*) AS "total"')
325
50d6de9c
C
326 const tasks: Bluebird<any>[] = []
327
759f8a29 328 for (let selection of selections) {
50d6de9c
C
329 let query = 'SELECT ' + selection + ' FROM "actor" ' +
330 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
331 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
332 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
333
334 if (count !== undefined) query += 'LIMIT ' + count
335 if (start !== undefined) query += ' OFFSET ' + start
336
337 const options = {
338 bind: { actorIds },
339 type: Sequelize.QueryTypes.SELECT,
340 transaction: t
341 }
342 tasks.push(ActorFollowModel.sequelize.query(query, options))
343 }
344
345 const [ followers, [ { total } ] ] = await
346 Promise.all(tasks)
347 const urls: string[] = followers.map(f => f.url)
348
349 return {
350 data: urls,
351 total: parseInt(total, 10)
352 }
353 }
354
60650c77
C
355 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
356 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
357
05bc4dfa 358 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
60650c77
C
359 'WHERE id IN (' +
360 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
361 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
362 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
363 ')'
364
365 const options = {
366 type: Sequelize.QueryTypes.BULKUPDATE,
367 transaction: t
368 }
369
370 return ActorFollowModel.sequelize.query(query, options)
371 }
372
373 private static listBadActorFollows () {
374 const query = {
375 where: {
376 score: {
377 [Sequelize.Op.lte]: 0
378 }
379 }
380 }
381
382 return ActorFollowModel.findAll(query)
383 }
384
385 toFormattedJSON (): AccountFollow {
50d6de9c
C
386 const follower = this.ActorFollower.toFormattedJSON()
387 const following = this.ActorFollowing.toFormattedJSON()
388
389 return {
390 id: this.id,
391 follower,
392 following,
60650c77 393 score: this.score,
50d6de9c
C
394 state: this.state,
395 createdAt: this.createdAt,
396 updatedAt: this.updatedAt
397 }
398 }
399}