diff options
Diffstat (limited to 'server/models/actor/actor-follow.ts')
-rw-r--r-- | server/models/actor/actor-follow.ts | 662 |
1 files changed, 0 insertions, 662 deletions
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts deleted file mode 100644 index 71ce9fa6f..000000000 --- a/server/models/actor/actor-follow.ts +++ /dev/null | |||
@@ -1,662 +0,0 @@ | |||
1 | import { difference } from 'lodash' | ||
2 | import { Attributes, FindOptions, Includeable, IncludeOptions, Op, QueryTypes, Transaction, WhereAttributeHash } from 'sequelize' | ||
3 | import { | ||
4 | AfterCreate, | ||
5 | AfterDestroy, | ||
6 | AfterUpdate, | ||
7 | AllowNull, | ||
8 | BelongsTo, | ||
9 | Column, | ||
10 | CreatedAt, | ||
11 | DataType, | ||
12 | Default, | ||
13 | ForeignKey, | ||
14 | Is, | ||
15 | IsInt, | ||
16 | Max, | ||
17 | Model, | ||
18 | Table, | ||
19 | UpdatedAt | ||
20 | } from 'sequelize-typescript' | ||
21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
22 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
23 | import { getServerActor } from '@server/models/application/application' | ||
24 | import { | ||
25 | MActor, | ||
26 | MActorFollowActors, | ||
27 | MActorFollowActorsDefault, | ||
28 | MActorFollowActorsDefaultSubscription, | ||
29 | MActorFollowFollowingHost, | ||
30 | MActorFollowFormattable, | ||
31 | MActorFollowSubscriptions | ||
32 | } from '@server/types/models' | ||
33 | import { AttributesOnly } from '@shared/typescript-utils' | ||
34 | import { FollowState } from '../../../shared/models/actors' | ||
35 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | ||
36 | import { logger } from '../../helpers/logger' | ||
37 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants' | ||
38 | import { AccountModel } from '../account/account' | ||
39 | import { ServerModel } from '../server/server' | ||
40 | import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' | ||
41 | import { doesExist } from '../shared/query' | ||
42 | import { VideoChannelModel } from '../video/video-channel' | ||
43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | ||
44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | ||
45 | import { InstanceListFollowingQueryBuilder, ListFollowingOptions } from './sql/instance-list-following-query-builder' | ||
46 | |||
47 | @Table({ | ||
48 | tableName: 'actorFollow', | ||
49 | indexes: [ | ||
50 | { | ||
51 | fields: [ 'actorId' ] | ||
52 | }, | ||
53 | { | ||
54 | fields: [ 'targetActorId' ] | ||
55 | }, | ||
56 | { | ||
57 | fields: [ 'actorId', 'targetActorId' ], | ||
58 | unique: true | ||
59 | }, | ||
60 | { | ||
61 | fields: [ 'score' ] | ||
62 | }, | ||
63 | { | ||
64 | fields: [ 'url' ], | ||
65 | unique: true | ||
66 | } | ||
67 | ] | ||
68 | }) | ||
69 | export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowModel>>> { | ||
70 | |||
71 | @AllowNull(false) | ||
72 | @Column(DataType.ENUM(...Object.values(FOLLOW_STATES))) | ||
73 | state: FollowState | ||
74 | |||
75 | @AllowNull(false) | ||
76 | @Default(ACTOR_FOLLOW_SCORE.BASE) | ||
77 | @IsInt | ||
78 | @Max(ACTOR_FOLLOW_SCORE.MAX) | ||
79 | @Column | ||
80 | score: number | ||
81 | |||
82 | // Allow null because we added this column in PeerTube v3, and don't want to generate fake URLs of remote follows | ||
83 | @AllowNull(true) | ||
84 | @Is('ActorFollowUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
85 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) | ||
86 | url: string | ||
87 | |||
88 | @CreatedAt | ||
89 | createdAt: Date | ||
90 | |||
91 | @UpdatedAt | ||
92 | updatedAt: Date | ||
93 | |||
94 | @ForeignKey(() => ActorModel) | ||
95 | @Column | ||
96 | actorId: number | ||
97 | |||
98 | @BelongsTo(() => ActorModel, { | ||
99 | foreignKey: { | ||
100 | name: 'actorId', | ||
101 | allowNull: false | ||
102 | }, | ||
103 | as: 'ActorFollower', | ||
104 | onDelete: 'CASCADE' | ||
105 | }) | ||
106 | ActorFollower: ActorModel | ||
107 | |||
108 | @ForeignKey(() => ActorModel) | ||
109 | @Column | ||
110 | targetActorId: number | ||
111 | |||
112 | @BelongsTo(() => ActorModel, { | ||
113 | foreignKey: { | ||
114 | name: 'targetActorId', | ||
115 | allowNull: false | ||
116 | }, | ||
117 | as: 'ActorFollowing', | ||
118 | onDelete: 'CASCADE' | ||
119 | }) | ||
120 | ActorFollowing: ActorModel | ||
121 | |||
122 | @AfterCreate | ||
123 | @AfterUpdate | ||
124 | static incrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { | ||
125 | return afterCommitIfTransaction(options.transaction, () => { | ||
126 | return Promise.all([ | ||
127 | ActorModel.rebuildFollowsCount(instance.actorId, 'following'), | ||
128 | ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers') | ||
129 | ]) | ||
130 | }) | ||
131 | } | ||
132 | |||
133 | @AfterDestroy | ||
134 | static decrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { | ||
135 | return afterCommitIfTransaction(options.transaction, () => { | ||
136 | return Promise.all([ | ||
137 | ActorModel.rebuildFollowsCount(instance.actorId, 'following'), | ||
138 | ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers') | ||
139 | ]) | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | // --------------------------------------------------------------------------- | ||
144 | |||
145 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
146 | return buildSQLAttributes({ | ||
147 | model: this, | ||
148 | tableName, | ||
149 | aliasPrefix | ||
150 | }) | ||
151 | } | ||
152 | |||
153 | // --------------------------------------------------------------------------- | ||
154 | |||
155 | /* | ||
156 | * @deprecated Use `findOrCreateCustom` instead | ||
157 | */ | ||
158 | static findOrCreate (): any { | ||
159 | throw new Error('Must not be called') | ||
160 | } | ||
161 | |||
162 | // findOrCreate has issues with actor follow hooks | ||
163 | static async findOrCreateCustom (options: { | ||
164 | byActor: MActor | ||
165 | targetActor: MActor | ||
166 | activityId: string | ||
167 | state: FollowState | ||
168 | transaction: Transaction | ||
169 | }): Promise<[ MActorFollowActors, boolean ]> { | ||
170 | const { byActor, targetActor, activityId, state, transaction } = options | ||
171 | |||
172 | let created = false | ||
173 | let actorFollow: MActorFollowActors = await ActorFollowModel.loadByActorAndTarget(byActor.id, targetActor.id, transaction) | ||
174 | |||
175 | if (!actorFollow) { | ||
176 | created = true | ||
177 | |||
178 | actorFollow = await ActorFollowModel.create({ | ||
179 | actorId: byActor.id, | ||
180 | targetActorId: targetActor.id, | ||
181 | url: activityId, | ||
182 | |||
183 | state | ||
184 | }, { transaction }) | ||
185 | |||
186 | actorFollow.ActorFollowing = targetActor | ||
187 | actorFollow.ActorFollower = byActor | ||
188 | } | ||
189 | |||
190 | return [ actorFollow, created ] | ||
191 | } | ||
192 | |||
193 | static removeFollowsOf (actorId: number, t?: Transaction) { | ||
194 | const query = { | ||
195 | where: { | ||
196 | [Op.or]: [ | ||
197 | { | ||
198 | actorId | ||
199 | }, | ||
200 | { | ||
201 | targetActorId: actorId | ||
202 | } | ||
203 | ] | ||
204 | }, | ||
205 | transaction: t | ||
206 | } | ||
207 | |||
208 | return ActorFollowModel.destroy(query) | ||
209 | } | ||
210 | |||
211 | // Remove actor follows with a score of 0 (too many requests where they were unreachable) | ||
212 | static async removeBadActorFollows () { | ||
213 | const actorFollows = await ActorFollowModel.listBadActorFollows() | ||
214 | |||
215 | const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy()) | ||
216 | await Promise.all(actorFollowsRemovePromises) | ||
217 | |||
218 | const numberOfActorFollowsRemoved = actorFollows.length | ||
219 | |||
220 | if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) | ||
221 | } | ||
222 | |||
223 | static isFollowedBy (actorId: number, followerActorId: number) { | ||
224 | const query = `SELECT 1 FROM "actorFollow" ` + | ||
225 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + | ||
226 | `LIMIT 1` | ||
227 | |||
228 | return doesExist(this.sequelize, query, { actorId, followerActorId }) | ||
229 | } | ||
230 | |||
231 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | ||
232 | const query = { | ||
233 | where: { | ||
234 | actorId, | ||
235 | targetActorId | ||
236 | }, | ||
237 | include: [ | ||
238 | { | ||
239 | model: ActorModel, | ||
240 | required: true, | ||
241 | as: 'ActorFollower' | ||
242 | }, | ||
243 | { | ||
244 | model: ActorModel, | ||
245 | required: true, | ||
246 | as: 'ActorFollowing' | ||
247 | } | ||
248 | ], | ||
249 | transaction: t | ||
250 | } | ||
251 | |||
252 | return ActorFollowModel.findOne(query) | ||
253 | } | ||
254 | |||
255 | static loadByActorAndTargetNameAndHostForAPI (options: { | ||
256 | actorId: number | ||
257 | targetName: string | ||
258 | targetHost: string | ||
259 | state?: FollowState | ||
260 | transaction?: Transaction | ||
261 | }): Promise<MActorFollowActorsDefaultSubscription> { | ||
262 | const { actorId, targetHost, targetName, state, transaction } = options | ||
263 | |||
264 | const actorFollowingPartInclude: IncludeOptions = { | ||
265 | model: ActorModel, | ||
266 | required: true, | ||
267 | as: 'ActorFollowing', | ||
268 | where: ActorModel.wherePreferredUsername(targetName), | ||
269 | include: [ | ||
270 | { | ||
271 | model: VideoChannelModel.unscoped(), | ||
272 | required: false | ||
273 | } | ||
274 | ] | ||
275 | } | ||
276 | |||
277 | if (targetHost === null) { | ||
278 | actorFollowingPartInclude.where['serverId'] = null | ||
279 | } else { | ||
280 | actorFollowingPartInclude.include.push({ | ||
281 | model: ServerModel, | ||
282 | required: true, | ||
283 | where: { | ||
284 | host: targetHost | ||
285 | } | ||
286 | }) | ||
287 | } | ||
288 | |||
289 | const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId } | ||
290 | if (state) where.state = state | ||
291 | |||
292 | const query: FindOptions<Attributes<ActorFollowModel>> = { | ||
293 | where, | ||
294 | include: [ | ||
295 | actorFollowingPartInclude, | ||
296 | { | ||
297 | model: ActorModel, | ||
298 | required: true, | ||
299 | as: 'ActorFollower' | ||
300 | } | ||
301 | ], | ||
302 | transaction | ||
303 | } | ||
304 | |||
305 | return ActorFollowModel.findOne(query) | ||
306 | } | ||
307 | |||
308 | static listSubscriptionsOf (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { | ||
309 | const whereTab = targets | ||
310 | .map(t => { | ||
311 | if (t.host) { | ||
312 | return { | ||
313 | [Op.and]: [ | ||
314 | ActorModel.wherePreferredUsername(t.name), | ||
315 | { $host$: t.host } | ||
316 | ] | ||
317 | } | ||
318 | } | ||
319 | |||
320 | return { | ||
321 | [Op.and]: [ | ||
322 | ActorModel.wherePreferredUsername(t.name), | ||
323 | { $serverId$: null } | ||
324 | ] | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | const query = { | ||
329 | attributes: [ 'id' ], | ||
330 | where: { | ||
331 | [Op.and]: [ | ||
332 | { | ||
333 | [Op.or]: whereTab | ||
334 | }, | ||
335 | { | ||
336 | state: 'accepted', | ||
337 | actorId | ||
338 | } | ||
339 | ] | ||
340 | }, | ||
341 | include: [ | ||
342 | { | ||
343 | attributes: [ 'preferredUsername' ], | ||
344 | model: ActorModel.unscoped(), | ||
345 | required: true, | ||
346 | as: 'ActorFollowing', | ||
347 | include: [ | ||
348 | { | ||
349 | attributes: [ 'host' ], | ||
350 | model: ServerModel.unscoped(), | ||
351 | required: false | ||
352 | } | ||
353 | ] | ||
354 | } | ||
355 | ] | ||
356 | } | ||
357 | |||
358 | return ActorFollowModel.findAll(query) | ||
359 | } | ||
360 | |||
361 | static listInstanceFollowingForApi (options: ListFollowingOptions) { | ||
362 | return Promise.all([ | ||
363 | new InstanceListFollowingQueryBuilder(this.sequelize, options).countFollowing(), | ||
364 | new InstanceListFollowingQueryBuilder(this.sequelize, options).listFollowing() | ||
365 | ]).then(([ total, data ]) => ({ total, data })) | ||
366 | } | ||
367 | |||
368 | static listFollowersForApi (options: ListFollowersOptions) { | ||
369 | return Promise.all([ | ||
370 | new InstanceListFollowersQueryBuilder(this.sequelize, options).countFollowers(), | ||
371 | new InstanceListFollowersQueryBuilder(this.sequelize, options).listFollowers() | ||
372 | ]).then(([ total, data ]) => ({ total, data })) | ||
373 | } | ||
374 | |||
375 | static listSubscriptionsForApi (options: { | ||
376 | actorId: number | ||
377 | start: number | ||
378 | count: number | ||
379 | sort: string | ||
380 | search?: string | ||
381 | }) { | ||
382 | const { actorId, start, count, sort } = options | ||
383 | const where = { | ||
384 | state: 'accepted', | ||
385 | actorId | ||
386 | } | ||
387 | |||
388 | if (options.search) { | ||
389 | Object.assign(where, { | ||
390 | [Op.or]: [ | ||
391 | searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), | ||
392 | searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$') | ||
393 | ] | ||
394 | }) | ||
395 | } | ||
396 | |||
397 | const getQuery = (forCount: boolean) => { | ||
398 | let channelInclude: Includeable[] = [] | ||
399 | |||
400 | if (forCount !== true) { | ||
401 | channelInclude = [ | ||
402 | { | ||
403 | attributes: { | ||
404 | exclude: unusedActorAttributesForAPI | ||
405 | }, | ||
406 | model: ActorModel, | ||
407 | required: true | ||
408 | }, | ||
409 | { | ||
410 | model: AccountModel.unscoped(), | ||
411 | required: true, | ||
412 | include: [ | ||
413 | { | ||
414 | attributes: { | ||
415 | exclude: unusedActorAttributesForAPI | ||
416 | }, | ||
417 | model: ActorModel, | ||
418 | required: true | ||
419 | } | ||
420 | ] | ||
421 | } | ||
422 | ] | ||
423 | } | ||
424 | |||
425 | return { | ||
426 | attributes: forCount === true | ||
427 | ? [] | ||
428 | : SORTABLE_COLUMNS.USER_SUBSCRIPTIONS, | ||
429 | distinct: true, | ||
430 | offset: start, | ||
431 | limit: count, | ||
432 | order: getSort(sort), | ||
433 | where, | ||
434 | include: [ | ||
435 | { | ||
436 | attributes: [ 'id' ], | ||
437 | model: ActorModel.unscoped(), | ||
438 | as: 'ActorFollowing', | ||
439 | required: true, | ||
440 | include: [ | ||
441 | { | ||
442 | model: VideoChannelModel.unscoped(), | ||
443 | required: true, | ||
444 | include: channelInclude | ||
445 | } | ||
446 | ] | ||
447 | } | ||
448 | ] | ||
449 | } | ||
450 | } | ||
451 | |||
452 | return Promise.all([ | ||
453 | ActorFollowModel.count(getQuery(true)), | ||
454 | ActorFollowModel.findAll<MActorFollowSubscriptions>(getQuery(false)) | ||
455 | ]).then(([ total, rows ]) => ({ | ||
456 | total, | ||
457 | data: rows.map(r => r.ActorFollowing.VideoChannel) | ||
458 | })) | ||
459 | } | ||
460 | |||
461 | static async keepUnfollowedInstance (hosts: string[]) { | ||
462 | const followerId = (await getServerActor()).id | ||
463 | |||
464 | const query = { | ||
465 | attributes: [ 'id' ], | ||
466 | where: { | ||
467 | actorId: followerId | ||
468 | }, | ||
469 | include: [ | ||
470 | { | ||
471 | attributes: [ 'id' ], | ||
472 | model: ActorModel.unscoped(), | ||
473 | required: true, | ||
474 | as: 'ActorFollowing', | ||
475 | where: { | ||
476 | preferredUsername: SERVER_ACTOR_NAME | ||
477 | }, | ||
478 | include: [ | ||
479 | { | ||
480 | attributes: [ 'host' ], | ||
481 | model: ServerModel.unscoped(), | ||
482 | required: true, | ||
483 | where: { | ||
484 | host: { | ||
485 | [Op.in]: hosts | ||
486 | } | ||
487 | } | ||
488 | } | ||
489 | ] | ||
490 | } | ||
491 | ] | ||
492 | } | ||
493 | |||
494 | const res = await ActorFollowModel.findAll(query) | ||
495 | const followedHosts = res.map(row => row.ActorFollowing.Server.host) | ||
496 | |||
497 | return difference(hosts, followedHosts) | ||
498 | } | ||
499 | |||
500 | static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) { | ||
501 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) | ||
502 | } | ||
503 | |||
504 | static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) { | ||
505 | return ActorFollowModel.createListAcceptedFollowForApiQuery( | ||
506 | 'followers', | ||
507 | actorIds, | ||
508 | t, | ||
509 | undefined, | ||
510 | undefined, | ||
511 | 'sharedInboxUrl', | ||
512 | true | ||
513 | ) | ||
514 | } | ||
515 | |||
516 | static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) { | ||
517 | return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) | ||
518 | } | ||
519 | |||
520 | static async getStats () { | ||
521 | const serverActor = await getServerActor() | ||
522 | |||
523 | const totalInstanceFollowing = await ActorFollowModel.count({ | ||
524 | where: { | ||
525 | actorId: serverActor.id, | ||
526 | state: 'accepted' | ||
527 | } | ||
528 | }) | ||
529 | |||
530 | const totalInstanceFollowers = await ActorFollowModel.count({ | ||
531 | where: { | ||
532 | targetActorId: serverActor.id, | ||
533 | state: 'accepted' | ||
534 | } | ||
535 | }) | ||
536 | |||
537 | return { | ||
538 | totalInstanceFollowing, | ||
539 | totalInstanceFollowers | ||
540 | } | ||
541 | } | ||
542 | |||
543 | static updateScore (inboxUrl: string, value: number, t?: Transaction) { | ||
544 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
545 | 'WHERE id IN (' + | ||
546 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
547 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
548 | `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + | ||
549 | ')' | ||
550 | |||
551 | const options = { | ||
552 | type: QueryTypes.BULKUPDATE, | ||
553 | transaction: t | ||
554 | } | ||
555 | |||
556 | return ActorFollowModel.sequelize.query(query, options) | ||
557 | } | ||
558 | |||
559 | static async updateScoreByFollowingServers (serverIds: number[], value: number, t?: Transaction) { | ||
560 | if (serverIds.length === 0) return | ||
561 | |||
562 | const me = await getServerActor() | ||
563 | const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds) | ||
564 | |||
565 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
566 | 'WHERE id IN (' + | ||
567 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
568 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' + | ||
569 | `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` + // I'm the follower | ||
570 | `AND "actor"."serverId" IN (${serverIdsString})` + // Criteria on followings | ||
571 | ')' | ||
572 | |||
573 | const options = { | ||
574 | type: QueryTypes.BULKUPDATE, | ||
575 | transaction: t | ||
576 | } | ||
577 | |||
578 | return ActorFollowModel.sequelize.query(query, options) | ||
579 | } | ||
580 | |||
581 | private static async createListAcceptedFollowForApiQuery ( | ||
582 | type: 'followers' | 'following', | ||
583 | actorIds: number[], | ||
584 | t: Transaction, | ||
585 | start?: number, | ||
586 | count?: number, | ||
587 | columnUrl = 'url', | ||
588 | distinct = false | ||
589 | ) { | ||
590 | let firstJoin: string | ||
591 | let secondJoin: string | ||
592 | |||
593 | if (type === 'followers') { | ||
594 | firstJoin = 'targetActorId' | ||
595 | secondJoin = 'actorId' | ||
596 | } else { | ||
597 | firstJoin = 'actorId' | ||
598 | secondJoin = 'targetActorId' | ||
599 | } | ||
600 | |||
601 | const selections: string[] = [] | ||
602 | if (distinct === true) selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`) | ||
603 | else selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`) | ||
604 | |||
605 | selections.push('COUNT(*) AS "total"') | ||
606 | |||
607 | const tasks: Promise<any>[] = [] | ||
608 | |||
609 | for (const selection of selections) { | ||
610 | let query = 'SELECT ' + selection + ' FROM "actor" ' + | ||
611 | 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + | ||
612 | 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + | ||
613 | `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL ` | ||
614 | |||
615 | if (count !== undefined) query += 'LIMIT ' + count | ||
616 | if (start !== undefined) query += ' OFFSET ' + start | ||
617 | |||
618 | const options = { | ||
619 | bind: { actorIds }, | ||
620 | type: QueryTypes.SELECT, | ||
621 | transaction: t | ||
622 | } | ||
623 | tasks.push(ActorFollowModel.sequelize.query(query, options)) | ||
624 | } | ||
625 | |||
626 | const [ followers, [ dataTotal ] ] = await Promise.all(tasks) | ||
627 | const urls: string[] = followers.map(f => f.selectionUrl) | ||
628 | |||
629 | return { | ||
630 | data: urls, | ||
631 | total: dataTotal ? parseInt(dataTotal.total, 10) : 0 | ||
632 | } | ||
633 | } | ||
634 | |||
635 | private static listBadActorFollows () { | ||
636 | const query = { | ||
637 | where: { | ||
638 | score: { | ||
639 | [Op.lte]: 0 | ||
640 | } | ||
641 | }, | ||
642 | logging: false | ||
643 | } | ||
644 | |||
645 | return ActorFollowModel.findAll(query) | ||
646 | } | ||
647 | |||
648 | toFormattedJSON (this: MActorFollowFormattable): ActorFollow { | ||
649 | const follower = this.ActorFollower.toFormattedJSON() | ||
650 | const following = this.ActorFollowing.toFormattedJSON() | ||
651 | |||
652 | return { | ||
653 | id: this.id, | ||
654 | follower, | ||
655 | following, | ||
656 | score: this.score, | ||
657 | state: this.state, | ||
658 | createdAt: this.createdAt, | ||
659 | updatedAt: this.updatedAt | ||
660 | } | ||
661 | } | ||
662 | } | ||