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