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