]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor-follow.ts
90a8ac43c6d3f1c2904887b9498b32a89c94d43a
[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 { AccountFollow } from '../../../shared/models/actors/follow.model'
23 import { logger } from '../../helpers/logger'
24 import { getServerActor } from '../../helpers/utils'
25 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
26 import { FOLLOW_STATES } from '../../initializers/constants'
27 import { ServerModel } from '../server/server'
28 import { getSort } from '../utils'
29 import { ActorModel } from './actor'
30 import { VideoChannelModel } from '../video/video-channel'
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 })
50 export 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 // 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
128 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
129 if (goodInboxes.length === 0 && badInboxes.length === 0) return
130
131 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
132
133 if (goodInboxes.length !== 0) {
134 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
135 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
136 }
137
138 if (badInboxes.length !== 0) {
139 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
140 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
141 }
142 }
143
144 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
145 const query = {
146 where: {
147 actorId,
148 targetActorId: targetActorId
149 },
150 include: [
151 {
152 model: ActorModel,
153 required: true,
154 as: 'ActorFollower'
155 },
156 {
157 model: ActorModel,
158 required: true,
159 as: 'ActorFollowing'
160 }
161 ],
162 transaction: t
163 }
164
165 return ActorFollowModel.findOne(query)
166 }
167
168 static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
169 const actorFollowingPartInclude = {
170 model: ActorModel,
171 required: true,
172 as: 'ActorFollowing',
173 where: {
174 preferredUsername: targetName
175 }
176 }
177
178 if (targetHost === null) {
179 actorFollowingPartInclude.where['serverId'] = null
180 } else {
181 Object.assign(actorFollowingPartInclude, {
182 include: [
183 {
184 model: ServerModel,
185 required: true,
186 where: {
187 host: targetHost
188 }
189 }
190 ]
191 })
192 }
193
194 const query = {
195 where: {
196 actorId
197 },
198 include: [
199 {
200 model: ActorModel,
201 required: true,
202 as: 'ActorFollower'
203 },
204 actorFollowingPartInclude
205 ],
206 transaction: t
207 }
208
209 return ActorFollowModel.findOne(query)
210 }
211
212 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
213 const query = {
214 distinct: true,
215 offset: start,
216 limit: count,
217 order: getSort(sort),
218 include: [
219 {
220 model: ActorModel,
221 required: true,
222 as: 'ActorFollower',
223 where: {
224 id
225 }
226 },
227 {
228 model: ActorModel,
229 as: 'ActorFollowing',
230 required: true,
231 include: [ ServerModel ]
232 }
233 ]
234 }
235
236 return ActorFollowModel.findAndCountAll(query)
237 .then(({ rows, count }) => {
238 return {
239 data: rows,
240 total: count
241 }
242 })
243 }
244
245 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
246 const query = {
247 distinct: true,
248 offset: start,
249 limit: count,
250 order: getSort(sort),
251 where: {
252 actorId: id
253 },
254 include: [
255 {
256 model: ActorModel,
257 as: 'ActorFollowing',
258 required: true,
259 include: [
260 {
261 model: VideoChannelModel,
262 required: true
263 }
264 ]
265 }
266 ]
267 }
268
269 return ActorFollowModel.findAndCountAll(query)
270 .then(({ rows, count }) => {
271 return {
272 data: rows.map(r => r.ActorFollowing.VideoChannel),
273 total: count
274 }
275 })
276 }
277
278 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
279 const query = {
280 distinct: true,
281 offset: start,
282 limit: count,
283 order: getSort(sort),
284 include: [
285 {
286 model: ActorModel,
287 required: true,
288 as: 'ActorFollower',
289 include: [ ServerModel ]
290 },
291 {
292 model: ActorModel,
293 as: 'ActorFollowing',
294 required: true,
295 where: {
296 id
297 }
298 }
299 ]
300 }
301
302 return ActorFollowModel.findAndCountAll(query)
303 .then(({ rows, count }) => {
304 return {
305 data: rows,
306 total: count
307 }
308 })
309 }
310
311 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
312 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
313 }
314
315 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
316 return ActorFollowModel.createListAcceptedFollowForApiQuery(
317 'followers',
318 actorIds,
319 t,
320 undefined,
321 undefined,
322 'sharedInboxUrl',
323 true
324 )
325 }
326
327 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
328 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
329 }
330
331 static async getStats () {
332 const serverActor = await getServerActor()
333
334 const totalInstanceFollowing = await ActorFollowModel.count({
335 where: {
336 actorId: serverActor.id
337 }
338 })
339
340 const totalInstanceFollowers = await ActorFollowModel.count({
341 where: {
342 targetActorId: serverActor.id
343 }
344 })
345
346 return {
347 totalInstanceFollowing,
348 totalInstanceFollowers
349 }
350 }
351
352 private static async createListAcceptedFollowForApiQuery (
353 type: 'followers' | 'following',
354 actorIds: number[],
355 t: Sequelize.Transaction,
356 start?: number,
357 count?: number,
358 columnUrl = 'url',
359 distinct = false
360 ) {
361 let firstJoin: string
362 let secondJoin: string
363
364 if (type === 'followers') {
365 firstJoin = 'targetActorId'
366 secondJoin = 'actorId'
367 } else {
368 firstJoin = 'actorId'
369 secondJoin = 'targetActorId'
370 }
371
372 const selections: string[] = []
373 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
374 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
375
376 selections.push('COUNT(*) AS "total"')
377
378 const tasks: Bluebird<any>[] = []
379
380 for (let selection of selections) {
381 let query = 'SELECT ' + selection + ' FROM "actor" ' +
382 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
383 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
384 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
385
386 if (count !== undefined) query += 'LIMIT ' + count
387 if (start !== undefined) query += ' OFFSET ' + start
388
389 const options = {
390 bind: { actorIds },
391 type: Sequelize.QueryTypes.SELECT,
392 transaction: t
393 }
394 tasks.push(ActorFollowModel.sequelize.query(query, options))
395 }
396
397 const [ followers, [ { total } ] ] = await Promise.all(tasks)
398 const urls: string[] = followers.map(f => f.url)
399
400 return {
401 data: urls,
402 total: parseInt(total, 10)
403 }
404 }
405
406 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
407 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
408
409 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
410 'WHERE id IN (' +
411 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
412 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
413 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
414 ')'
415
416 const options = t ? {
417 type: Sequelize.QueryTypes.BULKUPDATE,
418 transaction: t
419 } : undefined
420
421 return ActorFollowModel.sequelize.query(query, options)
422 }
423
424 private static listBadActorFollows () {
425 const query = {
426 where: {
427 score: {
428 [Sequelize.Op.lte]: 0
429 }
430 },
431 logging: false
432 }
433
434 return ActorFollowModel.findAll(query)
435 }
436
437 toFormattedJSON (): AccountFollow {
438 const follower = this.ActorFollower.toFormattedJSON()
439 const following = this.ActorFollowing.toFormattedJSON()
440
441 return {
442 id: this.id,
443 follower,
444 following,
445 score: this.score,
446 state: this.state,
447 createdAt: this.createdAt,
448 updatedAt: this.updatedAt
449 }
450 }
451 }