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