diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml deleted file mode 100644 index d06a446..0000000 --- a/.github/workflows/docker-build.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Docker Build - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Build Docker image - id: build-image - uses: docker/build-push-action@v2 - with: - push: false diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index c1bc907..eb29323 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -5,43 +5,69 @@ on: branches: - '*' tags: - - '*' + - '*.*.*' + pull_request: env: IMAGE_NAME: ghcr.io/yuzuzensai/kiroku jobs: - build: + check: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint, format, and typecheck + run: bun run check + + - name: Test + run: bun test + + docker: + needs: check + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 - name: Log in to the Container registry - uses: docker/login-action@v2 + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ secrets.GHCR_IO_USERNAME }} - password: ${{ secrets.GHCR_IO_TOKEN }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta id: docker-meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: images: | ${{ env.IMAGE_NAME }} tags: | - type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} type=raw,value=latest-dev,enable=${{ github.ref_type == 'branch' }} - type=raw,value=${{ github.ref_name }} + type=raw,value=${{ github.ref_name }},enable=${{ github.ref_type == 'branch' }} type=raw,value=${{ github.sha }},enable=${{ github.ref_type == 'branch' }} flavor: | - latest=false + latest=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} - name: Docker Build and Push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker-meta.outputs.tags }} - labels: ${{ steps.docker-meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.docker-meta.outputs.labels }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..eee54e7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +bun run check +bun test diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 7e243d4..0000000 --- a/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "singleQuote": true, - "printWidth": 120, - "editor.formatOnSave": true, - "proseWrap": "always", - "tabWidth": 4, - "requireConfig": false, - "useTabs": false, - "trailingComma": "none", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "semi": true -} \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..78dee05 --- /dev/null +++ b/biome.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "none", + "bracketSpacing": true + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index dc13ae4..015f876 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 225d5d3..1de96a3 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,36 @@ { - "name": "kiroku", - "module": "src/index.ts", - "type": "module", - "scripts": { - "tsc": "tsc", - "start": "bun --watch run ./src/index.ts", - "dev": "tsc && bun start" - }, - "devDependencies": { - "@types/bun": "^1.1.10", - "@types/steamapi": "^2.2.5", - "bun-types": "^1.1.29", - "typescript": "^5.6.2" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "discord.js": "^14.16.3", - "dotenv": "^16.4.5", - "elysia": "^1.1.17", - "steamapi": "^3.0.12" - } -} \ No newline at end of file + "name": "kiroku", + "module": "src/index.ts", + "type": "module", + "scripts": { + "tsc": "tsc", + "start": "bun --watch run ./src/index.ts", + "dev": "tsc && bun start", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", + "format": "biome format .", + "format:fix": "biome format --write .", + "typecheck": "tsc --noEmit", + "check": "biome check . && tsc --noEmit", + "check:fix": "biome check --write . && tsc --noEmit", + "test": "bun test", + "prepare": "husky" + }, + "devDependencies": { + "@biomejs/biome": "2.5.6", + "@sinclair/typebox": "^0.34.52", + "@types/bun": "^1.3.14", + "bun-types": "^1.3.14", + "husky": "^9.1.7", + "typescript": "^7.0.2" + }, + "peerDependencies": { + "typescript": "^7.0.0" + }, + "dependencies": { + "discord.js": "^14.27.0", + "dotenv": "^17.4.2", + "elysia": "^1.4.29", + "steamapi": "^3.1.5" + } +} diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts new file mode 100644 index 0000000..d5d5f32 --- /dev/null +++ b/src/__tests__/server.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, mock, test } from 'bun:test'; + +import { createServer } from '../server'; +import type { Config, ConfigSource, PresenceStore } from '../types'; + +function makeConfig(config: Config): ConfigSource { + return { getConfig: () => config }; +} + +describe('server', () => { + test('returns all data when no user is requested and lookup_all is enabled', async () => { + const config = makeConfig({ global: { lookup_all: true }, users: {} }); + const presenceStore: PresenceStore = { getAll: () => ({ alice: {} }), get: mock() }; + + const response = await createServer(config, presenceStore).handle(new Request('http://localhost/')); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ success: true, data: { alice: {} } }); + }); + + test('forbids lookup-all when disabled in config', async () => { + const config = makeConfig({ global: { lookup_all: false }, users: {} }); + const presenceStore: PresenceStore = { getAll: mock(), get: mock() }; + + const response = await createServer(config, presenceStore).handle(new Request('http://localhost/')); + const body = await response.json(); + + expect(response.status).toBe(403); + expect(body).toEqual({ success: false, message: 'Lookup all is disabled' }); + }); + + test('returns data for a specific user, case-insensitively', async () => { + const config = makeConfig({ global: { lookup_all: true }, users: {} }); + const get = mock().mockReturnValue({ discord: { user: { id: '1' } } }); + const presenceStore: PresenceStore = { getAll: mock(), get }; + + const response = await createServer(config, presenceStore).handle(new Request('http://localhost/?user=Alice')); + const body = await response.json(); + + expect(get).toHaveBeenCalledWith('alice'); + expect(response.status).toBe(200); + expect(body).toEqual({ success: true, data: { discord: { user: { id: '1' } } } }); + }); + + test('returns 404 when the requested user is not found', async () => { + const config = makeConfig({ global: { lookup_all: true }, users: {} }); + const presenceStore: PresenceStore = { getAll: mock(), get: mock().mockReturnValue(undefined) }; + + const response = await createServer(config, presenceStore).handle( + new Request('http://localhost/?user=unknown') + ); + const body = await response.json(); + + expect(response.status).toBe(404); + expect(body).toEqual({ success: false, message: 'Unable to find user' }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 742ff7a..b0aba4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,108 +1,18 @@ import dotenv from 'dotenv'; + dotenv.config(); import ConfigProvider from './providers/ConfigProvider'; -import DiscordProvider from './providers/DiscordProvider'; -import SteamProvider from './providers/SteamProvider'; -import { Elysia, t } from 'elysia'; +import { createServer } from './server'; +import PresenceAggregator from './services/PresenceAggregator'; -// 1 minute -const REFRESH_TIME = 60 * 1000; - -interface ReturnData { - [key: string]: { - discord?: { - user: any; - presences: any; - updatedAt: Date; - }; - steam?: { - user: any; - updatedAt: Date; - }; - }; -} - -let latestReturnData: ReturnData = {}; - -async function main() { +function main() { if (!ConfigProvider.isReady()) return; const port = process.env.PORT || 3000; - setInterval(async () => { - let newData: ReturnData = {}; + PresenceAggregator.start(); - for (let [key, value] of Object.entries(ConfigProvider.getConfig().users)) { - key = key.toLowerCase(); - newData[key] = {}; - - // Discord - if (value.discord && DiscordProvider.isReady) { - // Discord data is already cached by the library, no need to limit it - let user = await DiscordProvider.getUser(value.discord); - let presences = await DiscordProvider.getPresence(value.discord); - newData[key].discord = { - user, - presences: presences, - updatedAt: new Date() - }; - } - - // Steam - if (value.steam && SteamProvider.isReady) { - if ( - latestReturnData[key] && - latestReturnData[key].steam && - latestReturnData[key].steam!.updatedAt.getTime() > Date.now() - REFRESH_TIME - ) { - newData[key].steam = latestReturnData[key].steam; - } else { - let user = await SteamProvider.getProfile(value.steam); - newData[key].steam = { - user, - updatedAt: new Date() - }; - } - } - } - - // Check for changes - for (let [key, value] of Object.entries(newData)) { - if (JSON.stringify(value) !== JSON.stringify(latestReturnData[key])) { - latestReturnData[key] = value; - } - } - }, 500); - - const server = new Elysia() - .get('/', async ({ query, error }) => { - query: t.Object({ - user: t.Optional(t.String()) - }); - - let user = query.user; - - if (!user) { - if (!ConfigProvider.getConfig().global.lookup_all) - return error('Forbidden', { success: false, message: 'Lookup all is disabled' }); - - return { - success: true, - data: latestReturnData - }; - } - - user = user.toLowerCase(); - - if (!latestReturnData[user]) return error('Not Found', { success: false, message: 'Unable to find user' }); - const userData = latestReturnData[user]; - - return { - success: true, - data: userData - }; - }) - .listen(port); + createServer().listen(port); console.log(`Listening on port ${port}`); } diff --git a/src/providers/ConfigProvider.ts b/src/providers/ConfigProvider.ts index 4b9c855..62278d1 100644 --- a/src/providers/ConfigProvider.ts +++ b/src/providers/ConfigProvider.ts @@ -1,19 +1,8 @@ -import fs from 'fs'; -import { Client, Events, GatewayIntentBits } from 'discord.js'; +import fs from 'node:fs'; -export interface Config { - global: { - lookup_all: boolean; - discord_guild_id?: string; - discord_bot_token?: string; - steam_api_key?: string; - }; - users: { - [key: string]: any; - }; -} +import type { Config } from '../types'; -class ConfigProvider { +export class ConfigProvider { private config: Config = { global: { lookup_all: false @@ -22,14 +11,13 @@ class ConfigProvider { }; private ready = false; - constructor() { + constructor(path = './config.json') { try { - let data = fs.readFileSync('./config.json', 'utf8'); + const data = fs.readFileSync(path, 'utf8'); this.config = JSON.parse(data); this.ready = true; } catch (err) { console.error('[ConfigProvider]', err); - return; } } diff --git a/src/providers/DiscordProvider.ts b/src/providers/DiscordProvider.ts index 612be34..d3ceb5d 100644 --- a/src/providers/DiscordProvider.ts +++ b/src/providers/DiscordProvider.ts @@ -1,7 +1,9 @@ -import { ActivityType, Client, Events, GatewayIntentBits } from 'discord.js'; +import { ActivityType, Client, Events, GatewayIntentBits, type Presence, type User } from 'discord.js'; import ConfigProvider from './ConfigProvider'; +const STATUS_REFRESH_INTERVAL = 5 * 60 * 1000; + class DiscordProvider { private client: Client; private guildId: string = ''; @@ -17,7 +19,7 @@ class DiscordProvider { this.ready = true; const setStatus = () => { - this.client?.user?.setPresence({ + client.user.setPresence({ activities: [ { name: 'custom', @@ -31,34 +33,30 @@ class DiscordProvider { }; setStatus(); - setInterval(() => { - setStatus(); - }, 1000 * 60 * 5); + setInterval(setStatus, STATUS_REFRESH_INTERVAL); }); const token = ConfigProvider.getConfig().global.discord_bot_token; - const guild_id = ConfigProvider.getConfig().global.discord_guild_id; + const guildId = ConfigProvider.getConfig().global.discord_guild_id; - if (!token || !guild_id) { + if (!token || !guildId) { console.error('[DiscordProvider]', 'Missing token or guild_id in config.json'); return; } - this.guildId = guild_id; + this.guildId = guildId; this.client.login(token); } - public async getUser(id: string) { - const user = this.client.users.cache.get(id) || (await this.client.users.fetch(id)); - return user; + public async getUser(id: string): Promise { + return this.client.users.cache.get(id) ?? (await this.client.users.fetch(id)); } - public async getPresence(id: string) { - const guild = this.client.guilds.cache.get(this.guildId) || (await this.client.guilds.fetch(this.guildId)); - const member = guild.members.cache.get(id) || (await guild.members.fetch(id)); - const presence = member.presence; - return presence; + public async getPresence(id: string): Promise { + const guild = this.client.guilds.cache.get(this.guildId) ?? (await this.client.guilds.fetch(this.guildId)); + const member = guild.members.cache.get(id) ?? (await guild.members.fetch(id)); + return member.presence; } public get isReady(): boolean { diff --git a/src/providers/SteamProvider.ts b/src/providers/SteamProvider.ts index df6d226..51622f6 100644 --- a/src/providers/SteamProvider.ts +++ b/src/providers/SteamProvider.ts @@ -1,27 +1,26 @@ -import SteamAPI from 'steamapi'; +import SteamAPI, { type UserSummary } from 'steamapi'; import ConfigProvider from './ConfigProvider'; class SteamProvider { - private client: SteamAPI; + private client: SteamAPI | undefined; private ready: boolean = false; constructor() { - this.client = new SteamAPI('dummy'); - const token = ConfigProvider.getConfig().global.steam_api_key; + const apiKey = ConfigProvider.getConfig().global.steam_api_key; - if (!token) { + if (!apiKey) { console.error('[SteamProvider]', 'Missing steam_api_key in config'); return; } - this.client = new SteamAPI(token); + this.client = new SteamAPI(apiKey); this.ready = true; } - public async getProfile(id: string) { - let profile = await this.client.getUserSummary(id); - return profile; + public async getProfile(id: string): Promise { + if (!this.client) throw new Error('SteamProvider is not ready'); + return this.client.getUserSummary(id); } public get isReady(): boolean { diff --git a/src/providers/__tests__/ConfigProvider.test.ts b/src/providers/__tests__/ConfigProvider.test.ts new file mode 100644 index 0000000..21b24bd --- /dev/null +++ b/src/providers/__tests__/ConfigProvider.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { ConfigProvider } from '../ConfigProvider'; + +let tmpFile: string | undefined; + +function writeTempConfig(contents: string): string { + tmpFile = path.join(os.tmpdir(), `kiroku-config-test-${Date.now()}-${Math.random()}.json`); + fs.writeFileSync(tmpFile, contents); + return tmpFile; +} + +describe('ConfigProvider', () => { + afterEach(() => { + if (tmpFile) { + fs.rmSync(tmpFile, { force: true }); + tmpFile = undefined; + } + }); + + test('loads and parses a valid config file', () => { + const configPath = writeTempConfig( + JSON.stringify({ + global: { lookup_all: true, steam_api_key: 'abc' }, + users: { alice: { discord: '123' } } + }) + ); + + const provider = new ConfigProvider(configPath); + + expect(provider.isReady()).toBe(true); + expect(provider.getConfig().global.lookup_all).toBe(true); + expect(provider.getConfig().users.alice?.discord).toBe('123'); + }); + + test('is not ready and falls back to defaults when the file is missing', () => { + const provider = new ConfigProvider('./does-not-exist.json'); + + expect(provider.isReady()).toBe(false); + expect(provider.getConfig()).toEqual({ global: { lookup_all: false }, users: {} }); + }); + + test('is not ready when the file contains invalid JSON', () => { + const configPath = writeTempConfig('{ not valid json'); + + const provider = new ConfigProvider(configPath); + + expect(provider.isReady()).toBe(false); + }); +}); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..8ff5e0d --- /dev/null +++ b/src/server.ts @@ -0,0 +1,34 @@ +import { Elysia, t } from 'elysia'; + +import ConfigProviderSingleton from './providers/ConfigProvider'; +import PresenceAggregatorSingleton from './services/PresenceAggregator'; +import type { ConfigSource, PresenceStore } from './types'; + +export function createServer( + config: ConfigSource = ConfigProviderSingleton, + presenceStore: PresenceStore = PresenceAggregatorSingleton +) { + return new Elysia().get( + '/', + ({ query, status }) => { + const requestedUser = query.user?.toLowerCase(); + + if (!requestedUser) { + if (!config.getConfig().global.lookup_all) + return status('Forbidden', { success: false, message: 'Lookup all is disabled' }); + + return { success: true, data: presenceStore.getAll() }; + } + + const userData = presenceStore.get(requestedUser); + if (!userData) return status('Not Found', { success: false, message: 'Unable to find user' }); + + return { success: true, data: userData }; + }, + { + query: t.Object({ + user: t.Optional(t.String()) + }) + } + ); +} diff --git a/src/services/PresenceAggregator.ts b/src/services/PresenceAggregator.ts new file mode 100644 index 0000000..1fb8037 --- /dev/null +++ b/src/services/PresenceAggregator.ts @@ -0,0 +1,67 @@ +import ConfigProviderSingleton from '../providers/ConfigProvider'; +import DiscordProviderSingleton from '../providers/DiscordProvider'; +import SteamProviderSingleton from '../providers/SteamProvider'; +import type { ConfigSource, DiscordSource, PresenceData, SteamSource } from '../types'; + +const POLL_INTERVAL = 500; +const STEAM_CACHE_TTL = 60 * 1000; + +export class PresenceAggregator { + private data: PresenceData = {}; + private timer: ReturnType | undefined; + + constructor( + private readonly config: ConfigSource = ConfigProviderSingleton, + private readonly discord: DiscordSource = DiscordProviderSingleton, + private readonly steam: SteamSource = SteamProviderSingleton + ) {} + + public start(): void { + if (this.timer) return; + this.timer = setInterval(() => this.refresh(), POLL_INTERVAL); + } + + public stop(): void { + clearInterval(this.timer); + this.timer = undefined; + } + + public getAll(): PresenceData { + return this.data; + } + + public get(user: string): PresenceData[string] | undefined { + return this.data[user]; + } + + public async refresh(): Promise { + const newData: PresenceData = {}; + + for (const [rawKey, userConfig] of Object.entries(this.config.getConfig().users)) { + const key = rawKey.toLowerCase(); + newData[key] = {}; + + if (userConfig.discord && this.discord.isReady) { + const [user, presence] = await Promise.all([ + this.discord.getUser(userConfig.discord), + this.discord.getPresence(userConfig.discord) + ]); + newData[key].discord = { user, presence, updatedAt: new Date() }; + } + + if (userConfig.steam && this.steam.isReady) { + const cached = this.data[key]?.steam; + if (cached && cached.updatedAt.getTime() > Date.now() - STEAM_CACHE_TTL) { + newData[key].steam = cached; + } else { + const user = await this.steam.getProfile(userConfig.steam); + newData[key].steam = { user, updatedAt: new Date() }; + } + } + } + + this.data = newData; + } +} + +export default new PresenceAggregator(); diff --git a/src/services/__tests__/PresenceAggregator.test.ts b/src/services/__tests__/PresenceAggregator.test.ts new file mode 100644 index 0000000..bfc67d8 --- /dev/null +++ b/src/services/__tests__/PresenceAggregator.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, mock, test } from 'bun:test'; +import type { Presence, User } from 'discord.js'; +import type { UserSummary } from 'steamapi'; + +import type { Config, ConfigSource, DiscordSource, SteamSource } from '../../types'; +import { PresenceAggregator } from '../PresenceAggregator'; + +const fakeUser = { id: 'd1', tag: 'alice#0' } as unknown as User; +const fakePresence = { status: 'online' } as unknown as Presence; +const fakeSteamUser = { nickname: 'alice' } as unknown as UserSummary; + +function makeConfig(config: Config): ConfigSource { + return { getConfig: () => config }; +} + +function makeDiscord(overrides: Partial = {}): DiscordSource { + return { + isReady: true, + getUser: mock(), + getPresence: mock(), + ...overrides + }; +} + +function makeSteam(overrides: Partial = {}): SteamSource { + return { + isReady: true, + getProfile: mock(), + ...overrides + }; +} + +describe('PresenceAggregator', () => { + test('aggregates discord and steam data per user, lowercasing keys', async () => { + const discord = makeDiscord({ + getUser: mock().mockResolvedValue(fakeUser), + getPresence: mock().mockResolvedValue(fakePresence) + }); + const steam = makeSteam({ + getProfile: mock().mockResolvedValue(fakeSteamUser) + }); + const config = makeConfig({ + global: { lookup_all: true }, + users: { Alice: { discord: 'd1', steam: 's1' } } + }); + const aggregator = new PresenceAggregator(config, discord, steam); + + await aggregator.refresh(); + + const entry = aggregator.get('alice'); + expect(entry?.discord?.user).toEqual(fakeUser); + expect(entry?.discord?.presence).toEqual(fakePresence); + expect(entry?.steam?.user).toEqual(fakeSteamUser); + expect(aggregator.get('Alice')).toBeUndefined(); + }); + + test('skips discord/steam sections when providers are not ready', async () => { + const discord = makeDiscord({ isReady: false }); + const steam = makeSteam({ isReady: false }); + const config = makeConfig({ + global: { lookup_all: true }, + users: { bob: { discord: 'd2', steam: 's2' } } + }); + const aggregator = new PresenceAggregator(config, discord, steam); + + await aggregator.refresh(); + + expect(aggregator.get('bob')).toEqual({}); + expect(discord.getUser).not.toHaveBeenCalled(); + expect(steam.getProfile).not.toHaveBeenCalled(); + }); + + test('reuses cached steam data within the TTL window instead of refetching', async () => { + const getProfile = mock().mockResolvedValue({ nickname: 'carol' } as unknown as UserSummary); + const steam = makeSteam({ getProfile }); + const config = makeConfig({ + global: { lookup_all: true }, + users: { carol: { steam: 's3' } } + }); + const aggregator = new PresenceAggregator(config, makeDiscord(), steam); + + await aggregator.refresh(); + await aggregator.refresh(); + + expect(getProfile).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..13b884f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,59 @@ +import type { Presence, User } from 'discord.js'; +import type { UserSummary } from 'steamapi'; + +export interface UserConfig { + discord?: string; + steam?: string; +} + +export interface Config { + global: { + lookup_all: boolean; + discord_guild_id?: string; + discord_bot_token?: string; + steam_api_key?: string; + }; + users: { + [key: string]: UserConfig; + }; +} + +export interface DiscordEntry { + user: User; + presence: Presence | null; + updatedAt: Date; +} + +export interface SteamEntry { + user: UserSummary; + updatedAt: Date; +} + +export interface UserData { + discord?: DiscordEntry; + steam?: SteamEntry; +} + +export type PresenceData = { + [key: string]: UserData; +}; + +export interface ConfigSource { + getConfig(): Config; +} + +export interface DiscordSource { + readonly isReady: boolean; + getUser(id: string): Promise; + getPresence(id: string): Promise; +} + +export interface SteamSource { + readonly isReady: boolean; + getProfile(id: string): Promise; +} + +export interface PresenceStore { + getAll(): PresenceData; + get(user: string): PresenceData[string] | undefined; +} diff --git a/tsconfig.json b/tsconfig.json index 5450f44..921517c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,19 @@ { - "compilerOptions": { - "lib": [ - "ESNext" - ], - "module": "esnext", - "target": "esnext", - "moduleResolution": "bundler", - "moduleDetection": "force", - "allowImportingTsExtensions": true, - "noEmit": true, - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": false, - "types": [ - "bun-types" // add Bun global - ] - } -} \ No newline at end of file + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": false, + "types": ["bun-types"] + } +}