]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor-follow.ts
920c83d885bc957fa55f40affc970e59746f995b
[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 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, Table,
6 UpdatedAt
7 } from 'sequelize-typescript'
8 import { FollowState } from '../../../shared/models/actors'
9 import { AccountFollow } from '../../../shared/models/actors/follow.model'
10 import { logger } from '../../helpers/logger'
11 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
12 import { FOLLOW_STATES } from '../../initializers/constants'
13 import { ServerModel } from '../server/server'
14 import { getSort } from '../utils'
15 import { ActorModel } from './actor'
16
17 @Table({
18 tableName: 'actorFollow',
19 indexes: [
20 {
21 fields: [ 'actorId' ]
22 },
23 {
24 fields: [ 'targetActorId' ]
25 },
26 {
27 fields: [ 'actorId', 'targetActorId' ],
28 unique: true
29 },
30 {
31 fields: [ 'score' ]
32 }
33 ]
34 })
35 export class ActorFollowModel extends Model<ActorFollowModel> {
36
37 @AllowNull(false)
38 @Column(DataType.ENUM(values(FOLLOW_STATES)))
39 state: FollowState
40
41 @AllowNull(false)
42 @Default(ACTOR_FOLLOW_SCORE.BASE)
43 @IsInt
44 @Max(ACTOR_FOLLOW_SCORE.MAX)
45 @Column
46 score: number
47
48 @CreatedAt
49 createdAt: Date
50
51 @UpdatedAt
52 updatedAt: Date
53
54 @ForeignKey(() => ActorModel)
55 @Column
56 actorId: number
57
58 @BelongsTo(() => ActorModel, {
59 foreignKey: {
60 name: 'actorId',
61 allowNull: false
62 },
63 as: 'ActorFollower',
64 onDelete: 'CASCADE'
65 })
66 ActorFollower: ActorModel
67
68 @ForeignKey(() => ActorModel)
69 @Column
70 targetActorId: number
71
72 @BelongsTo(() => ActorModel, {
73 foreignKey: {
74 name: 'targetActorId',
75 allowNull: false
76 },
77 as: 'ActorFollowing',
78 onDelete: 'CASCADE'
79 })
80 ActorFollowing: ActorModel
81
82 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
83 static async removeBadActorFollows () {
84 const actorFollows = await ActorFollowModel.listBadActorFollows()
85
86 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
87 await Promise.all(actorFollowsRemovePromises)
88
89 const numberOfActorFollowsRemoved = actorFollows.length
90
91 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
92 }
93
94 static updateActorFollowsScoreAndRemoveBadOnes (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) {
95 if (goodInboxes.length === 0 && badInboxes.length === 0) return
96
97 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
98
99 if (goodInboxes.length !== 0) {
100 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
101 .catch(err => logger.error('Cannot increment scores of good actor follows.', err))
102 }
103
104 if (badInboxes.length !== 0) {
105 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
106 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', err))
107 }
108 }
109
110 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
111 const query = {
112 where: {
113 actorId,
114 targetActorId: targetActorId
115 },
116 include: [
117 {
118 model: ActorModel,
119 required: true,
120 as: 'ActorFollower'
121 },
122 {
123 model: ActorModel,
124 required: true,
125 as: 'ActorFollowing'
126 }
127 ],
128 transaction: t
129 }
130
131 return ActorFollowModel.findOne(query)
132 }
133
134 static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
135 const query = {
136 where: {
137 actorId
138 },
139 include: [
140 {
141 model: ActorModel,
142 required: true,
143 as: 'ActorFollower'
144 },
145 {
146 model: ActorModel,
147 required: true,
148 as: 'ActorFollowing',
149 include: [
150 {
151 model: ServerModel,
152 required: true,
153 where: {
154 host: targetHost
155 }
156 }
157 ]
158 }
159 ],
160 transaction: t
161 }
162
163 return ActorFollowModel.findOne(query)
164 }
165
166 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
167 const query = {
168 distinct: true,
169 offset: start,
170 limit: count,
171 order: [ getSort(sort) ],
172 include: [
173 {
174 model: ActorModel,
175 required: true,
176 as: 'ActorFollower',
177 where: {
178 id
179 }
180 },
181 {
182 model: ActorModel,
183 as: 'ActorFollowing',
184 required: true,
185 include: [ ServerModel ]
186 }
187 ]
188 }
189
190 return ActorFollowModel.findAndCountAll(query)
191 .then(({ rows, count }) => {
192 return {
193 data: rows,
194 total: count
195 }
196 })
197 }
198
199 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
200 const query = {
201 distinct: true,
202 offset: start,
203 limit: count,
204 order: [ getSort(sort) ],
205 include: [
206 {
207 model: ActorModel,
208 required: true,
209 as: 'ActorFollower',
210 include: [ ServerModel ]
211 },
212 {
213 model: ActorModel,
214 as: 'ActorFollowing',
215 required: true,
216 where: {
217 id
218 }
219 }
220 ]
221 }
222
223 return ActorFollowModel.findAndCountAll(query)
224 .then(({ rows, count }) => {
225 return {
226 data: rows,
227 total: count
228 }
229 })
230 }
231
232 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
233 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
234 }
235
236 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
237 return ActorFollowModel.createListAcceptedFollowForApiQuery(
238 'followers',
239 actorIds,
240 t,
241 undefined,
242 undefined,
243 'sharedInboxUrl',
244 true
245 )
246 }
247
248 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
249 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
250 }
251
252 private static async createListAcceptedFollowForApiQuery (
253 type: 'followers' | 'following',
254 actorIds: number[],
255 t: Sequelize.Transaction,
256 start?: number,
257 count?: number,
258 columnUrl = 'url',
259 distinct = false
260 ) {
261 let firstJoin: string
262 let secondJoin: string
263
264 if (type === 'followers') {
265 firstJoin = 'targetActorId'
266 secondJoin = 'actorId'
267 } else {
268 firstJoin = 'actorId'
269 secondJoin = 'targetActorId'
270 }
271
272 const selections: string[] = []
273 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
274 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
275
276 selections.push('COUNT(*) AS "total"')
277
278 const tasks: Bluebird<any>[] = []
279
280 for (let selection of selections) {
281 let query = 'SELECT ' + selection + ' FROM "actor" ' +
282 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
283 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
284 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
285
286 if (count !== undefined) query += 'LIMIT ' + count
287 if (start !== undefined) query += ' OFFSET ' + start
288
289 const options = {
290 bind: { actorIds },
291 type: Sequelize.QueryTypes.SELECT,
292 transaction: t
293 }
294 tasks.push(ActorFollowModel.sequelize.query(query, options))
295 }
296
297 const [ followers, [ { total } ] ] = await
298 Promise.all(tasks)
299 const urls: string[] = followers.map(f => f.url)
300
301 return {
302 data: urls,
303 total: parseInt(total, 10)
304 }
305 }
306
307 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
308 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
309
310 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
311 'WHERE id IN (' +
312 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
313 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
314 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
315 ')'
316
317 const options = {
318 type: Sequelize.QueryTypes.BULKUPDATE,
319 transaction: t
320 }
321
322 return ActorFollowModel.sequelize.query(query, options)
323 }
324
325 private static listBadActorFollows () {
326 const query = {
327 where: {
328 score: {
329 [Sequelize.Op.lte]: 0
330 }
331 }
332 }
333
334 return ActorFollowModel.findAll(query)
335 }
336
337 toFormattedJSON (): AccountFollow {
338 const follower = this.ActorFollower.toFormattedJSON()
339 const following = this.ActorFollowing.toFormattedJSON()
340
341 return {
342 id: this.id,
343 follower,
344 following,
345 score: this.score,
346 state: this.state,
347 createdAt: this.createdAt,
348 updatedAt: this.updatedAt
349 }
350 }
351 }