refactor: convert to modular system with Hytale update trackers
- Core infrastructure: module interface, Telegram wrapper, OAuth token manager, state store - Migrate Discord forwarder to modules/discord-forwarder/ - Add Hytale update trackers: launcher, patches, downloader, server - Support multiple Telegram chats with per-chat topic IDs - Unified config with legacy migration and env var fallbacks - Auto-refresh OAuth tokens (5min buffer) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
369a37903f
commit
a7d4df6986
29 changed files with 1368 additions and 121 deletions
33
src/modules/hytale-launcher/formatter.ts
Normal file
33
src/modules/hytale-launcher/formatter.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { LauncherUpdate } from './tracker.js'
|
||||
|
||||
function formatDownloadUrls(downloadUrl: Record<string, Record<string, { url: string, sha256: string }>>): string {
|
||||
const lines: string[] = []
|
||||
|
||||
for (const [os, architectures] of Object.entries(downloadUrl)) {
|
||||
for (const [arch, data] of Object.entries(architectures)) {
|
||||
if (data.url && data.sha256) {
|
||||
lines.push(` • ${os}/${arch}: <a href="${data.url}">Download</a> (SHA256: <code>${data.sha256.slice(0, 16)}...</code>)`)
|
||||
}
|
||||
else if (data.url) {
|
||||
lines.push(` • ${os}/${arch}: <a href="${data.url}">Download</a>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.length > 0 ? `\n<b>Downloads:</b>\n${lines.join('\n')}` : ''
|
||||
}
|
||||
|
||||
export function formatLauncherUpdate(update: LauncherUpdate): string {
|
||||
const { version, previousVersion, downloadUrl } = update
|
||||
|
||||
let message = `<b>🚀 Hytale Launcher Update</b>
|
||||
|
||||
<b>New version:</b> <code>${version}</code>
|
||||
${previousVersion !== 'unknown' ? `<b>Previous:</b> <code>${previousVersion}</code>` : ''}`
|
||||
|
||||
if (downloadUrl && Object.keys(downloadUrl).length > 0) {
|
||||
message += formatDownloadUrls(downloadUrl)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
73
src/modules/hytale-launcher/index.ts
Normal file
73
src/modules/hytale-launcher/index.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { Module, ModuleDependencies } from '../../core/module.js'
|
||||
import type { HytaleTrackerConfig } from '../../types.js'
|
||||
import { formatLauncherUpdate } from './formatter.js'
|
||||
import { checkLauncherUpdate } from './tracker.js'
|
||||
|
||||
export function hytaleLauncherFactory(
|
||||
moduleConfig: unknown,
|
||||
deps: ModuleDependencies,
|
||||
): Module {
|
||||
const config = moduleConfig as HytaleTrackerConfig
|
||||
|
||||
class HytaleLauncherModule implements Module {
|
||||
readonly name = 'hytale-launcher'
|
||||
private dependencies: ModuleDependencies
|
||||
private config: HytaleTrackerConfig
|
||||
private running = false
|
||||
private intervalId: NodeJS.Timeout | null = null
|
||||
|
||||
constructor(cfg: HytaleTrackerConfig, deps: ModuleDependencies) {
|
||||
this.config = cfg
|
||||
this.dependencies = deps
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.running
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.running)
|
||||
return
|
||||
|
||||
this.dependencies.logger(this.name, `Starting (poll interval: ${this.config.pollIntervalMinutes}min)`)
|
||||
|
||||
// Initial check
|
||||
await this.check()
|
||||
|
||||
// Start polling
|
||||
this.intervalId = setInterval(() => {
|
||||
this.check().catch((err) => {
|
||||
this.dependencies.logger(this.name, `Check failed: ${err}`)
|
||||
})
|
||||
}, this.config.pollIntervalMinutes * 60 * 1000)
|
||||
|
||||
this.running = true
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.running)
|
||||
return
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
}
|
||||
this.running = false
|
||||
}
|
||||
|
||||
private async check(): Promise<void> {
|
||||
const update = await checkLauncherUpdate(this.dependencies.stateStore)
|
||||
if (update) {
|
||||
const message = formatLauncherUpdate(update)
|
||||
await this.dependencies.telegram.sendMessage({
|
||||
text: message,
|
||||
chatIds: this.config.chatIds,
|
||||
disableLinkPreview: true,
|
||||
})
|
||||
this.dependencies.logger(this.name, `Update detected: ${update.version}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new HytaleLauncherModule(config, deps)
|
||||
}
|
||||
43
src/modules/hytale-launcher/tracker.ts
Normal file
43
src/modules/hytale-launcher/tracker.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { StateStore } from '../../core/state-store.js'
|
||||
|
||||
interface LauncherResponse {
|
||||
version: string
|
||||
download_url: Record<string, Record<string, { url: string, sha256: string }>>
|
||||
}
|
||||
|
||||
export interface LauncherUpdate {
|
||||
version: string
|
||||
previousVersion: string
|
||||
downloadUrl: Record<string, Record<string, { url: string, sha256: string }>>
|
||||
}
|
||||
|
||||
const ENDPOINT = 'https://launcher.hytale.com/version/release/launcher.json'
|
||||
const STATE_KEY = 'hytale-launcher'
|
||||
|
||||
export async function checkLauncherUpdate(stateStore: StateStore): Promise<LauncherUpdate | null> {
|
||||
const response = await fetch(ENDPOINT)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Launcher endpoint returned ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json() as LauncherResponse
|
||||
const currentVersion = data.version
|
||||
|
||||
const state = stateStore.get<{ lastVersion?: string, lastCheck?: string }>(STATE_KEY)
|
||||
const lastVersion = state?.lastVersion
|
||||
|
||||
if (lastVersion === currentVersion) {
|
||||
// No update, just update check time
|
||||
stateStore.set(STATE_KEY, { lastVersion: currentVersion, lastCheck: new Date().toISOString() })
|
||||
return null
|
||||
}
|
||||
|
||||
// Update detected
|
||||
stateStore.set(STATE_KEY, { lastVersion: currentVersion, lastCheck: new Date().toISOString() })
|
||||
|
||||
return {
|
||||
version: currentVersion,
|
||||
previousVersion: lastVersion ?? 'unknown',
|
||||
downloadUrl: data.download_url,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue