]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor-follow.ts
ebb2d47c263bba185c82220ef514e98de3194683
[github/Chocobozzz/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,
6 AfterDestroy,
7 AfterUpdate,
8 AllowNull,
9 BelongsTo,
10 Column,
11 CreatedAt,
12 DataType,
13 Default,
14 ForeignKey,
15 IsInt,
16 Max,
17 Model,
18 Table,
19 UpdatedAt
20 } from 'sequelize-typescript'
21 import { FollowState } from '../../../shared/models/actors'
22 import { AccountFollow } from '../../../shared/models/actors/follow.model'
23 import { logger } from '../../helpers/logger'
24 import { getServerActor } from '../../helpers/utils'
25 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
26 import { FOLLOW_STATES } from '../../initializers/constants'
27 import { ServerModel } from '../server/server'
28 import { getSort } from '../utils'
29 import { ActorModel, unusedActorAttributesForAPI } from './actor'
30 import { VideoChannelModel } from '../video/video-channel'
31 import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
32 import { AccountModel } from '../account/account'
33
34 @Table({
35 tableName: 'actorFollow',
36 indexes: [
37 {
38 fields: [ 'actorId' ]
39 },
40 {
41 fields: [ 'targetActorId' ]
42 },
43 {
44 fields: [ 'actorId', 'targetActorId' ],
45 unique: true
46 },
47 {
48 fields: [ 'score' ]
49 }
50 ]
51 })
52 export class ActorFollowModel extends Model<ActorFollowModel> {
53
54 @AllowNull(false)
55 @Column(DataType.ENUM(values(FOLLOW_STATES)))
56 state: FollowState
57
58 @AllowNull(false)
59 @Default(ACTOR_FOLLOW_SCORE.BASE)
60 @IsInt
61 @Max(ACTOR_FOLLOW_SCORE.MAX)
62 @Column
63 score: number
64
65 @CreatedAt
66 createdAt: Date
67
68 @UpdatedAt
69 updatedAt: Date
70
71 @ForeignKey(() => ActorModel)
72 @Column
73 actorId: number
74
75 @BelongsTo(() => ActorModel, {
76 foreignKey: {
77 name: 'actorId',
78 allowNull: false
79 },
80 as: 'ActorFollower',
81 onDelete: 'CASCADE'
82 })
83 ActorFollower: ActorModel
84
85 @ForeignKey(() => ActorModel)
86 @Column
87 targetActorId: number
88
89 @BelongsTo(() => ActorModel, {
90 foreignKey: {
91 name: 'targetActorId',
92 allowNull: false
93 },
94 as: 'ActorFollowing',
95 onDelete: 'CASCADE'
96 })
97 ActorFollowing: ActorModel
98
99 @AfterCreate
100 @AfterUpdate
101 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
102 if (instance.state !== 'accepted') return undefined
103
104 return Promise.all([
105 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
106 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
107 ])
108 }
109
110 @AfterDestroy
111 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
112 return Promise.all([
113 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
114 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
115 ])
116 }
117
118 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
119 static async removeBadActorFollows () {
120 const actorFollows = await ActorFollowModel.listBadActorFollows()
121
122 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
123 await Promise.all(actorFollowsRemovePromises)
124
125 const numberOfActorFollowsRemoved = actorFollows.length
126
127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
128 }
129
130 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
131 if (goodInboxes.length === 0 && badInboxes.length === 0) return
132
133 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
134
135 if (goodInboxes.length !== 0) {
136 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
137 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
138 }
139
140 if (badInboxes.length !== 0) {
141 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
142 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
143 }
144 }
145
146 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
147 const query = {
148 where: {
149 actorId,
150 targetActorId: targetActorId
151 },
152 include: [
153 {
154 model: ActorModel,
155 required: true,
156 as: 'ActorFollower'
157 },
158 {
159 model: ActorModel,
160 required: true,
161 as: 'ActorFollowing'
162 }
163 ],
164 transaction: t
165 }
166
167 return ActorFollowModel.findOne(query)
168 }
169
170 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
171 const actorFollowingPartInclude: IIncludeOptions = {
172 model: ActorModel,
173 required: true,
174 as: 'ActorFollowing',
175 where: {
176 preferredUsername: targetName
177 },
178 include: [
179 {
180 model: VideoChannelModel.unscoped(),
181 required: false
182 }
183 ]
184 }
185
186 if (targetHost === null) {
187 actorFollowingPartInclude.where['serverId'] = null
188 } else {
189 actorFollowingPartInclude.include.push({
190 model: ServerModel,
191 required: true,
192 where: {
193 host: targetHost
194 }
195 })
196 }
197
198 const query = {
199 where: {
200 actorId
201 },
202 include: [
203 actorFollowingPartInclude,
204 {
205 model: ActorModel,
206 required: true,
207 as: 'ActorFollower'
208 }
209 ],
210 transaction: t
211 }
212
213 return ActorFollowModel.findOne(query)
214 .then(result => {
215 if (result && result.ActorFollowing.VideoChannel) {
216 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
217 }
218
219 return result
220 })
221 }
222
223 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
224 const whereTab = targets
225 .map(t => {
226 if (t.host) {
227 return {
228 [ Sequelize.Op.and ]: [
229 {
230 '$preferredUsername$': t.name
231 },
232 {
233 '$host$': t.host
234 }
235 ]
236 }
237 }
238
239 return {
240 [ Sequelize.Op.and ]: [
241 {
242 '$preferredUsername$': t.name
243 },
244 {
245 '$serverId$': null
246 }
247 ]
248 }
249 })
250
251 const query = {
252 attributes: [],
253 where: {
254 [ Sequelize.Op.and ]: [
255 {
256 [ Sequelize.Op.or ]: whereTab
257 },
258 {
259 actorId
260 }
261 ]
262 },
263 include: [
264 {
265 attributes: [ 'preferredUsername' ],
266 model: ActorModel.unscoped(),
267 required: true,
268 as: 'ActorFollowing',
269 include: [
270 {
271 attributes: [ 'host' ],
272 model: ServerModel.unscoped(),
273 required: false
274 }
275 ]
276 }
277 ]
278 }
279
280 return ActorFollowModel.findAll(query)
281 }
282
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
284 const query = {
285 distinct: true,
286 offset: start,
287 limit: count,
288 order: getSort(sort),
289 include: [
290 {
291 model: ActorModel,
292 required: true,
293 as: 'ActorFollower',
294 where: {
295 id
296 }
297 },
298 {
299 model: ActorModel,
300 as: 'ActorFollowing',
301 required: true,
302 include: [ ServerModel ]
303 }
304 ]
305 }
306
307 return ActorFollowModel.findAndCountAll(query)
308 .then(({ rows, count }) => {
309 return {
310 data: rows,
311 total: count
312 }
313 })
314 }
315
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
317 const query = {
318 attributes: [],
319 distinct: true,
320 offset: start,
321 limit: count,
322 order: getSort(sort),
323 where: {
324 actorId: id
325 },
326 include: [
327 {
328 attributes: {
329 exclude: unusedActorAttributesForAPI
330 },
331 model: ActorModel,
332 as: 'ActorFollowing',
333 required: true,
334 include: [
335 {
336 model: VideoChannelModel,
337 required: true,
338 include: [
339 {
340 attributes: {
341 exclude: unusedActorAttributesForAPI
342 },
343 model: ActorModel,
344 required: true
345 },
346 {
347 model: AccountModel,
348 required: true,
349 include: [
350 {
351 attributes: {
352 exclude: unusedActorAttributesForAPI
353 },
354 model: ActorModel,
355 required: true
356 }
357 ]
358 }
359 ]
360 }
361 ]
362 }
363 ]
364 }
365
366 return ActorFollowModel.findAndCountAll(query)
367 .then(({ rows, count }) => {
368 return {
369 data: rows.map(r => r.ActorFollowing.VideoChannel),
370 total: count
371 }
372 })
373 }
374
375 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
376 const query = {
377 distinct: true,
378 offset: start,
379 limit: count,
380 order: getSort(sort),
381 include: [
382 {
383 model: ActorModel,
384 required: true,
385 as: 'ActorFollower',
386 include: [ ServerModel ]
387 },
388 {
389 model: ActorModel,
390 as: 'ActorFollowing',
391 required: true,
392 where: {
393 id
394 }
395 }
396 ]
397 }
398
399 return ActorFollowModel.findAndCountAll(query)
400 .then(({ rows, count }) => {
401 return {
402 data: rows,
403 total: count
404 }
405 })
406 }
407
408 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
409 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
410 }
411
412 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
413 return ActorFollowModel.createListAcceptedFollowForApiQuery(
414 'followers',
415 actorIds,
416 t,
417 undefined,
418 undefined,
419 'sharedInboxUrl',
420 true
421 )
422 }
423
424 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
425 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
426 }
427
428 static async getStats () {
429 const serverActor = await getServerActor()
430
431 const totalInstanceFollowing = await ActorFollowModel.count({
432 where: {
433 actorId: serverActor.id
434 }
435 })
436
437 const totalInstanceFollowers = await ActorFollowModel.count({
438 where: {
439 targetActorId: serverActor.id
440 }
441 })
442
443 return {
444 totalInstanceFollowing,
445 totalInstanceFollowers
446 }
447 }
448
449 private static async createListAcceptedFollowForApiQuery (
450 type: 'followers' | 'following',
451 actorIds: number[],
452 t: Sequelize.Transaction,
453 start?: number,
454 count?: number,
455 columnUrl = 'url',
456 distinct = false
457 ) {
458 let firstJoin: string
459 let secondJoin: string
460
461 if (type === 'followers') {
462 firstJoin = 'targetActorId'
463 secondJoin = 'actorId'
464 } else {
465 firstJoin = 'actorId'
466 secondJoin = 'targetActorId'
467 }
468
469 const selections: string[] = []
470 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
471 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
472
473 selections.push('COUNT(*) AS "total"')
474
475 const tasks: Bluebird<any>[] = []
476
477 for (let selection of selections) {
478 let query = 'SELECT ' + selection + ' FROM "actor" ' +
479 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
480 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
481 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
482
483 if (count !== undefined) query += 'LIMIT ' + count
484 if (start !== undefined) query += ' OFFSET ' + start
485
486 const options = {
487 bind: { actorIds },
488 type: Sequelize.QueryTypes.SELECT,
489 transaction: t
490 }
491 tasks.push(ActorFollowModel.sequelize.query(query, options))
492 }
493
494 const [ followers, [ { total } ] ] = await Promise.all(tasks)
495 const urls: string[] = followers.map(f => f.url)
496
497 return {
498 data: urls,
499 total: parseInt(total, 10)
500 }
501 }
502
503 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
504 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
505
506 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
507 'WHERE id IN (' +
508 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
509 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
510 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
511 ')'
512
513 const options = t ? {
514 type: Sequelize.QueryTypes.BULKUPDATE,
515 transaction: t
516 } : undefined
517
518 return ActorFollowModel.sequelize.query(query, options)
519 }
520
521 private static listBadActorFollows () {
522 const query = {
523 where: {
524 score: {
525 [Sequelize.Op.lte]: 0
526 }
527 },
528 logging: false
529 }
530
531 return ActorFollowModel.findAll(query)
532 }
533
534 toFormattedJSON (): AccountFollow {
535 const follower = this.ActorFollower.toFormattedJSON()
536 const following = this.ActorFollowing.toFormattedJSON()
537
538 return {
539 id: this.id,
540 follower,
541 following,
542 score: this.score,
543 state: this.state,
544 createdAt: this.createdAt,
545 updatedAt: this.updatedAt
546 }
547 }
548 }