Files
steam-chat/src/index.ts
2026-06-25 11:05:28 +08:00

222 lines
6.6 KiB
TypeScript

'use strict';
import type { CallbackStyleFunction, Persona, UnknownRecord } from './types';
import type { IncomingMessage } from 'node:http';
import { errorMessage, isRecord } from './types';
const winston = require('winston');
const { createAuthStore } = require('./auth/store');
const { createSessionManager } = require('./auth/session');
const { loadConfig } = require('./config/load');
const { AUTH_DB_PATH, REFRESH_TOKEN_PATH } = require('./paths');
const { getClientIp } = require('./server/auth');
const { createChatService } = require('./server/chat-service');
const { createSteamLoginService, readRefreshToken } = require('./steam/lifecycle');
const {
steamIdToString
} = require('./storage/chat-log');
const { createSteamMessageLogger } = require('./steam/message-logger');
type PersonaResponse = UnknownRecord & {
personas?: Persona[];
users?: Record<string, Persona>;
};
type SteamUserMain = {
users?: Record<string, Persona>;
steamID?: unknown;
getPersonas?: (ids: string[], callback: (error: unknown, response: PersonaResponse) => void) => void;
chat?: {
sendFriendMessage?: CallbackStyleFunction;
getEmoticonList?: CallbackStyleFunction;
};
sendFriendMessage?: CallbackStyleFunction;
getEmoticonList?: CallbackStyleFunction;
myFriends?: UnknownRecord;
myGroups?: unknown;
groups?: unknown;
on: (event: string, listener: (...args: unknown[]) => void) => void;
off?: (event: string, listener: (...args: unknown[]) => void) => void;
logOn: (options: unknown) => void;
webLogOn: () => void;
setPersona?: (state: number) => void;
EPersonaState?: { Online?: number };
logOff?: () => void;
};
type SteamCommunityMain = {
setCookies?: (cookies: string[]) => void;
startConfirmationChecker?: (intervalMs: number, identitySecret: string) => void;
sendImageToUser?: CallbackStyleFunction;
};
type SteamUserConstructor = new (options: { renewRefreshTokens: boolean }) => SteamUserMain;
type SteamCommunityConstructor = new () => SteamCommunityMain;
type ChatServiceRuntime = {
start: () => unknown;
};
type LogInfo = {
timestamp?: string;
level: string;
message: string;
stack?: string;
};
const SteamCommunity = require('steamcommunity') as SteamCommunityConstructor;
const SteamUser = require('steam-user') as SteamUserConstructor;
const config = loadConfig();
const steamUser = new SteamUser({ renewRefreshTokens: true });
const steamCommunity = new SteamCommunity();
const users: Record<string, Persona> = {};
const authStore = createAuthStore({ dbPath: AUTH_DB_PATH });
const chatAuthConfig = isRecord(config.chat) && isRecord(config.chat.auth) ? config.chat.auth : {};
const sessionManager = createSessionManager({
store: authStore,
getClientIp: (req: IncomingMessage) => getClientIp(req, chatAuthConfig)
});
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.printf((info: LogInfo) => `${info.timestamp} ${info.level}: ${info.message}${info.stack ? `\n${info.stack}` : ''}`)
),
transports: [
new winston.transports.Console()
]
});
function getPersonaFromCache(id: string) {
return users[id] || steamUser.users?.[id] || null;
}
function personaFromUnknown(value: unknown): Persona | null {
return isRecord(value) ? value : null;
}
async function getUserInfo(value: unknown): Promise<Persona> {
const id = steamIdToString(value);
if (!id) return { player_name: 'Unknown' };
const cached = getPersonaFromCache(id);
if (cached) return cached;
if (typeof steamUser.getPersonas !== 'function') {
return { player_name: 'Unknown' };
}
try {
const personas = await new Promise<PersonaResponse>((resolve, reject) => {
steamUser.getPersonas?.([id], (error: unknown, response: PersonaResponse) => {
if (error) reject(error);
else resolve(response);
});
});
const persona = personas.personas?.[0] || personaFromUnknown(personas[id]) || personas.users?.[id] || null;
if (persona) {
users[id] = persona;
return persona;
}
} catch (error) {
logger.warn('Steam persona lookup failed', { id, error: errorMessage(error) });
}
return { player_name: 'Unknown' };
}
async function getSelfName() {
const selfId = steamIdToString(steamUser.steamID || config.steamID);
if (!selfId) return 'Me';
const info = await getUserInfo(selfId);
return info.player_name || info.personaName || 'Me';
}
const lifecycle = createSteamLoginService({
steamUser,
steamCommunity,
config: {
steamID: typeof config.steamID === 'string' ? config.steamID : undefined,
identitySecret: typeof config.identitySecret === 'string' ? config.identitySecret : undefined
},
logger,
refreshTokenPath: REFRESH_TOKEN_PATH,
getDefaultLogonID: authStore.getOrCreateSteamLogonID,
onLoggedOn(steamId: string | null) {
if (!steamId) return;
authStore.upsertSteamAccount({
steamId,
refreshToken: readRefreshToken(REFRESH_TOKEN_PATH) || undefined,
setActive: true
});
},
onRefreshToken(refreshToken: string, steamId: string | null) {
if (!steamId) return;
authStore.upsertSteamAccount({
steamId,
refreshToken,
setActive: true
});
}
});
createSteamMessageLogger({
steamUser,
getUserInfo,
getSelfName,
getSteamAccountId: () => authStore.getActiveSteamAccount(false)?.steamId || steamIdToString(steamUser.steamID || ''),
logger
});
let steamLoginPromise = lifecycle.waitForLogin();
let steamWebLoginPromise = lifecycle.waitForWebSession();
let chatService: ChatServiceRuntime | null = null;
function start() {
const tokenLoginPromise = lifecycle.start().catch((error: unknown) => {
logger.warn('Steam token login failed', { error: errorMessage(error) });
return false;
});
steamLoginPromise = lifecycle.waitForLogin();
steamWebLoginPromise = lifecycle.waitForWebSession();
chatService = createChatService({
config,
steamUser,
steamCommunity,
logger,
getUserInfo,
getSelfName,
waitForLogin: () => lifecycle.waitForLogin(),
waitForWebSession: () => lifecycle.waitForWebSession(),
refreshWebSession: lifecycle.refreshWebSession,
authStore,
sessionManager,
steamLoginService: lifecycle
});
chatService.start();
return tokenLoginPromise;
}
if (process.env.STEAM_CHAT_DISABLE_AUTOSTART !== '1') {
start().catch((error: unknown) => {
logger.error('Steam startup failed', { error: errorMessage(error) });
});
}
module.exports = {
authStore,
chatService,
config,
getSelfName,
getUserInfo,
lifecycle,
logger,
sessionManager,
start,
steamCommunity,
steamLoginPromise,
steamUser,
steamWebLoginPromise,
users
};