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