Skip to content

Commit

Permalink
refactor(core): make app-overfocused business logics separated into i…
Browse files Browse the repository at this point in the history
…ndividual tasks
  • Loading branch information
async3619 committed Dec 20, 2022
1 parent 5e53aa9 commit 53507b3
Show file tree
Hide file tree
Showing 41 changed files with 777 additions and 447 deletions.
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const jestConfig: JestConfigWithTsJest = {
transform: {
"^.+\\.(t|j)s$": "ts-jest",
},
collectCoverageFrom: ["**/*.ts", "!coverage/**", "!utils/noop.ts", "!index.ts", "!app.ts"],
collectCoverageFrom: ["**/*.ts", "!coverage/**", "!utils/noop.ts", "!index.ts", "!app.ts", "!**/models/*.ts"],
coverageDirectory: "../coverage",
testEnvironment: "node",
roots: ["<rootDir>"],
Expand Down
138 changes: 19 additions & 119 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,18 @@ import chalk from "chalk";
import prettyMilliseconds from "pretty-ms";
import pluralize from "pluralize";

import { UserRepository } from "@repositories/user";
import { UserLogRepository } from "@repositories/user-log";
import { UserRepository, User } from "@repositories/user";
import { UserLogRepository, UserLog } from "@repositories/user-log";

import { User, UserData } from "@repositories/models/user";
import { UserLog, UserLogType } from "@root/repositories/models/user-log";
import { DEFAULT_TASKS, TaskData } from "@tasks";

import { Config } from "@utils/config";
import { throttle } from "@utils/throttle";
import { mapBy } from "@utils/mapBy";
import { measureTime } from "@utils/measureTime";
import { sleep } from "@utils/sleep";
import { Logger } from "@utils/logger";
import { Loggable } from "@utils/types";
import { getDiff } from "@utils/getDiff";
import { Config, throttle, measureTime, sleep, Logger, Loggable } from "@utils";

export class App extends Loggable {
private readonly followerDataSource: DataSource;
private readonly userRepository: UserRepository;
private readonly userLogRepository: UserLogRepository;
private readonly taskClasses = DEFAULT_TASKS;

private cleaningUp = false;
private config: Config | null = null;
Expand Down Expand Up @@ -71,8 +64,7 @@ export class App extends Loggable {

this.config = await Config.create(this.configFilePath);
if (!this.config) {
process.exit(-1);
return;
throw new Error("config is not loaded.");
}

const watchers = this.config.watchers;
Expand Down Expand Up @@ -154,114 +146,22 @@ export class App extends Loggable {
throw new Error("Config is not loaded.");
}

const startedDate = new Date();
const allUserData: UserData[] = [];
for (const [, watcher] of this.config.watchers) {
try {
const userData = await watcher.doWatch();

allUserData.push(...userData);
} catch (e) {
if (!(e instanceof Error)) {
throw e;
}

this.logger.error("an error occurred while watching through `{green}`: {}", [
watcher.getName(),
e.message,
]);

process.exit(-1);
}
}

this.logger.info(`all {} {} collected.`, [allUserData.length, pluralize("follower", allUserData.length)]);

const followingMap = await this.userLogRepository.getFollowStatusMap();
const oldUsers = await this.userRepository.find();
const oldUsersMap = mapBy(oldUsers, "id");

const newUsers = await this.userRepository.createFromData(allUserData, startedDate);
const newUserMap = mapBy(newUsers, "id");

// find user renaming their displayName or userId
const displayNameRenamedUsers = getDiff(newUsers, oldUsers, "uniqueId", "displayName");
const userIdRenamedUsers = getDiff(newUsers, oldUsers, "uniqueId", "userId");

// find users who are not followed yet.
const newFollowers = newUsers.filter(p => !followingMap[p.id]);
const unfollowers = oldUsers.filter(p => !newUserMap[p.id] && followingMap[p.id]);

const renameLogs: UserLog[] = [];
if (displayNameRenamedUsers.length || userIdRenamedUsers.length) {
const displayName = displayNameRenamedUsers.map(p => {
const oldUser = oldUsersMap[p.id];
const userLog = this.userLogRepository.create();
userLog.type = UserLogType.RenameDisplayName;
userLog.user = p;
userLog.oldDisplayName = oldUser?.displayName;
userLog.oldUserId = oldUser?.userId;

return userLog;
});

const userId = userIdRenamedUsers.map(p => {
const oldUser = oldUsersMap[p.id];
const userLog = this.userLogRepository.create();
userLog.type = UserLogType.RenameUserId;
userLog.user = p;
userLog.oldDisplayName = oldUser?.displayName;
userLog.oldUserId = oldUser?.userId;

return userLog;
});

renameLogs.push(...displayName, ...userId);
}

const newLogs = await this.userLogRepository.batchWriteLogs(
[
[newFollowers, UserLogType.Follow],
[unfollowers, UserLogType.Unfollow],
],
renameLogs,
);

this.logger.info(`all {} {} saved`, [newLogs.length, pluralize("log", newLogs.length)]);

this.logger.info("tracked {} new {}", [newFollowers.length, pluralize("follower", newFollowers.length)]);
this.logger.info("tracked {} {}", [unfollowers.length, pluralize("unfollower", unfollowers.length)]);

const notifiers = this.config.notifiers;
if (!notifiers.length) {
this.logger.info("no notifiers are configured. skipping notification...");
return;
}

const ignoredIds = this.config.ignores;
if (ignoredIds.length) {
this.logger.info("ignoring {}", [pluralize("user", ignoredIds.length, true)]);

for (const log of newLogs) {
if (!ignoredIds.includes(log.user.id)) {
continue;
}
const taskData: TaskData[] = [];
for (const TaskClass of this.taskClasses) {
const taskInstance = new TaskClass(
this.config.watchers.map(([, watcher]) => watcher),
this.config.notifiers.map(([, notifier]) => notifier),
this.userRepository,
this.userLogRepository,
TaskClass.name,
);

newLogs.splice(newLogs.indexOf(log), 1);
const data = await taskInstance.doWork(taskData);
if (data.type === "terminate") {
return;
}
}

if (newLogs.length <= 0) {
return;
}

const watcherMap = this.config.watcherMap;
for (const [, notifier] of notifiers) {
await notifier.notify(
newLogs.map(log => {
return [watcherMap[log.user.from], log];
}),
);
taskData.push(data);
}
};
}
27 changes: 17 additions & 10 deletions src/notifiers/base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { NotifyPair } from "@notifiers/type";
import { capitalCase } from "change-case";

import { UserLog, UserLogType } from "@repositories/models/user-log";

import { UserLogMap } from "@notifiers/type";

import { Loggable } from "@utils/types";
import { UserLogType } from "@repositories/models/user-log";
import { Logger } from "@utils/logger";
import { Loggable } from "@utils/types";

export abstract class BaseNotifier<TType extends string> extends Loggable<TType> {
protected constructor(name: TType) {
Expand All @@ -15,26 +18,30 @@ export abstract class BaseNotifier<TType extends string> extends Loggable<TType>

public abstract initialize(): Promise<void>;

public abstract notify(logs: NotifyPair[]): Promise<void>;
public abstract notify(logs: UserLog[], logMap: UserLogMap): Promise<void>;

protected formatNotify(pair: NotifyPair): string {
const [watcher, log] = pair;
protected formatNotify(log: UserLog): string {
const { user } = log;

if (log.type === UserLogType.RenameUserId || log.type === UserLogType.RenameDisplayName) {
const tokens = [
watcher.getName(),
capitalCase(user.from),
log.oldDisplayName || "",
log.oldUserId || "",
watcher.getProfileUrl(log.user),
user.profileUrl,
log.type === UserLogType.RenameDisplayName ? "" : "@",
log.type === UserLogType.RenameUserId ? user.userId : user.displayName,
];

return Logger.format("[{}] [{} (@{})]({}) → {}{}", ...tokens);
}

const tokens = [watcher.getName(), user.displayName, user.userId, watcher.getProfileUrl(user)];
return Logger.format("[{}] [{} (@{})]({})", ...tokens);
return Logger.format(
"[{}] [{} (@{})]({})",
capitalCase(user.from),
user.displayName,
user.userId,
user.profileUrl,
);
}
}
34 changes: 14 additions & 20 deletions src/notifiers/discord.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import pluralize from "pluralize";
import dayjs from "dayjs";

import { UserLogType } from "@repositories/models/user-log";
import { UserLog, UserLogType } from "@repositories/models/user-log";

import { BaseNotifier } from "@notifiers/base";
import { BaseNotifierOption, NotifyPair } from "@notifiers/type";
import { BaseNotifierOption, UserLogMap } from "@notifiers/type";

import { Fetcher } from "@utils/fetcher";
import { Logger } from "@utils/logger";
Expand Down Expand Up @@ -40,12 +40,12 @@ export class DiscordNotifier extends BaseNotifier<"Discord"> {
public async initialize() {
this.webhookUrl = this.options.webhookUrl;
}
public async notify(pairs: NotifyPair[]) {
public async notify(logs: UserLog[], logMap: UserLogMap) {
if (!this.webhookUrl) {
throw new Error("DiscordNotifier is not initialized");
}

if (pairs.length <= 0) {
if (logs.length <= 0) {
return;
}

Expand All @@ -55,9 +55,9 @@ export class DiscordNotifier extends BaseNotifier<"Discord"> {
{
title: Logger.format(
"Total {} {} {} found",
pairs.length,
pluralize("change", pairs.length),
pluralize("was", pairs.length),
logs.length,
pluralize("change", logs.length),
pluralize("was", logs.length),
),
color: 5814783,
fields: [],
Expand All @@ -72,12 +72,9 @@ export class DiscordNotifier extends BaseNotifier<"Discord"> {
};

const fields: DiscordWebhookData["embeds"][0]["fields"] = [];
const followerLogs = pairs.filter(([, l]) => l.type === UserLogType.Follow);
const unfollowerLogs = pairs.filter(([, l]) => l.type === UserLogType.Unfollow);
const renameLogs = pairs.filter(
([, l]) => l.type === UserLogType.RenameUserId || l.type === UserLogType.RenameDisplayName,
);

const followerLogs = logMap[UserLogType.Follow];
const unfollowerLogs = logMap[UserLogType.Unfollow];
const renameLogs = logMap[UserLogType.Rename];
if (followerLogs.length > 0) {
fields.push(this.composeLogs(followerLogs, "🎉 {} new {}", "follower"));
}
Expand All @@ -100,15 +97,12 @@ export class DiscordNotifier extends BaseNotifier<"Discord"> {
}

private composeLogs(
logs: NotifyPair[],
logs: UserLog[],
messageFormat: string,
word: string,
): DiscordWebhookData["embeds"][0]["fields"][0] {
const { name, value } = this.generateEmbedField(
logs,
Logger.format(messageFormat, logs.length, pluralize(word, logs.length)),
);

const message = Logger.format(messageFormat, logs.length, pluralize(word, logs.length));
const { name, value } = this.generateEmbedField(logs, message);
const valueLines = [value];
if (logs.length > 10) {
valueLines.push(`_... and ${logs.length - 10} more_`);
Expand All @@ -119,7 +113,7 @@ export class DiscordNotifier extends BaseNotifier<"Discord"> {
value: valueLines.join("\n"),
};
}
private generateEmbedField(logs: NotifyPair[], title: string) {
private generateEmbedField(logs: UserLog[], title: string) {
return {
name: title,
value: logs.slice(0, 10).map(this.formatNotify).join("\n"),
Expand Down
24 changes: 8 additions & 16 deletions src/notifiers/slack.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IncomingWebhook, IncomingWebhookSendArguments } from "@slack/webhook";

import { BaseNotifier } from "@notifiers/base";
import { BaseNotifierOption, NotifyPair } from "@notifiers/type";
import { BaseNotifierOption, UserLogMap } from "@notifiers/type";

import { groupNotifies } from "@utils/groupNotifies";
import { Logger } from "@utils/logger";
import { UserLog } from "@repositories/models/user-log";

export interface SlackNotifierOptions extends BaseNotifierOption<SlackNotifier> {
webhookUrl: string;
Expand All @@ -24,24 +24,16 @@ export class SlackNotifier extends BaseNotifier<"Slack"> {
public async initialize(): Promise<void> {
return;
}
public async notify(logs: NotifyPair[]): Promise<void> {
const { follow, unfollow, rename } = groupNotifies(logs);
const targets: [NotifyPair[], number, string, string][] = [
public async notify(logs: UserLog[], logMap: UserLogMap): Promise<void> {
const { follow, unfollow, rename } = logMap;
const targets: [UserLog[], number, string, string][] = [
[follow.slice(0, MAX_ITEMS_PER_MESSAGE), follow.length, "🎉 {} new {}", "follower"],
[unfollow.slice(0, MAX_ITEMS_PER_MESSAGE), unfollow.length, "❌ {} {}", "unfollower"],
[rename.slice(0, MAX_ITEMS_PER_MESSAGE), rename.length, "✏️ {} {}", "rename"],
];

const result: IncomingWebhookSendArguments = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "_*🦜 Cage Report*_",
},
},
],
blocks: [{ type: "section", text: { type: "mrkdwn", text: "_*🦜 Cage Report*_" } }],
};

let shouldNotify = false;
Expand Down Expand Up @@ -78,7 +70,7 @@ export class SlackNotifier extends BaseNotifier<"Slack"> {
}
}

protected formatNotify(pair: NotifyPair): string {
return super.formatNotify(pair).replace(/ \[(.*? \(@.*?\))\]\((.*?)\)/g, "<$2|$1>");
protected formatNotify(log: UserLog): string {
return super.formatNotify(log).replace(/ \[(.*? \(@.*?\))\]\((.*?)\)/g, "<$2|$1>");
}
}
24 changes: 7 additions & 17 deletions src/notifiers/telegram/constants.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
export const TELEGRAM_FOLLOWERS_TEMPLATE = `
*🎉 {} {}*
import { UserLogType } from "@repositories/models/user-log";

{}
{}
`.trim();
export const TELEGRAM_UNFOLLOWERS_TEMPLATE = `
*❌ {} {}*
export const CONTENT_TEMPLATES: Partial<Record<UserLogType, string[]>> = {
[UserLogType.Follow]: ["**🎉 {}**\n\n{}", "new follower"],
[UserLogType.Unfollow]: ["**❌ {}**\n\n{}", "unfollower"],
[UserLogType.Rename]: ["**✏️ {}**\n\n{}", "rename"],
};

{}
{}
`.trim();
export const TELEGRAM_RENAMES_TEMPLATE = `
*✏️ {} {}*
{}
{}
`.trim();
export const TELEGRAM_LOG_COUNT = 25;
export const MAXIMUM_LOG_COUNT = 50;
Loading

0 comments on commit 53507b3

Please sign in to comment.