]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/activitypub/actor-follow.ts
Add local user subscriptions
[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'
60650c77
C
22import { AccountFollow } from '../../../shared/models/actors/follow.model'
23import { logger } from '../../helpers/logger'
09cababd 24import { getServerActor } from '../../helpers/utils'
60650c77 25import { ACTOR_FOLLOW_SCORE } from '../../initializers'
50d6de9c
C
26import { FOLLOW_STATES } from '../../initializers/constants'
27import { ServerModel } from '../server/server'
28import { getSort } from '../utils'
29import { ActorModel } from './actor'
06a05d5f 30import { VideoChannelModel } from '../video/video-channel'
99492dbc 31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
22a16e36 32import { AccountModel } from '../account/account'
50d6de9c
C
33
34@Table({
35 tableName: 'actorFollow',
36 indexes: [
37 {
38 fields: [ 'actorId' ]
39 },
40 {
41 fields: [ 'targetActorId' ]
42 },
43 {
44 fields: [ 'actorId', 'targetActorId' ],
45 unique: true
60650c77
C
46 },
47 {
48 fields: [ 'score' ]
50d6de9c
C
49 }
50 ]
51})
52export class ActorFollowModel extends Model<ActorFollowModel> {
53
54 @AllowNull(false)
55 @Column(DataType.ENUM(values(FOLLOW_STATES)))
56 state: FollowState
57
60650c77
C
58 @AllowNull(false)
59 @Default(ACTOR_FOLLOW_SCORE.BASE)
60 @IsInt
61 @Max(ACTOR_FOLLOW_SCORE.MAX)
62 @Column
63 score: number
64
50d6de9c
C
65 @CreatedAt
66 createdAt: Date
67
68 @UpdatedAt
69 updatedAt: Date
70
71 @ForeignKey(() => ActorModel)
72 @Column
73 actorId: number
74
75 @BelongsTo(() => ActorModel, {
76 foreignKey: {
77 name: 'actorId',
78 allowNull: false
79 },
80 as: 'ActorFollower',
81 onDelete: 'CASCADE'
82 })
83 ActorFollower: ActorModel
84
85 @ForeignKey(() => ActorModel)
86 @Column
87 targetActorId: number
88
89 @BelongsTo(() => ActorModel, {
90 foreignKey: {
91 name: 'targetActorId',
92 allowNull: false
93 },
94 as: 'ActorFollowing',
95 onDelete: 'CASCADE'
96 })
97 ActorFollowing: ActorModel
98
32b2b43c
C
99 @AfterCreate
100 @AfterUpdate
101 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
38768a36 102 if (instance.state !== 'accepted') return undefined
32b2b43c
C
103
104 return Promise.all([
105 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
106 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
107 ])
108 }
109
110 @AfterDestroy
111 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
112 return Promise.all([
113 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
114 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
115 ])
116 }
117
60650c77
C
118 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
119 static async removeBadActorFollows () {
120 const actorFollows = await ActorFollowModel.listBadActorFollows()
121
122 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
123 await Promise.all(actorFollowsRemovePromises)
124
125 const numberOfActorFollowsRemoved = actorFollows.length
126
127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
128 }
129
c1e791ba 130 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
60650c77
C
131 if (goodInboxes.length === 0 && badInboxes.length === 0) return
132
133 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
134
135 if (goodInboxes.length !== 0) {
136 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
d5b7d911 137 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
60650c77
C
138 }
139
140 if (badInboxes.length !== 0) {
141 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
d5b7d911 142 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
60650c77
C
143 }
144 }
145
50d6de9c
C
146 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.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
06a05d5f 170 static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
99492dbc 171 const actorFollowingPartInclude: IIncludeOptions = {
06a05d5f
C
172 model: ActorModel,
173 required: true,
174 as: 'ActorFollowing',
175 where: {
176 preferredUsername: targetName
99492dbc
C
177 },
178 include: [
179 {
180 model: VideoChannelModel,
181 required: false
182 }
183 ]
06a05d5f
C
184 }
185
186 if (targetHost === null) {
187 actorFollowingPartInclude.where['serverId'] = null
188 } else {
99492dbc
C
189 actorFollowingPartInclude.include.push({
190 model: ServerModel,
191 required: true,
192 where: {
193 host: targetHost
194 }
06a05d5f
C
195 })
196 }
197
50d6de9c
C
198 const query = {
199 where: {
200 actorId
201 },
202 include: [
203 {
204 model: ActorModel,
205 required: true,
206 as: 'ActorFollower'
207 },
06a05d5f 208 actorFollowingPartInclude
50d6de9c
C
209 ],
210 transaction: t
211 }
212
6502c3d4
C
213 return ActorFollowModel.findOne(query)
214 }
215
50d6de9c
C
216 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
217 const query = {
218 distinct: true,
219 offset: start,
220 limit: count,
3bb6c526 221 order: getSort(sort),
50d6de9c
C
222 include: [
223 {
224 model: ActorModel,
225 required: true,
226 as: 'ActorFollower',
227 where: {
228 id
229 }
230 },
231 {
232 model: ActorModel,
233 as: 'ActorFollowing',
234 required: true,
235 include: [ ServerModel ]
236 }
237 ]
238 }
239
240 return ActorFollowModel.findAndCountAll(query)
241 .then(({ rows, count }) => {
242 return {
243 data: rows,
244 total: count
245 }
246 })
247 }
248
06a05d5f
C
249 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
250 const query = {
251 distinct: true,
252 offset: start,
253 limit: count,
254 order: getSort(sort),
255 where: {
256 actorId: id
257 },
258 include: [
259 {
260 model: ActorModel,
261 as: 'ActorFollowing',
262 required: true,
263 include: [
264 {
265 model: VideoChannelModel,
22a16e36
C
266 required: true,
267 include: [
268 {
269 model: AccountModel,
270 required: true
271 }
272 ]
06a05d5f
C
273 }
274 ]
275 }
276 ]
277 }
278
279 return ActorFollowModel.findAndCountAll(query)
280 .then(({ rows, count }) => {
281 return {
282 data: rows.map(r => r.ActorFollowing.VideoChannel),
283 total: count
284 }
285 })
286 }
287
50d6de9c
C
288 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
289 const query = {
290 distinct: true,
291 offset: start,
292 limit: count,
3bb6c526 293 order: getSort(sort),
50d6de9c
C
294 include: [
295 {
296 model: ActorModel,
297 required: true,
298 as: 'ActorFollower',
299 include: [ ServerModel ]
300 },
301 {
302 model: ActorModel,
303 as: 'ActorFollowing',
304 required: true,
305 where: {
306 id
307 }
308 }
309 ]
310 }
311
312 return ActorFollowModel.findAndCountAll(query)
313 .then(({ rows, count }) => {
314 return {
315 data: rows,
316 total: count
317 }
318 })
319 }
320
321 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
322 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
323 }
324
325 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
ca309a9f 326 return ActorFollowModel.createListAcceptedFollowForApiQuery(
759f8a29 327 'followers',
ca309a9f
C
328 actorIds,
329 t,
330 undefined,
331 undefined,
759f8a29
C
332 'sharedInboxUrl',
333 true
ca309a9f 334 )
50d6de9c
C
335 }
336
337 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
338 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
339 }
340
09cababd
C
341 static async getStats () {
342 const serverActor = await getServerActor()
343
344 const totalInstanceFollowing = await ActorFollowModel.count({
345 where: {
346 actorId: serverActor.id
347 }
348 })
349
350 const totalInstanceFollowers = await ActorFollowModel.count({
351 where: {
352 targetActorId: serverActor.id
353 }
354 })
355
356 return {
357 totalInstanceFollowing,
358 totalInstanceFollowers
359 }
360 }
361
759f8a29
C
362 private static async createListAcceptedFollowForApiQuery (
363 type: 'followers' | 'following',
364 actorIds: number[],
365 t: Sequelize.Transaction,
366 start?: number,
367 count?: number,
368 columnUrl = 'url',
369 distinct = false
370 ) {
50d6de9c
C
371 let firstJoin: string
372 let secondJoin: string
373
374 if (type === 'followers') {
375 firstJoin = 'targetActorId'
376 secondJoin = 'actorId'
377 } else {
378 firstJoin = 'actorId'
379 secondJoin = 'targetActorId'
380 }
381
759f8a29
C
382 const selections: string[] = []
383 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
384 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
385
386 selections.push('COUNT(*) AS "total"')
387
50d6de9c
C
388 const tasks: Bluebird<any>[] = []
389
759f8a29 390 for (let selection of selections) {
50d6de9c
C
391 let query = 'SELECT ' + selection + ' FROM "actor" ' +
392 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
393 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
394 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
395
396 if (count !== undefined) query += 'LIMIT ' + count
397 if (start !== undefined) query += ' OFFSET ' + start
398
399 const options = {
400 bind: { actorIds },
401 type: Sequelize.QueryTypes.SELECT,
402 transaction: t
403 }
404 tasks.push(ActorFollowModel.sequelize.query(query, options))
405 }
406
8fffe21a 407 const [ followers, [ { total } ] ] = await Promise.all(tasks)
50d6de9c
C
408 const urls: string[] = followers.map(f => f.url)
409
410 return {
411 data: urls,
412 total: parseInt(total, 10)
413 }
414 }
415
c1e791ba 416 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
60650c77
C
417 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
418
05bc4dfa 419 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
60650c77
C
420 'WHERE id IN (' +
421 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
422 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
423 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
424 ')'
425
c1e791ba 426 const options = t ? {
60650c77
C
427 type: Sequelize.QueryTypes.BULKUPDATE,
428 transaction: t
c1e791ba 429 } : undefined
60650c77
C
430
431 return ActorFollowModel.sequelize.query(query, options)
432 }
433
434 private static listBadActorFollows () {
435 const query = {
436 where: {
437 score: {
438 [Sequelize.Op.lte]: 0
439 }
54e74059 440 },
23e27dd5 441 logging: false
60650c77
C
442 }
443
444 return ActorFollowModel.findAll(query)
445 }
446
447 toFormattedJSON (): AccountFollow {
50d6de9c
C
448 const follower = this.ActorFollower.toFormattedJSON()
449 const following = this.ActorFollowing.toFormattedJSON()
450
451 return {
452 id: this.id,
453 follower,
454 following,
60650c77 455 score: this.score,
50d6de9c
C
456 state: this.state,
457 createdAt: this.createdAt,
458 updatedAt: this.updatedAt
459 }
460 }
461}