]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor-follow.ts
f9b4f57f3413d9a66d117c971d06c6eae1263f17
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor-follow.ts
1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
3 import * as Sequelize from 'sequelize'
4 import {
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
20 } from 'sequelize-typescript'
21 import { FollowState } from '../../../shared/models/actors'
22 import { ActorFollow } from '../../../shared/models/actors/follow.model'
23 import { logger } from '../../helpers/logger'
24 import { getServerActor } from '../../helpers/utils'
25 import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants'
26 import { ServerModel } from '../server/server'
27 import { getSort } from '../utils'
28 import { ActorModel, unusedActorAttributesForAPI } from './actor'
29 import { VideoChannelModel } from '../video/video-channel'
30 import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
31 import { AccountModel } from '../account/account'
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
45 },
46 {
47 fields: [ 'score' ]
48 }
49 ]
50 })
51 export class ActorFollowModel extends Model<ActorFollowModel> {
52
53 @AllowNull(false)
54 @Column(DataType.ENUM(values(FOLLOW_STATES)))
55 state: FollowState
56
57 @AllowNull(false)
58 @Default(ACTOR_FOLLOW_SCORE.BASE)
59 @IsInt
60 @Max(ACTOR_FOLLOW_SCORE.MAX)
61 @Column
62 score: number
63
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
98 @AfterCreate
99 @AfterUpdate
100 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
101 if (instance.state !== 'accepted') return undefined
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
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
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
153 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
154 const actorFollowingPartInclude: IIncludeOptions = {
155 model: ActorModel,
156 required: true,
157 as: 'ActorFollowing',
158 where: {
159 preferredUsername: targetName
160 },
161 include: [
162 {
163 model: VideoChannelModel.unscoped(),
164 required: false
165 }
166 ]
167 }
168
169 if (targetHost === null) {
170 actorFollowingPartInclude.where['serverId'] = null
171 } else {
172 actorFollowingPartInclude.include.push({
173 model: ServerModel,
174 required: true,
175 where: {
176 host: targetHost
177 }
178 })
179 }
180
181 const query = {
182 where: {
183 actorId
184 },
185 include: [
186 actorFollowingPartInclude,
187 {
188 model: ActorModel,
189 required: true,
190 as: 'ActorFollower'
191 }
192 ],
193 transaction: t
194 }
195
196 return ActorFollowModel.findOne(query)
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)
264 }
265
266 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
267 const query = {
268 distinct: true,
269 offset: start,
270 limit: count,
271 order: getSort(sort),
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,
285 include: [
286 {
287 model: ServerModel,
288 required: true,
289 where: search ? {
290 host: {
291 [Sequelize.Op.iLike]: '%' + search + '%'
292 }
293 } : undefined
294 }
295 ]
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
309 static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) {
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: {
337 id: actorId
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
352 static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
353 const query = {
354 attributes: [],
355 distinct: true,
356 offset: start,
357 limit: count,
358 order: getSort(sort),
359 where: {
360 actorId: actorId
361 },
362 include: [
363 {
364 attributes: [ 'id' ],
365 model: ActorModel.unscoped(),
366 as: 'ActorFollowing',
367 required: true,
368 include: [
369 {
370 model: VideoChannelModel.unscoped(),
371 required: true,
372 include: [
373 {
374 attributes: {
375 exclude: unusedActorAttributesForAPI
376 },
377 model: ActorModel,
378 required: true
379 },
380 {
381 model: AccountModel.unscoped(),
382 required: true,
383 include: [
384 {
385 attributes: {
386 exclude: unusedActorAttributesForAPI
387 },
388 model: ActorModel,
389 required: true
390 }
391 ]
392 }
393 ]
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
409 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
410 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
411 }
412
413 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
414 return ActorFollowModel.createListAcceptedFollowForApiQuery(
415 'followers',
416 actorIds,
417 t,
418 undefined,
419 undefined,
420 'sharedInboxUrl',
421 true
422 )
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
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
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 (' +
453 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
454 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
455 `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
456 ')'
457
458 const options = {
459 type: Sequelize.QueryTypes.BULKUPDATE,
460 transaction: t
461 }
462
463 return ActorFollowModel.sequelize.query(query, options)
464 }
465
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 ) {
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
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
492 const tasks: Bluebird<any>[] = []
493
494 for (let selection of selections) {
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
511 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
512 const urls: string[] = followers.map(f => f.url)
513
514 return {
515 data: urls,
516 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
517 }
518 }
519
520 private static listBadActorFollows () {
521 const query = {
522 where: {
523 score: {
524 [Sequelize.Op.lte]: 0
525 }
526 },
527 logging: false
528 }
529
530 return ActorFollowModel.findAll(query)
531 }
532
533 toFormattedJSON (): ActorFollow {
534 const follower = this.ActorFollower.toFormattedJSON()
535 const following = this.ActorFollowing.toFormattedJSON()
536
537 return {
538 id: this.id,
539 follower,
540 following,
541 score: this.score,
542 state: this.state,
543 createdAt: this.createdAt,
544 updatedAt: this.updatedAt
545 }
546 }
547 }