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