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