Skip to content

Commit

Permalink
Initial setup of service-capital
Browse files Browse the repository at this point in the history
  • Loading branch information
csuvajit committed Sep 22, 2023
1 parent 04ce164 commit 43651cf
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 58 deletions.
5 changes: 3 additions & 2 deletions apps/service-app/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { MongodbModule } from '@app/mongodb';
import { RedisModule } from '@app/redis';
import { RestModule } from '@app/rest';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RedisModule } from '@app/redis';

@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), MongodbModule, RedisModule],
imports: [ConfigModule.forRoot({ isGlobal: true }), MongodbModule, RedisModule, RestModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
2 changes: 2 additions & 0 deletions apps/service-app/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Tokens } from '@app/constants';
import { MongodbService } from '@app/mongodb';
import { RedisClient, RedisService } from '@app/redis';
import RestHandler from '@app/rest/rest.module';
import { Inject, Injectable } from '@nestjs/common';
import { Db } from 'mongodb';

Expand All @@ -9,6 +10,7 @@ export class AppService {
constructor(
@Inject(Tokens.MONGODB) private readonly db: Db,
@Inject(Tokens.REDIS) private readonly redis: RedisClient,
@Inject(Tokens.REST) private readonly restClient: RestHandler,
private readonly redisService: RedisService,
private readonly mongoService: MongodbService,
) {}
Expand Down
22 changes: 0 additions & 22 deletions apps/service-capital/src/service-capital.controller.spec.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/service-capital/src/service-capital.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { ServiceCapitalService } from './service-capital.service';
import { CapitalService } from './service-capital.service';

@Controller()
export class ServiceCapitalController {
constructor(private readonly serviceCapitalService: ServiceCapitalService) {}
constructor(private readonly serviceCapitalService: CapitalService) {}

@Get()
getHello(): string {
Expand Down
10 changes: 7 additions & 3 deletions apps/service-capital/src/service-capital.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { MongodbModule } from '@app/mongodb';
import { RedisModule } from '@app/redis';
import { RestModule } from '@app/rest';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ServiceCapitalController } from './service-capital.controller';
import { ServiceCapitalService } from './service-capital.service';
import { CapitalService } from './service-capital.service';

@Module({
imports: [],
imports: [ConfigModule.forRoot({ isGlobal: true }), MongodbModule, RedisModule, RestModule],
controllers: [ServiceCapitalController],
providers: [ServiceCapitalService],
providers: [CapitalService],
})
export class ServiceCapitalModule {}
80 changes: 78 additions & 2 deletions apps/service-capital/src/service-capital.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,83 @@
import { Injectable } from '@nestjs/common';
import { Collections, RedisKeyPrefixes, Tokens } from '@app/constants';
import { CapitalRaidSeasonsEntity } from '@app/entities/capital.entity';
import { MongodbService } from '@app/mongodb';
import { RedisClient, RedisService, TrackedClanList, getRedisKey } from '@app/redis';
import RestHandler from '@app/rest/rest.module';
import { Inject, Injectable } from '@nestjs/common';
import moment from 'moment';
import { Collection, Db } from 'mongodb';

@Injectable()
export class ServiceCapitalService {
export class CapitalService {
constructor(
@Inject(Tokens.MONGODB) private readonly db: Db,
@Inject(Tokens.REDIS) private readonly redis: RedisClient,
@Inject(Tokens.REST) private readonly restClient: RestHandler,
private readonly redisService: RedisService,
private readonly mongoService: MongodbService,
@Inject(Collections.CAPITAL_RAID_SEASONS)
private readonly capitalRaidsCollection: Collection<CapitalRaidSeasonsEntity>,
) {}

private readonly cached = new Map<string, TrackedClanList>();

async onModuleInit() {
await this.loadClans();
}

async fetchCapitalRaidWeekend(clanTag: string) {
const clan = await this.redisService.getClan(clanTag);
if (!clan) return null;

const { body, res } = await this.restClient.getCapitalRaidSeasons(clanTag, { limit: 1 });
if (!res.ok || !body.items.length) return null;

const season = body.items.at(0)!;
if (!Array.isArray(season.members)) return null;

const { weekId } = this.getCapitalRaidWeekendTiming();
const raidWeekId = this.getCurrentWeekId(season.startTime);

const isCached = await this.redis.get(
getRedisKey(RedisKeyPrefixes.CAPITAL_RAID_WEEK, `${weekId}-${clan.tag}`),
);
if (!isCached && raidWeekId === weekId) {
// TODO: push reminders
}
}

async loadClans() {
const clans = await this.redisService.getTrackedClans();
for (const clan of clans) this.cached.set(clan.tag, clan);
}

async startPolling() {
for (const clanTag of this.cached.keys()) {
await this.fetchCapitalRaidWeekend(clanTag);
}
}

public getCapitalRaidWeekendTiming() {
const start = moment();
const day = start.day();
const hours = start.hours();
const isRaidWeekend =
(day === 5 && hours >= 7) || [0, 6].includes(day) || (day === 1 && hours < 7);
if (day < 5 || (day <= 5 && hours < 7)) start.day(-7);
start.day(5);
start.hours(7).minutes(0).seconds(0).milliseconds(0);
return {
startTime: start.toDate(),
endTime: start.clone().add(3, 'days').toDate(),
weekId: start.format('YYYY-MM-DD'),
isRaidWeekend,
};
}

private getCurrentWeekId(weekId: string) {
return moment(weekId).toDate().toISOString().substring(0, 10);
}

getHello(): string {
return 'Hello World!';
}
Expand Down
13 changes: 8 additions & 5 deletions libs/constants/src/constants.values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ export enum TokenType {
ACCESS_TOKEN = 'access_token',
}

export enum RedisKeyPrefixes {
CAPITAL_RAID_SEASON = 'CRS',
CAPITAL_RAID_WEEK = 'CR',
CLAN = 'C',
PLAYER = 'P',
LINKED_CLANS = 'LINKED_CLANS',
}

export enum Collections {
// LOG_CHANNELS
CLAN_STORES = 'ClanStores',
DONATION_LOGS = 'DonationLogs',
LAST_SEEN_LOGS = 'LastSeenLogs',
Expand All @@ -25,18 +32,15 @@ export enum Collections {

LEGEND_ATTACKS = 'LegendAttacks',

// FLAGS
FLAGS = 'Flags',

// LINKED_DATA
LINKED_CLANS = 'LinkedClans',
LINKED_PLAYERS = 'LinkedPlayers',
LINKED_CHANNELS = 'LinkedChannels',
TIME_ZONES = 'TimeZones',
REMINDERS = 'Reminders',
SCHEDULERS = 'Schedulers',

// LARGE_DATA
PATRONS = 'Patrons',
SETTINGS = 'Settings',
LAST_SEEN = 'LastSeen',
Expand All @@ -58,7 +62,6 @@ export enum Collections {
CAPITAL_RANKS = 'CapitalRanks',
CLAN_RANKS = 'ClanRanks',

// BOT_STATS
BOT_GROWTH = 'BotGrowth',
BOT_USAGE = 'BotUsage',
BOT_GUILDS = 'BotGuilds',
Expand Down
3 changes: 3 additions & 0 deletions libs/entities/src/capital.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { APICapitalRaidSeason } from 'clashofclans.js';

export interface CapitalRaidSeasonsEntity extends APICapitalRaidSeason {}
10 changes: 2 additions & 8 deletions libs/entities/src/wars.entity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
export class ClanStoresEntity {
guildId: string;
import { APIClanWar } from 'clashofclans.js';

name: string;

tag: string;

createdAt: Date;
}
export interface ClanWarsEntity extends APIClanWar {}
14 changes: 11 additions & 3 deletions libs/mongodb/src/mongodb.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tokens } from '@app/constants';
import { Collections, Tokens } from '@app/constants';
import { Module, Provider } from '@nestjs/common';
import { Db, MongoClient } from 'mongodb';
import { MongodbService } from './mongodb.service';
Expand All @@ -11,8 +11,16 @@ const MongodbProvider: Provider = {
},
};

export const collectionProviders: Provider[] = Object.values(Collections).map((collection) => ({
provide: collection,
useFactory: async (db: Db) => {
return db.collection(collection);
},
inject: [Tokens.MONGODB],
}));

@Module({
providers: [MongodbProvider, MongodbService],
exports: [MongodbProvider, MongodbService],
providers: [MongodbProvider, MongodbService, ...collectionProviders],
exports: [MongodbProvider, MongodbService, ...collectionProviders],
})
export class MongodbModule {}
24 changes: 23 additions & 1 deletion libs/redis/src/redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { Tokens } from '@app/constants';
import { RedisKeyPrefixes, Tokens } from '@app/constants';
import { Inject, Injectable } from '@nestjs/common';
import { APIClan } from 'clashofclans.js';
import { RedisClient } from './redis.module';

export const getRedisKey = (prefix: RedisKeyPrefixes, key: string): string => {
return `${prefix}:${key}`;
};

@Injectable()
export class RedisService {
constructor(@Inject(Tokens.REDIS) private readonly redis: RedisClient) {}

async getTrackedClans(): Promise<TrackedClanList[]> {
const result = await this.redis.json.get(getRedisKey(RedisKeyPrefixes.LINKED_CLANS, 'ALL'));
return (result ?? []) as unknown as TrackedClanList[];
}

async getClan(clanTag: string): Promise<APIClan | null> {
const result = await this.redis.json.get(getRedisKey(RedisKeyPrefixes.CLAN, clanTag));
return result as unknown as APIClan;
}
}

export interface TrackedClanList {
clan: string;
tag: string;
isPatron: boolean;
guildIds: string[];
}
48 changes: 44 additions & 4 deletions libs/rest/src/rest.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import { Tokens } from '@app/constants';
import { Module, Provider } from '@nestjs/common';
import { Client } from 'clashofclans.js';
import { Logger, Module, Provider } from '@nestjs/common';
import {
QueueThrottler,
RESTManager,
RequestHandler,
RequestOptions,
Result,
} from 'clashofclans.js';
import { RestService } from './rest.service';

class ReqHandler extends RequestHandler {
private readonly logger = new Logger('ClashApiRest');

public async request<T>(path: string, options: RequestOptions = {}): Promise<Result<T>> {
const result = await super.request<T>(path, options);
if (
!result.res.ok &&
// @ts-expect-error ---
!(!result.body?.message && result.res.status === 403) &&
!(path.includes('war') && result.res.status === 404)
) {
this.logger.log(`${result.res.status} ${path}`);
}
return result;
}
}

export default class RestHandler extends RESTManager {
public constructor(rateLimit: number) {
super();
this.requestHandler = new ReqHandler({
cache: false,
rejectIfNotValid: false,
restRequestTimeout: 10_000,
retryLimit: 0,
connections: 50,
pipelining: 10,
baseURL: process.env.CLASH_API_BASE_URL,
keys: process.env.CLASH_API_TOKENS?.split(',') ?? [],
throttler: rateLimit ? new QueueThrottler(rateLimit) : null,
});
}
}

const RestProvider: Provider = {
provide: Tokens.REST,
useFactory: (): Client => {
return new Client();
useFactory: (): RestHandler => {
return new RestHandler(0);
},
};

Expand Down
2 changes: 1 addition & 1 deletion libs/rest/src/rest.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Tokens } from '@app/constants';
import { Inject, Injectable } from '@nestjs/common';
import type { Client } from 'clashofclans.js';
import { Client } from 'clashofclans.js';

@Injectable()
export class RestService {
Expand Down
Loading

0 comments on commit 43651cf

Please sign in to comment.