]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/activitypub/actor-follow.ts
Better error message in videos list
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor-follow.ts
CommitLineData
50d6de9c 1import * as Bluebird from 'bluebird'
6f1b4fa4 2import { values, difference } from 'lodash'
60650c77 3import {
06a05d5f
C
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
60650c77 19} from 'sequelize-typescript'
50d6de9c 20import { FollowState } from '../../../shared/models/actors'
c48e82b5 21import { ActorFollow } from '../../../shared/models/actors/follow.model'
60650c77 22import { logger } from '../../helpers/logger'
09cababd 23import { getServerActor } from '../../helpers/utils'
6f1b4fa4 24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
50d6de9c 25import { ServerModel } from '../server/server'
cb5ce4cb 26import { createSafeIn, getSort, getFollowsSort } from '../utils'
f37dc0dd 27import { ActorModel, unusedActorAttributesForAPI } from './actor'
06a05d5f 28import { VideoChannelModel } from '../video/video-channel'
22a16e36 29import { AccountModel } from '../account/account'
97ecddae 30import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
453e83ea
C
31import {
32 MActorFollowActorsDefault,
33 MActorFollowActorsDefaultSubscription,
34 MActorFollowFollowingHost,
1ca9f7c3 35 MActorFollowFormattable,
453e83ea
C
36 MActorFollowSubscriptions
37} from '@server/typings/models'
97ecddae 38import { ActivityPubActorType } from '@shared/models'
50d6de9c
C
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
60650c77
C
52 },
53 {
54 fields: [ 'score' ]
50d6de9c
C
55 }
56 ]
57})
58export class ActorFollowModel extends Model<ActorFollowModel> {
59
60 @AllowNull(false)
1735c825 61 @Column(DataType.ENUM(...values(FOLLOW_STATES)))
50d6de9c
C
62 state: FollowState
63
60650c77
C
64 @AllowNull(false)
65 @Default(ACTOR_FOLLOW_SCORE.BASE)
66 @IsInt
67 @Max(ACTOR_FOLLOW_SCORE.MAX)
68 @Column
69 score: number
70
50d6de9c
C
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
32b2b43c
C
105 @AfterCreate
106 @AfterUpdate
107 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
38768a36 108 if (instance.state !== 'accepted') return undefined
32b2b43c
C
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
44b88f18
C
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
60650c77
C
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
453e83ea 154 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
50d6de9c
C
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
453e83ea
C
178 static loadByActorAndTargetNameAndHostForAPI (
179 actorId: number,
180 targetName: string,
181 targetHost: string,
182 t?: Transaction
183 ): Bluebird<MActorFollowActorsDefaultSubscription> {
1735c825 184 const actorFollowingPartInclude: IncludeOptions = {
06a05d5f
C
185 model: ActorModel,
186 required: true,
187 as: 'ActorFollowing',
188 where: {
189 preferredUsername: targetName
99492dbc
C
190 },
191 include: [
192 {
f37dc0dd 193 model: VideoChannelModel.unscoped(),
99492dbc
C
194 required: false
195 }
196 ]
06a05d5f
C
197 }
198
199 if (targetHost === null) {
200 actorFollowingPartInclude.where['serverId'] = null
201 } else {
99492dbc
C
202 actorFollowingPartInclude.include.push({
203 model: ServerModel,
204 required: true,
205 where: {
206 host: targetHost
207 }
06a05d5f
C
208 })
209 }
210
50d6de9c
C
211 const query = {
212 where: {
213 actorId
214 },
215 include: [
aa55a4da
C
216 actorFollowingPartInclude,
217 {
218 model: ActorModel,
219 required: true,
220 as: 'ActorFollower'
221 }
50d6de9c
C
222 ],
223 transaction: t
224 }
225
6502c3d4 226 return ActorFollowModel.findOne(query)
f37dc0dd
C
227 .then(result => {
228 if (result && result.ActorFollowing.VideoChannel) {
229 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
230 }
231
232 return result
233 })
234 }
235
453e83ea 236 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Bluebird<MActorFollowFollowingHost[]> {
f37dc0dd
C
237 const whereTab = targets
238 .map(t => {
239 if (t.host) {
240 return {
1735c825 241 [ Op.and ]: [
f37dc0dd
C
242 {
243 '$preferredUsername$': t.name
244 },
245 {
246 '$host$': t.host
247 }
248 ]
249 }
250 }
251
252 return {
1735c825 253 [ Op.and ]: [
f37dc0dd
C
254 {
255 '$preferredUsername$': t.name
256 },
257 {
258 '$serverId$': null
259 }
260 ]
261 }
262 })
263
264 const query = {
265 attributes: [],
266 where: {
1735c825 267 [ Op.and ]: [
f37dc0dd 268 {
1735c825 269 [ Op.or ]: whereTab
f37dc0dd
C
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)
6502c3d4
C
294 }
295
b8f4167f
C
296 static listFollowingForApi (options: {
297 id: number,
298 start: number,
299 count: number,
300 sort: string,
301 state?: FollowState,
97ecddae 302 actorType?: ActivityPubActorType,
b8f4167f
C
303 search?: string
304 }) {
97ecddae 305 const { id, start, count, sort, search, state, actorType } = options
b8f4167f
C
306
307 const followWhere = state ? { state } : {}
97ecddae
C
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 }
b8f4167f 322
50d6de9c
C
323 const query = {
324 distinct: true,
325 offset: start,
326 limit: count,
cb5ce4cb 327 order: getFollowsSort(sort),
b8f4167f 328 where: followWhere,
50d6de9c
C
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,
97ecddae 342 where: followingWhere,
b014b6b9
C
343 include: [
344 {
345 model: ServerModel,
346 required: true,
97ecddae 347 where: followingServerWhere
b014b6b9
C
348 }
349 ]
50d6de9c
C
350 }
351 ]
352 }
353
453e83ea 354 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
50d6de9c
C
355 .then(({ rows, count }) => {
356 return {
357 data: rows,
358 total: count
359 }
360 })
361 }
362
b8f4167f
C
363 static listFollowersForApi (options: {
364 actorId: number,
365 start: number,
366 count: number,
367 sort: string,
368 state?: FollowState,
97ecddae 369 actorType?: ActivityPubActorType,
b8f4167f
C
370 search?: string
371 }) {
97ecddae 372 const { actorId, start, count, sort, search, state, actorType } = options
b8f4167f
C
373
374 const followWhere = state ? { state } : {}
97ecddae
C
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 }
b8f4167f 389
b014b6b9
C
390 const query = {
391 distinct: true,
392 offset: start,
393 limit: count,
cb5ce4cb 394 order: getFollowsSort(sort),
b8f4167f 395 where: followWhere,
b014b6b9
C
396 include: [
397 {
398 model: ActorModel,
399 required: true,
400 as: 'ActorFollower',
97ecddae 401 where: followerWhere,
b014b6b9
C
402 include: [
403 {
404 model: ServerModel,
405 required: true,
97ecddae 406 where: followerServerWhere
b014b6b9
C
407 }
408 ]
409 },
410 {
411 model: ActorModel,
412 as: 'ActorFollowing',
413 required: true,
414 where: {
cef534ed 415 id: actorId
b014b6b9
C
416 }
417 }
418 ]
419 }
420
453e83ea 421 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
b014b6b9
C
422 .then(({ rows, count }) => {
423 return {
424 data: rows,
425 total: count
426 }
427 })
428 }
429
cef534ed 430 static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
06a05d5f 431 const query = {
f37dc0dd 432 attributes: [],
06a05d5f
C
433 distinct: true,
434 offset: start,
435 limit: count,
436 order: getSort(sort),
437 where: {
cef534ed 438 actorId: actorId
06a05d5f
C
439 },
440 include: [
441 {
f5b0af50
C
442 attributes: [ 'id' ],
443 model: ActorModel.unscoped(),
06a05d5f
C
444 as: 'ActorFollowing',
445 required: true,
446 include: [
447 {
f5b0af50 448 model: VideoChannelModel.unscoped(),
22a16e36
C
449 required: true,
450 include: [
451 {
f37dc0dd
C
452 attributes: {
453 exclude: unusedActorAttributesForAPI
454 },
455 model: ActorModel,
22a16e36 456 required: true
f37dc0dd
C
457 },
458 {
f5b0af50 459 model: AccountModel.unscoped(),
f37dc0dd
C
460 required: true,
461 include: [
462 {
463 attributes: {
464 exclude: unusedActorAttributesForAPI
465 },
466 model: ActorModel,
467 required: true
468 }
469 ]
22a16e36
C
470 }
471 ]
06a05d5f
C
472 }
473 ]
474 }
475 ]
476 }
477
453e83ea 478 return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
06a05d5f
C
479 .then(({ rows, count }) => {
480 return {
481 data: rows.map(r => r.ActorFollowing.VideoChannel),
482 total: count
483 }
484 })
485 }
486
6f1b4fa4
C
487 static async keepUnfollowedInstance (hosts: string[]) {
488 const followerId = (await getServerActor()).id
489
490 const query = {
10a105f0 491 attributes: [ 'id' ],
6f1b4fa4
C
492 where: {
493 actorId: followerId
494 },
495 include: [
496 {
10a105f0 497 attributes: [ 'id' ],
6f1b4fa4
C
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)
10a105f0 521 const followedHosts = res.map(row => row.ActorFollowing.Server.host)
6f1b4fa4
C
522
523 return difference(hosts, followedHosts)
524 }
525
1735c825 526 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
50d6de9c
C
527 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
528 }
529
1735c825 530 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
ca309a9f 531 return ActorFollowModel.createListAcceptedFollowForApiQuery(
759f8a29 532 'followers',
ca309a9f
C
533 actorIds,
534 t,
535 undefined,
536 undefined,
759f8a29
C
537 'sharedInboxUrl',
538 true
ca309a9f 539 )
50d6de9c
C
540 }
541
1735c825 542 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
50d6de9c
C
543 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
544 }
545
09cababd
C
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
6b9c966f 567 static updateScore (inboxUrl: string, value: number, t?: Transaction) {
2f5c6b2f
C
568 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
569 'WHERE id IN (' +
cef534ed
C
570 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
571 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
572 `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
2f5c6b2f
C
573 ')'
574
575 const options = {
1735c825 576 type: QueryTypes.BULKUPDATE,
2f5c6b2f
C
577 transaction: t
578 }
579
580 return ActorFollowModel.sequelize.query(query, options)
581 }
582
6b9c966f
C
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
327b3318 589 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
6b9c966f
C
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
759f8a29
C
605 private static async createListAcceptedFollowForApiQuery (
606 type: 'followers' | 'following',
607 actorIds: number[],
1735c825 608 t: Transaction,
759f8a29
C
609 start?: number,
610 count?: number,
611 columnUrl = 'url',
612 distinct = false
613 ) {
50d6de9c
C
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
759f8a29 625 const selections: string[] = []
862ead21
C
626 if (distinct === true) selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`)
627 else selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`)
759f8a29
C
628
629 selections.push('COUNT(*) AS "total"')
630
50d6de9c
C
631 const tasks: Bluebird<any>[] = []
632
759f8a29 633 for (let selection of selections) {
50d6de9c
C
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" ' +
862ead21 637 `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL `
50d6de9c
C
638
639 if (count !== undefined) query += 'LIMIT ' + count
640 if (start !== undefined) query += ' OFFSET ' + start
641
642 const options = {
643 bind: { actorIds },
1735c825 644 type: QueryTypes.SELECT,
50d6de9c
C
645 transaction: t
646 }
647 tasks.push(ActorFollowModel.sequelize.query(query, options))
648 }
649
babecc3c 650 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
47581df0 651 const urls: string[] = followers.map(f => f.selectionUrl)
50d6de9c
C
652
653 return {
654 data: urls,
babecc3c 655 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
50d6de9c
C
656 }
657 }
658
60650c77
C
659 private static listBadActorFollows () {
660 const query = {
661 where: {
662 score: {
1735c825 663 [Op.lte]: 0
60650c77 664 }
54e74059 665 },
23e27dd5 666 logging: false
60650c77
C
667 }
668
669 return ActorFollowModel.findAll(query)
670 }
671
1ca9f7c3 672 toFormattedJSON (this: MActorFollowFormattable): ActorFollow {
50d6de9c
C
673 const follower = this.ActorFollower.toFormattedJSON()
674 const following = this.ActorFollowing.toFormattedJSON()
675
676 return {
677 id: this.id,
678 follower,
679 following,
60650c77 680 score: this.score,
50d6de9c
C
681 state: this.state,
682 createdAt: this.createdAt,
683 updatedAt: this.updatedAt
684 }
685 }
686}