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