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