"use strict";
var ActorFollowModel_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActorFollowModel = void 0;
const tslib_1 = require("tslib");
const lodash_1 = require("lodash");
const sequelize_1 = require("sequelize");
const sequelize_typescript_1 = require("sequelize-typescript");
const misc_1 = require("../../helpers/custom-validators/activitypub/misc");
const database_utils_1 = require("../../helpers/database-utils");
const application_1 = require("../application/application");
const logger_1 = require("../../helpers/logger");
const constants_1 = require("../../initializers/constants");
const account_1 = require("../account/account");
const server_1 = require("../server/server");
const query_1 = require("../shared/query");
const utils_1 = require("../utils");
const video_channel_1 = require("../video/video-channel");
const actor_1 = require("./actor");
const instance_list_followers_query_builder_1 = require("./sql/instance-list-followers-query-builder");
const instance_list_following_query_builder_1 = require("./sql/instance-list-following-query-builder");
let ActorFollowModel = ActorFollowModel_1 = class ActorFollowModel extends sequelize_typescript_1.Model {
    static incrementFollowerAndFollowingCount(instance, options) {
        return (0, database_utils_1.afterCommitIfTransaction)(options.transaction, () => {
            return Promise.all([
                actor_1.ActorModel.rebuildFollowsCount(instance.actorId, 'following'),
                actor_1.ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers')
            ]);
        });
    }
    static decrementFollowerAndFollowingCount(instance, options) {
        return (0, database_utils_1.afterCommitIfTransaction)(options.transaction, () => {
            return Promise.all([
                actor_1.ActorModel.rebuildFollowsCount(instance.actorId, 'following'),
                actor_1.ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers')
            ]);
        });
    }
    static findOrCreate() {
        throw new Error('Must not be called');
    }
    static findOrCreateCustom(options) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { byActor, targetActor, activityId, state, transaction } = options;
            let created = false;
            let actorFollow = yield ActorFollowModel_1.loadByActorAndTarget(byActor.id, targetActor.id, transaction);
            if (!actorFollow) {
                created = true;
                actorFollow = yield ActorFollowModel_1.create({
                    actorId: byActor.id,
                    targetActorId: targetActor.id,
                    url: activityId,
                    state
                }, { transaction });
                actorFollow.ActorFollowing = targetActor;
                actorFollow.ActorFollower = byActor;
            }
            return [actorFollow, created];
        });
    }
    static removeFollowsOf(actorId, t) {
        const query = {
            where: {
                [sequelize_1.Op.or]: [
                    {
                        actorId
                    },
                    {
                        targetActorId: actorId
                    }
                ]
            },
            transaction: t
        };
        return ActorFollowModel_1.destroy(query);
    }
    static removeBadActorFollows() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const actorFollows = yield ActorFollowModel_1.listBadActorFollows();
            const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy());
            yield Promise.all(actorFollowsRemovePromises);
            const numberOfActorFollowsRemoved = actorFollows.length;
            if (numberOfActorFollowsRemoved)
                logger_1.logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved);
        });
    }
    static isFollowedBy(actorId, followerActorId) {
        const query = `SELECT 1 FROM "actorFollow" ` +
            `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` +
            `LIMIT 1`;
        return (0, query_1.doesExist)(query, { actorId, followerActorId });
    }
    static loadByActorAndTarget(actorId, targetActorId, t) {
        const query = {
            where: {
                actorId,
                targetActorId
            },
            include: [
                {
                    model: actor_1.ActorModel,
                    required: true,
                    as: 'ActorFollower'
                },
                {
                    model: actor_1.ActorModel,
                    required: true,
                    as: 'ActorFollowing'
                }
            ],
            transaction: t
        };
        return ActorFollowModel_1.findOne(query);
    }
    static loadByActorAndTargetNameAndHostForAPI(options) {
        const { actorId, targetHost, targetName, state, transaction } = options;
        const actorFollowingPartInclude = {
            model: actor_1.ActorModel,
            required: true,
            as: 'ActorFollowing',
            where: {
                preferredUsername: targetName
            },
            include: [
                {
                    model: video_channel_1.VideoChannelModel.unscoped(),
                    required: false
                }
            ]
        };
        if (targetHost === null) {
            actorFollowingPartInclude.where['serverId'] = null;
        }
        else {
            actorFollowingPartInclude.include.push({
                model: server_1.ServerModel,
                required: true,
                where: {
                    host: targetHost
                }
            });
        }
        const where = { actorId };
        if (state)
            where.state = state;
        const query = {
            where,
            include: [
                actorFollowingPartInclude,
                {
                    model: actor_1.ActorModel,
                    required: true,
                    as: 'ActorFollower'
                }
            ],
            transaction
        };
        return ActorFollowModel_1.findOne(query);
    }
    static listSubscriptionsOf(actorId, targets) {
        const whereTab = targets
            .map(t => {
            if (t.host) {
                return {
                    [sequelize_1.Op.and]: [
                        {
                            $preferredUsername$: t.name
                        },
                        {
                            $host$: t.host
                        }
                    ]
                };
            }
            return {
                [sequelize_1.Op.and]: [
                    {
                        $preferredUsername$: t.name
                    },
                    {
                        $serverId$: null
                    }
                ]
            };
        });
        const query = {
            attributes: ['id'],
            where: {
                [sequelize_1.Op.and]: [
                    {
                        [sequelize_1.Op.or]: whereTab
                    },
                    {
                        state: 'accepted',
                        actorId
                    }
                ]
            },
            include: [
                {
                    attributes: ['preferredUsername'],
                    model: actor_1.ActorModel.unscoped(),
                    required: true,
                    as: 'ActorFollowing',
                    include: [
                        {
                            attributes: ['host'],
                            model: server_1.ServerModel.unscoped(),
                            required: false
                        }
                    ]
                }
            ]
        };
        return ActorFollowModel_1.findAll(query);
    }
    static listInstanceFollowingForApi(options) {
        return Promise.all([
            new instance_list_following_query_builder_1.InstanceListFollowingQueryBuilder(this.sequelize, options).countFollowing(),
            new instance_list_following_query_builder_1.InstanceListFollowingQueryBuilder(this.sequelize, options).listFollowing()
        ]).then(([total, data]) => ({ total, data }));
    }
    static listFollowersForApi(options) {
        return Promise.all([
            new instance_list_followers_query_builder_1.InstanceListFollowersQueryBuilder(this.sequelize, options).countFollowers(),
            new instance_list_followers_query_builder_1.InstanceListFollowersQueryBuilder(this.sequelize, options).listFollowers()
        ]).then(([total, data]) => ({ total, data }));
    }
    static listSubscriptionsForApi(options) {
        const { actorId, start, count, sort } = options;
        const where = {
            state: 'accepted',
            actorId
        };
        if (options.search) {
            Object.assign(where, {
                [sequelize_1.Op.or]: [
                    (0, utils_1.searchAttribute)(options.search, '$ActorFollowing.preferredUsername$'),
                    (0, utils_1.searchAttribute)(options.search, '$ActorFollowing.VideoChannel.name$')
                ]
            });
        }
        const getQuery = (forCount) => {
            let channelInclude = [];
            if (forCount !== true) {
                channelInclude = [
                    {
                        attributes: {
                            exclude: actor_1.unusedActorAttributesForAPI
                        },
                        model: actor_1.ActorModel,
                        required: true
                    },
                    {
                        model: account_1.AccountModel.unscoped(),
                        required: true,
                        include: [
                            {
                                attributes: {
                                    exclude: actor_1.unusedActorAttributesForAPI
                                },
                                model: actor_1.ActorModel,
                                required: true
                            }
                        ]
                    }
                ];
            }
            return {
                attributes: forCount === true
                    ? []
                    : constants_1.SORTABLE_COLUMNS.USER_SUBSCRIPTIONS,
                distinct: true,
                offset: start,
                limit: count,
                order: (0, utils_1.getSort)(sort),
                where,
                include: [
                    {
                        attributes: ['id'],
                        model: actor_1.ActorModel.unscoped(),
                        as: 'ActorFollowing',
                        required: true,
                        include: [
                            {
                                model: video_channel_1.VideoChannelModel.unscoped(),
                                required: true,
                                include: channelInclude
                            }
                        ]
                    }
                ]
            };
        };
        return Promise.all([
            ActorFollowModel_1.count(getQuery(true)),
            ActorFollowModel_1.findAll(getQuery(false))
        ]).then(([total, rows]) => ({
            total,
            data: rows.map(r => r.ActorFollowing.VideoChannel)
        }));
    }
    static keepUnfollowedInstance(hosts) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const followerId = (yield (0, application_1.getServerActor)()).id;
            const query = {
                attributes: ['id'],
                where: {
                    actorId: followerId
                },
                include: [
                    {
                        attributes: ['id'],
                        model: actor_1.ActorModel.unscoped(),
                        required: true,
                        as: 'ActorFollowing',
                        where: {
                            preferredUsername: constants_1.SERVER_ACTOR_NAME
                        },
                        include: [
                            {
                                attributes: ['host'],
                                model: server_1.ServerModel.unscoped(),
                                required: true,
                                where: {
                                    host: {
                                        [sequelize_1.Op.in]: hosts
                                    }
                                }
                            }
                        ]
                    }
                ]
            };
            const res = yield ActorFollowModel_1.findAll(query);
            const followedHosts = res.map(row => row.ActorFollowing.Server.host);
            return (0, lodash_1.difference)(hosts, followedHosts);
        });
    }
    static listAcceptedFollowerUrlsForAP(actorIds, t, start, count) {
        return ActorFollowModel_1.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count);
    }
    static listAcceptedFollowerSharedInboxUrls(actorIds, t) {
        return ActorFollowModel_1.createListAcceptedFollowForApiQuery('followers', actorIds, t, undefined, undefined, 'sharedInboxUrl', true);
    }
    static listAcceptedFollowingUrlsForApi(actorIds, t, start, count) {
        return ActorFollowModel_1.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count);
    }
    static getStats() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const serverActor = yield (0, application_1.getServerActor)();
            const totalInstanceFollowing = yield ActorFollowModel_1.count({
                where: {
                    actorId: serverActor.id,
                    state: 'accepted'
                }
            });
            const totalInstanceFollowers = yield ActorFollowModel_1.count({
                where: {
                    targetActorId: serverActor.id,
                    state: 'accepted'
                }
            });
            return {
                totalInstanceFollowing,
                totalInstanceFollowers
            };
        });
    }
    static updateScore(inboxUrl, value, t) {
        const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${constants_1.ACTOR_FOLLOW_SCORE.MAX}) ` +
            'WHERE id IN (' +
            'SELECT "actorFollow"."id" FROM "actorFollow" ' +
            'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
            `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
            ')';
        const options = {
            type: sequelize_1.QueryTypes.BULKUPDATE,
            transaction: t
        };
        return ActorFollowModel_1.sequelize.query(query, options);
    }
    static updateScoreByFollowingServers(serverIds, value, t) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (serverIds.length === 0)
                return;
            const me = yield (0, application_1.getServerActor)();
            const serverIdsString = (0, utils_1.createSafeIn)(ActorFollowModel_1.sequelize, serverIds);
            const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${constants_1.ACTOR_FOLLOW_SCORE.MAX}) ` +
                'WHERE id IN (' +
                'SELECT "actorFollow"."id" FROM "actorFollow" ' +
                'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' +
                `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` +
                `AND "actor"."serverId" IN (${serverIdsString})` +
                ')';
            const options = {
                type: sequelize_1.QueryTypes.BULKUPDATE,
                transaction: t
            };
            return ActorFollowModel_1.sequelize.query(query, options);
        });
    }
    static createListAcceptedFollowForApiQuery(type, actorIds, t, start, count, columnUrl = 'url', distinct = false) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let firstJoin;
            let secondJoin;
            if (type === 'followers') {
                firstJoin = 'targetActorId';
                secondJoin = 'actorId';
            }
            else {
                firstJoin = 'actorId';
                secondJoin = 'targetActorId';
            }
            const selections = [];
            if (distinct === true)
                selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`);
            else
                selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`);
            selections.push('COUNT(*) AS "total"');
            const tasks = [];
            for (const selection of selections) {
                let query = 'SELECT ' + selection + ' FROM "actor" ' +
                    'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
                    'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
                    `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL `;
                if (count !== undefined)
                    query += 'LIMIT ' + count;
                if (start !== undefined)
                    query += ' OFFSET ' + start;
                const options = {
                    bind: { actorIds },
                    type: sequelize_1.QueryTypes.SELECT,
                    transaction: t
                };
                tasks.push(ActorFollowModel_1.sequelize.query(query, options));
            }
            const [followers, [dataTotal]] = yield Promise.all(tasks);
            const urls = followers.map(f => f.selectionUrl);
            return {
                data: urls,
                total: dataTotal ? parseInt(dataTotal.total, 10) : 0
            };
        });
    }
    static listBadActorFollows() {
        const query = {
            where: {
                score: {
                    [sequelize_1.Op.lte]: 0
                }
            },
            logging: false
        };
        return ActorFollowModel_1.findAll(query);
    }
    toFormattedJSON() {
        const follower = this.ActorFollower.toFormattedJSON();
        const following = this.ActorFollowing.toFormattedJSON();
        return {
            id: this.id,
            follower,
            following,
            score: this.score,
            state: this.state,
            createdAt: this.createdAt,
            updatedAt: this.updatedAt
        };
    }
};
tslib_1.__decorate([
    (0, sequelize_typescript_1.AllowNull)(false),
    (0, sequelize_typescript_1.Column)(sequelize_typescript_1.DataType.ENUM(...Object.values(constants_1.FOLLOW_STATES))),
    tslib_1.__metadata("design:type", String)
], ActorFollowModel.prototype, "state", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.AllowNull)(false),
    (0, sequelize_typescript_1.Default)(constants_1.ACTOR_FOLLOW_SCORE.BASE),
    sequelize_typescript_1.IsInt,
    (0, sequelize_typescript_1.Max)(constants_1.ACTOR_FOLLOW_SCORE.MAX),
    sequelize_typescript_1.Column,
    tslib_1.__metadata("design:type", Number)
], ActorFollowModel.prototype, "score", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.AllowNull)(true),
    (0, sequelize_typescript_1.Is)('ActorFollowUrl', value => (0, utils_1.throwIfNotValid)(value, misc_1.isActivityPubUrlValid, 'url')),
    (0, sequelize_typescript_1.Column)(sequelize_typescript_1.DataType.STRING(constants_1.CONSTRAINTS_FIELDS.COMMONS.URL.max)),
    tslib_1.__metadata("design:type", String)
], ActorFollowModel.prototype, "url", void 0);
tslib_1.__decorate([
    sequelize_typescript_1.CreatedAt,
    tslib_1.__metadata("design:type", Date)
], ActorFollowModel.prototype, "createdAt", void 0);
tslib_1.__decorate([
    sequelize_typescript_1.UpdatedAt,
    tslib_1.__metadata("design:type", Date)
], ActorFollowModel.prototype, "updatedAt", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.ForeignKey)(() => actor_1.ActorModel),
    sequelize_typescript_1.Column,
    tslib_1.__metadata("design:type", Number)
], ActorFollowModel.prototype, "actorId", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.BelongsTo)(() => actor_1.ActorModel, {
        foreignKey: {
            name: 'actorId',
            allowNull: false
        },
        as: 'ActorFollower',
        onDelete: 'CASCADE'
    }),
    tslib_1.__metadata("design:type", actor_1.ActorModel)
], ActorFollowModel.prototype, "ActorFollower", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.ForeignKey)(() => actor_1.ActorModel),
    sequelize_typescript_1.Column,
    tslib_1.__metadata("design:type", Number)
], ActorFollowModel.prototype, "targetActorId", void 0);
tslib_1.__decorate([
    (0, sequelize_typescript_1.BelongsTo)(() => actor_1.ActorModel, {
        foreignKey: {
            name: 'targetActorId',
            allowNull: false
        },
        as: 'ActorFollowing',
        onDelete: 'CASCADE'
    }),
    tslib_1.__metadata("design:type", actor_1.ActorModel)
], ActorFollowModel.prototype, "ActorFollowing", void 0);
tslib_1.__decorate([
    sequelize_typescript_1.AfterCreate,
    sequelize_typescript_1.AfterUpdate,
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [ActorFollowModel, Object]),
    tslib_1.__metadata("design:returntype", void 0)
], ActorFollowModel, "incrementFollowerAndFollowingCount", null);
tslib_1.__decorate([
    sequelize_typescript_1.AfterDestroy,
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [ActorFollowModel, Object]),
    tslib_1.__metadata("design:returntype", void 0)
], ActorFollowModel, "decrementFollowerAndFollowingCount", null);
ActorFollowModel = ActorFollowModel_1 = tslib_1.__decorate([
    (0, sequelize_typescript_1.Table)({
        tableName: 'actorFollow',
        indexes: [
            {
                fields: ['actorId']
            },
            {
                fields: ['targetActorId']
            },
            {
                fields: ['actorId', 'targetActorId'],
                unique: true
            },
            {
                fields: ['score']
            },
            {
                fields: ['url'],
                unique: true
            }
        ]
    })
], ActorFollowModel);
exports.ActorFollowModel = ActorFollowModel;
//# sourceMappingURL=actor-follow.js.map