1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
19 } from 'sequelize-typescript'
20 import { FollowState } from '../../../shared/models/actors'
21 import { ActorFollow } from '../../../shared/models/actors/follow.model'
22 import { logger } from '../../helpers/logger'
23 import { getServerActor } from '../../helpers/utils'
24 import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants'
25 import { ServerModel } from '../server/server'
26 import { createSafeIn, getSort } from '../utils'
27 import { ActorModel, unusedActorAttributesForAPI } from './actor'
28 import { VideoChannelModel } from '../video/video-channel'
29 import { AccountModel } from '../account/account'
30 import { IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize'
32 MActorFollowActorsDefault,
33 MActorFollowActorsDefaultSubscription,
34 MActorFollowFollowingHost,
35 MActorFollowSubscriptions
36 } from '@server/typings/models'
39 tableName: 'actorFollow',
45 fields: [ 'targetActorId' ]
48 fields: [ 'actorId', 'targetActorId' ],
56 export class ActorFollowModel extends Model<ActorFollowModel> {
59 @Column(DataType.ENUM(...values(FOLLOW_STATES)))
63 @Default(ACTOR_FOLLOW_SCORE.BASE)
65 @Max(ACTOR_FOLLOW_SCORE.MAX)
75 @ForeignKey(() => ActorModel)
79 @BelongsTo(() => ActorModel, {
87 ActorFollower: ActorModel
89 @ForeignKey(() => ActorModel)
93 @BelongsTo(() => ActorModel, {
95 name: 'targetActorId',
101 ActorFollowing: ActorModel
105 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
106 if (instance.state !== 'accepted') return undefined
109 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
110 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
115 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
117 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
118 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
122 static removeFollowsOf (actorId: number, t?: Transaction) {
130 targetActorId: actorId
137 return ActorFollowModel.destroy(query)
140 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
141 static async removeBadActorFollows () {
142 const actorFollows = await ActorFollowModel.listBadActorFollows()
144 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
145 await Promise.all(actorFollowsRemovePromises)
147 const numberOfActorFollowsRemoved = actorFollows.length
149 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
152 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
156 targetActorId: targetActorId
173 return ActorFollowModel.findOne(query)
176 static loadByActorAndTargetNameAndHostForAPI (
181 ): Bluebird<MActorFollowActorsDefaultSubscription> {
182 const actorFollowingPartInclude: IncludeOptions = {
185 as: 'ActorFollowing',
187 preferredUsername: targetName
191 model: VideoChannelModel.unscoped(),
197 if (targetHost === null) {
198 actorFollowingPartInclude.where['serverId'] = null
200 actorFollowingPartInclude.include.push({
214 actorFollowingPartInclude,
224 return ActorFollowModel.findOne(query)
226 if (result && result.ActorFollowing.VideoChannel) {
227 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
234 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Bluebird<MActorFollowFollowingHost[]> {
235 const whereTab = targets
241 '$preferredUsername$': t.name
253 '$preferredUsername$': t.name
276 attributes: [ 'preferredUsername' ],
277 model: ActorModel.unscoped(),
279 as: 'ActorFollowing',
282 attributes: [ 'host' ],
283 model: ServerModel.unscoped(),
291 return ActorFollowModel.findAll(query)
294 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
299 order: getSort(sort),
311 as: 'ActorFollowing',
319 [Op.iLike]: '%' + search + '%'
328 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
329 .then(({ rows, count }) => {
337 static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) {
342 order: getSort(sort),
354 [ Op.iLike ]: '%' + search + '%'
362 as: 'ActorFollowing',
371 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
372 .then(({ rows, count }) => {
380 static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
386 order: getSort(sort),
392 attributes: [ 'id' ],
393 model: ActorModel.unscoped(),
394 as: 'ActorFollowing',
398 model: VideoChannelModel.unscoped(),
403 exclude: unusedActorAttributesForAPI
409 model: AccountModel.unscoped(),
414 exclude: unusedActorAttributesForAPI
428 return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
429 .then(({ rows, count }) => {
431 data: rows.map(r => r.ActorFollowing.VideoChannel),
437 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
438 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
441 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
442 return ActorFollowModel.createListAcceptedFollowForApiQuery(
453 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
454 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
457 static async getStats () {
458 const serverActor = await getServerActor()
460 const totalInstanceFollowing = await ActorFollowModel.count({
462 actorId: serverActor.id
466 const totalInstanceFollowers = await ActorFollowModel.count({
468 targetActorId: serverActor.id
473 totalInstanceFollowing,
474 totalInstanceFollowers
478 static updateScore (inboxUrl: string, value: number, t?: Transaction) {
479 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
481 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
482 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
483 `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
487 type: QueryTypes.BULKUPDATE,
491 return ActorFollowModel.sequelize.query(query, options)
494 static async updateScoreByFollowingServers (serverIds: number[], value: number, t?: Transaction) {
495 if (serverIds.length === 0) return
497 const me = await getServerActor()
498 const serverIdsString = createSafeIn(ActorFollowModel, serverIds)
500 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
502 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
503 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' +
504 `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` + // I'm the follower
505 `AND "actor"."serverId" IN (${serverIdsString})` + // Criteria on followings
509 type: QueryTypes.BULKUPDATE,
513 return ActorFollowModel.sequelize.query(query, options)
516 private static async createListAcceptedFollowForApiQuery (
517 type: 'followers' | 'following',
525 let firstJoin: string
526 let secondJoin: string
528 if (type === 'followers') {
529 firstJoin = 'targetActorId'
530 secondJoin = 'actorId'
532 firstJoin = 'actorId'
533 secondJoin = 'targetActorId'
536 const selections: string[] = []
537 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
538 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
540 selections.push('COUNT(*) AS "total"')
542 const tasks: Bluebird<any>[] = []
544 for (let selection of selections) {
545 let query = 'SELECT ' + selection + ' FROM "actor" ' +
546 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
547 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
548 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
550 if (count !== undefined) query += 'LIMIT ' + count
551 if (start !== undefined) query += ' OFFSET ' + start
555 type: QueryTypes.SELECT,
558 tasks.push(ActorFollowModel.sequelize.query(query, options))
561 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
562 const urls: string[] = followers.map(f => f.url)
566 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
570 private static listBadActorFollows () {
580 return ActorFollowModel.findAll(query)
583 toFormattedJSON (): ActorFollow {
584 const follower = this.ActorFollower.toFormattedJSON()
585 const following = this.ActorFollowing.toFormattedJSON()
593 createdAt: this.createdAt,
594 updatedAt: this.updatedAt