storage: persist message attachments and user AI settings

This commit is contained in:
2026-05-10 22:52:10 +03:00
parent 28f67aefc2
commit d666244863
14 changed files with 1147 additions and 45 deletions
+29 -5
View File
@@ -2,9 +2,14 @@ import {StoredMessage} from "../model/stored-message";
import {Message} from "typescript-telegram-bot-api";
import {extractTextMessage, getPhotoMaxSize, isStoredMessage} from "../util/utils";
import {messageDao} from "../index";
import {KeyedAsyncLock} from "../util/async-lock";
import {setLruMapValue} from "../util/lru-map";
const MESSAGE_CACHE_MAX_ENTRIES = 10_000;
export class MessageStore {
private static map = new Map<string, StoredMessage>();
private static locks = new KeyedAsyncLock();
private static key(chatId: number, messageId: number) {
return `${chatId}:${messageId}`;
@@ -23,13 +28,32 @@ export class MessageStore {
replyToMessageId: m.reply_to_message?.message_id,
fromId: <number>m.from?.id,
text: extractTextMessage(m),
quoteText: m.quote?.text,
date: m.date ?? 0,
photoMaxSizeFilePath: maxSizePath ? [maxSizePath] : undefined,
};
this.map.set(this.key(msg.chatId, msg.id), msg);
await messageDao.insert(messageDao.mapStoredTo([msg]));
return msg;
const key = this.key(msg.chatId, msg.id);
return this.locks.runExclusive(key, async () => {
const existing = this.map.get(key) ?? await messageDao.getById({chatId: msg.chatId, id: msg.id});
const merged: StoredMessage = {
chatId: msg.chatId,
id: msg.id,
replyToMessageId: msg.replyToMessageId ?? existing?.replyToMessageId,
fromId: msg.fromId,
text: msg.text !== undefined ? msg.text : existing?.text,
quoteText: msg.quoteText ? msg.quoteText : existing?.quoteText,
date: msg.date,
photoMaxSizeFilePath: msg.photoMaxSizeFilePath !== undefined
? msg.photoMaxSizeFilePath
: existing?.photoMaxSizeFilePath,
attachments: msg.attachments !== undefined ? msg.attachments : existing?.attachments,
};
setLruMapValue(this.map, key, merged, MESSAGE_CACHE_MAX_ENTRIES);
await messageDao.insert(messageDao.mapStoredTo([merged]));
return merged;
});
}
static async get(chatId: number, messageId: number | undefined): Promise<StoredMessage | null> {
@@ -38,11 +62,11 @@ export class MessageStore {
const message = await messageDao.getById({chatId: chatId, id: messageId});
if (!message) return null;
this.map.set(this.key(message.chatId, messageId), message);
setLruMapValue(this.map, this.key(message.chatId, messageId), message, MESSAGE_CACHE_MAX_ENTRIES);
return message;
}
static clear() {
this.map.clear();
}
}
}
+450
View File
@@ -0,0 +1,450 @@
import {Environment} from "./environment";
import {UserStore} from "./user-store";
import {AiProvider} from "../model/ai-provider";
import {StoredUser} from "../model/stored-user";
import {resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
import {
DEFAULT_LANGUAGE_CHOICE,
LanguageChoice,
Localization,
} from "./localization";
export const DEFAULT_AI_PROVIDER_CHOICE = "DEFAULT";
export const DEFAULT_AI_CONTEXT_SIZE_CHOICE = "DEFAULT";
export const USER_AI_CONTEXT_SIZE_PRESETS = [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 256000] as const;
export const MIN_USER_AI_CONTEXT_SIZE = 64;
export const MAX_USER_AI_CONTEXT_SIZE = 1_000_000;
export const AI_VOICE_MODE_EXECUTE = "execute";
export const AI_VOICE_MODE_TRANSCRIPT = "transcript";
export type UserAiProviderChoice = AiProvider | typeof DEFAULT_AI_PROVIDER_CHOICE;
export type UserAiContextSizeChoice = number | typeof DEFAULT_AI_CONTEXT_SIZE_CHOICE;
export type UserAiVoiceMode = typeof AI_VOICE_MODE_EXECUTE | typeof AI_VOICE_MODE_TRANSCRIPT;
export type UserInterfaceLanguage = LanguageChoice;
export type UserAiResponseLanguage = LanguageChoice;
export type UserTier = "creator" | "admin" | "user";
export type EffectiveUserAiSettings = {
userId: number;
tier: UserTier;
providerChoice: UserAiProviderChoice;
effectiveProvider: AiProvider;
interfaceLanguage: UserInterfaceLanguage;
responseLanguage: UserAiResponseLanguage;
contextSizeChoice: UserAiContextSizeChoice;
contextSize?: number;
voiceMode: UserAiVoiceMode;
availableProviderChoices: UserAiProviderChoice[];
availableContextSizeChoices: UserAiContextSizeChoice[];
availableVoiceModes: UserAiVoiceMode[];
};
const CREATOR_PROVIDERS: readonly AiProvider[] = [
AiProvider.OLLAMA,
AiProvider.GEMINI,
AiProvider.MISTRAL,
AiProvider.OPENAI,
];
const ADMIN_PROVIDERS: readonly AiProvider[] = [
AiProvider.MISTRAL,
AiProvider.OPENAI,
];
const USER_PROVIDERS: readonly AiProvider[] = [
AiProvider.MISTRAL,
AiProvider.GEMINI,
AiProvider.OLLAMA,
];
export const DEFAULT_INTERFACE_LANGUAGE: UserInterfaceLanguage = DEFAULT_LANGUAGE_CHOICE;
export const DEFAULT_AI_RESPONSE_LANGUAGE: UserAiResponseLanguage = DEFAULT_LANGUAGE_CHOICE;
export const DEFAULT_AI_VOICE_MODE: UserAiVoiceMode = AI_VOICE_MODE_EXECUTE;
export function getUserLanguageChoices(): string[] {
return Localization.languageChoices();
}
export function getUserAiContextSizeChoices(): UserAiContextSizeChoice[] {
return [DEFAULT_AI_CONTEXT_SIZE_CHOICE, ...USER_AI_CONTEXT_SIZE_PRESETS];
}
export function getUserAiVoiceModes(): UserAiVoiceMode[] {
return [AI_VOICE_MODE_EXECUTE, AI_VOICE_MODE_TRANSCRIPT];
}
export function getUserTier(userId: number): UserTier {
if (userId === Environment.CREATOR_ID) return "creator";
if (Environment.ADMIN_IDS.has(userId)) return "admin";
return "user";
}
function allowedProvidersForTier(tier: UserTier): readonly AiProvider[] {
switch (tier) {
case "creator":
return CREATOR_PROVIDERS;
case "admin":
return ADMIN_PROVIDERS;
case "user":
return USER_PROVIDERS;
}
}
export function isAiProviderConfigured(provider: AiProvider): boolean {
const target = resolveAiRuntimeTarget(provider, "chat");
switch (provider) {
case AiProvider.OLLAMA:
return !!target.baseUrl && !!target.model;
case AiProvider.GEMINI:
return !!target.apiKey && !!target.model;
case AiProvider.MISTRAL:
return !!target.apiKey && !!target.model;
case AiProvider.OPENAI:
return !!target.apiKey && !!target.model;
}
}
export function getAvailableAiProviderChoices(userId: number): UserAiProviderChoice[] {
const tier = getUserTier(userId);
const providers = allowedProvidersForTier(tier).filter(isAiProviderConfigured);
return [DEFAULT_AI_PROVIDER_CHOICE, ...providers];
}
export function normalizeAiProviderChoice(value: string | undefined | null): UserAiProviderChoice | undefined {
if (!value) return undefined;
if (value === DEFAULT_AI_PROVIDER_CHOICE) return DEFAULT_AI_PROVIDER_CHOICE;
const providers = Object.values(AiProvider) as string[];
return providers.includes(value) ? value as AiProvider : undefined;
}
export function normalizeAiContextSizeChoice(value: string | number | undefined | null): UserAiContextSizeChoice | undefined {
if (value === undefined || value === null || value === "") return undefined;
let numericValue: number;
if (typeof value === "string") {
const normalized = value.trim();
const lower = normalized.toLowerCase();
if (normalized === DEFAULT_AI_CONTEXT_SIZE_CHOICE || lower === "default" || lower === "auto") {
return DEFAULT_AI_CONTEXT_SIZE_CHOICE;
}
numericValue = Number(normalized);
} else {
numericValue = value;
}
if (!Number.isSafeInteger(numericValue) || numericValue < MIN_USER_AI_CONTEXT_SIZE || numericValue > MAX_USER_AI_CONTEXT_SIZE) {
return undefined;
}
return numericValue;
}
export function normalizeAiVoiceMode(value: string | undefined | null): UserAiVoiceMode | undefined {
if (!value) return undefined;
switch (value.trim().toLowerCase()) {
case AI_VOICE_MODE_EXECUTE:
case "command":
case "commands":
case "ai":
return AI_VOICE_MODE_EXECUTE;
case AI_VOICE_MODE_TRANSCRIPT:
case "transcribe":
case "transcription":
case "text":
return AI_VOICE_MODE_TRANSCRIPT;
default:
return undefined;
}
}
export function normalizeUserLanguageChoice(value: string | undefined | null): UserInterfaceLanguage | undefined {
if (!value) return undefined;
if (value === DEFAULT_LANGUAGE_CHOICE) return DEFAULT_LANGUAGE_CHOICE;
const normalized = Localization.normalizeLocale(value);
return normalized && Localization.isKnownLanguageChoice(normalized)
? normalized
: undefined;
}
export function normalizeInterfaceLanguage(value: string | undefined | null): UserInterfaceLanguage | undefined {
return normalizeUserLanguageChoice(value);
}
export function normalizeAiResponseLanguage(value: string | undefined | null): UserAiResponseLanguage | undefined {
return normalizeUserLanguageChoice(value);
}
export function getProviderChoiceLabel(choice: UserAiProviderChoice): string {
if (choice === DEFAULT_AI_PROVIDER_CHOICE) {
return Localization.text("providerChoice.default", {}, "Default");
}
return choice.charAt(0) + choice.slice(1).toLowerCase();
}
export function getResponseLanguageLabel(language: UserAiResponseLanguage): string {
return Localization.languageLabel(language);
}
export function getContextSizeLabel(choice: UserAiContextSizeChoice): string {
if (choice === DEFAULT_AI_CONTEXT_SIZE_CHOICE) {
return Environment.userSettingsContextSizeDefaultText;
}
return Environment.getUserSettingsContextSizeText(choice);
}
export function getVoiceModeLabel(mode: UserAiVoiceMode): string {
switch (mode) {
case AI_VOICE_MODE_EXECUTE:
return Environment.userSettingsVoiceModeExecuteText;
case AI_VOICE_MODE_TRANSCRIPT:
return Environment.userSettingsVoiceModeTranscriptText;
}
}
export function getInterfaceLanguageLabel(language: UserInterfaceLanguage): string {
return Localization.languageLabel(language);
}
export function getResponseLanguageInstruction(language: UserAiResponseLanguage): string {
const instructions = [
"Language:"
];
if (language === DEFAULT_LANGUAGE_CHOICE) {
instructions.push("Always answer in the language of the users latest message unless explicitly asked otherwise.");
} else {
instructions.push(`Always answer to the user in ${Localization.languageInstructionName(language)}. If the user specifically requests another language, comply with that request.`);
}
return instructions.join("\n");
}
function shouldUpdateInterfaceLanguage(user: StoredUser | null, language: UserInterfaceLanguage): boolean {
return !!user?.interfaceLanguage && user.interfaceLanguage !== language;
}
function shouldUpdateProvider(user: StoredUser | null, choice: UserAiProviderChoice): boolean {
return !!user?.aiProvider && user.aiProvider !== choice;
}
function shouldUpdateLanguage(user: StoredUser | null, language: UserAiResponseLanguage): boolean {
return !!user?.aiResponseLanguage && user.aiResponseLanguage !== language;
}
function shouldUpdateContextSize(user: StoredUser | null, choice: UserAiContextSizeChoice): boolean {
if (!user) return false;
if (choice === DEFAULT_AI_CONTEXT_SIZE_CHOICE && user.aiContextSize === undefined) return false;
return normalizeAiContextSizeChoice(user.aiContextSize) !== choice;
}
function shouldUpdateVoiceMode(user: StoredUser | null, mode: UserAiVoiceMode): boolean {
return !!user?.aiVoiceMode && user.aiVoiceMode !== mode;
}
function contextSizeChoiceToStored(choice: UserAiContextSizeChoice): number | undefined {
return choice === DEFAULT_AI_CONTEXT_SIZE_CHOICE ? undefined : choice;
}
export async function ensureValidUserAiSettings(userId: number): Promise<EffectiveUserAiSettings> {
const user = await UserStore.get(userId);
const availableProviderChoices = getAvailableAiProviderChoices(userId);
let availableContextSizeChoices = getUserAiContextSizeChoices();
const availableVoiceModes = getUserAiVoiceModes();
let providerChoice = normalizeAiProviderChoice(user?.aiProvider) ?? DEFAULT_AI_PROVIDER_CHOICE;
let interfaceLanguage = normalizeInterfaceLanguage(user?.interfaceLanguage) ?? DEFAULT_INTERFACE_LANGUAGE;
let responseLanguage = normalizeAiResponseLanguage(user?.aiResponseLanguage) ?? DEFAULT_AI_RESPONSE_LANGUAGE;
let contextSizeChoice = normalizeAiContextSizeChoice(user?.aiContextSize) ?? DEFAULT_AI_CONTEXT_SIZE_CHOICE;
let voiceMode = normalizeAiVoiceMode(user?.aiVoiceMode) ?? DEFAULT_AI_VOICE_MODE;
if (!availableProviderChoices.includes(providerChoice)) {
providerChoice = availableProviderChoices[0] ?? DEFAULT_AI_PROVIDER_CHOICE;
}
if (!Localization.isKnownLanguageChoice(interfaceLanguage)) {
interfaceLanguage = DEFAULT_INTERFACE_LANGUAGE;
}
if (!Localization.isKnownLanguageChoice(responseLanguage)) {
responseLanguage = DEFAULT_AI_RESPONSE_LANGUAGE;
}
if (contextSizeChoice !== DEFAULT_AI_CONTEXT_SIZE_CHOICE && !normalizeAiContextSizeChoice(contextSizeChoice)) {
contextSizeChoice = DEFAULT_AI_CONTEXT_SIZE_CHOICE;
}
if (!availableContextSizeChoices.includes(contextSizeChoice)) {
availableContextSizeChoices = [
DEFAULT_AI_CONTEXT_SIZE_CHOICE,
...[
...availableContextSizeChoices.filter(choice => choice !== DEFAULT_AI_CONTEXT_SIZE_CHOICE),
contextSizeChoice,
].sort((a, b) => Number(a) - Number(b)),
];
}
if (!availableVoiceModes.includes(voiceMode)) {
voiceMode = DEFAULT_AI_VOICE_MODE;
}
if (
shouldUpdateProvider(user, providerChoice)
|| shouldUpdateInterfaceLanguage(user, interfaceLanguage)
|| shouldUpdateLanguage(user, responseLanguage)
|| shouldUpdateContextSize(user, contextSizeChoice)
|| shouldUpdateVoiceMode(user, voiceMode)
) {
await UserStore.updateSettings(userId, {
interfaceLanguage,
aiProvider: providerChoice,
aiResponseLanguage: responseLanguage,
aiContextSize: contextSizeChoiceToStored(contextSizeChoice),
aiVoiceMode: voiceMode,
});
}
return {
userId,
tier: getUserTier(userId),
providerChoice,
effectiveProvider: providerChoice === DEFAULT_AI_PROVIDER_CHOICE ? Environment.DEFAULT_AI_PROVIDER : providerChoice,
interfaceLanguage,
responseLanguage,
contextSizeChoice,
contextSize: contextSizeChoiceToStored(contextSizeChoice),
voiceMode,
availableProviderChoices,
availableContextSizeChoices,
availableVoiceModes,
};
}
export async function resolveEffectiveAiProviderForUser(userId: number | undefined): Promise<AiProvider> {
if (!userId) return Environment.DEFAULT_AI_PROVIDER;
return (await ensureValidUserAiSettings(userId)).effectiveProvider;
}
export async function resolveAiResponseLanguageForUser(userId: number | undefined): Promise<UserAiResponseLanguage> {
if (!userId) return DEFAULT_AI_RESPONSE_LANGUAGE;
return (await ensureValidUserAiSettings(userId)).responseLanguage;
}
export async function resolveAiContextSizeForUser(userId: number | undefined): Promise<number | undefined> {
if (!userId) return undefined;
return (await ensureValidUserAiSettings(userId)).contextSize;
}
export async function resolveAiVoiceModeForUser(userId: number | undefined): Promise<UserAiVoiceMode> {
if (!userId) return DEFAULT_AI_VOICE_MODE;
return (await ensureValidUserAiSettings(userId)).voiceMode;
}
export async function resolveInterfaceLocaleForUser(
userId: number | undefined,
telegramLanguageCode?: string,
): Promise<string> {
if (!userId) {
return Localization.resolveLocale(DEFAULT_INTERFACE_LANGUAGE, telegramLanguageCode);
}
const settings = await ensureValidUserAiSettings(userId);
const user = await UserStore.get(userId);
return Localization.resolveLocale(settings.interfaceLanguage, telegramLanguageCode ?? user?.langCode);
}
export async function setUserAiProviderChoice(
userId: number,
choice: UserAiProviderChoice,
): Promise<{ ok: boolean; settings: EffectiveUserAiSettings }> {
const settings = await ensureValidUserAiSettings(userId);
if (!settings.availableProviderChoices.includes(choice)) {
return {ok: false, settings};
}
await UserStore.updateSettings(userId, {
interfaceLanguage: settings.interfaceLanguage,
aiProvider: choice,
aiResponseLanguage: settings.responseLanguage,
});
return {ok: true, settings: await ensureValidUserAiSettings(userId)};
}
export async function setUserAiContextSizeChoice(
userId: number,
choice: UserAiContextSizeChoice,
): Promise<{ ok: boolean; settings: EffectiveUserAiSettings }> {
const settings = await ensureValidUserAiSettings(userId);
const normalized = normalizeAiContextSizeChoice(choice);
if (!normalized) {
return {ok: false, settings};
}
await UserStore.updateSettings(userId, {
aiContextSize: contextSizeChoiceToStored(normalized),
});
return {ok: true, settings: await ensureValidUserAiSettings(userId)};
}
export async function setUserAiVoiceMode(
userId: number,
mode: UserAiVoiceMode,
): Promise<{ ok: boolean; settings: EffectiveUserAiSettings }> {
const settings = await ensureValidUserAiSettings(userId);
if (!getUserAiVoiceModes().includes(mode)) {
return {ok: false, settings};
}
await UserStore.updateSettings(userId, {
aiVoiceMode: mode,
});
return {ok: true, settings: await ensureValidUserAiSettings(userId)};
}
export async function setUserAiResponseLanguage(
userId: number,
language: UserAiResponseLanguage,
): Promise<{ ok: boolean; settings: EffectiveUserAiSettings }> {
const settings = await ensureValidUserAiSettings(userId);
if (!Localization.isKnownLanguageChoice(language)) {
return {ok: false, settings};
}
await UserStore.updateSettings(userId, {
interfaceLanguage: settings.interfaceLanguage,
aiProvider: settings.providerChoice,
aiResponseLanguage: language,
});
return {ok: true, settings: await ensureValidUserAiSettings(userId)};
}
export async function setUserInterfaceLanguage(
userId: number,
language: UserInterfaceLanguage,
): Promise<{ ok: boolean; settings: EffectiveUserAiSettings }> {
const settings = await ensureValidUserAiSettings(userId);
if (!Localization.isKnownLanguageChoice(language)) {
return {ok: false, settings};
}
await UserStore.updateSettings(userId, {
interfaceLanguage: language,
aiProvider: settings.providerChoice,
aiResponseLanguage: settings.responseLanguage,
});
return {ok: true, settings: await ensureValidUserAiSettings(userId)};
}
+196
View File
@@ -0,0 +1,196 @@
import {InlineKeyboardMarkup} from "typescript-telegram-bot-api";
import {Environment} from "./environment";
import {
DEFAULT_AI_PROVIDER_CHOICE,
EffectiveUserAiSettings,
getContextSizeLabel,
getInterfaceLanguageLabel,
getProviderChoiceLabel,
getResponseLanguageLabel,
getVoiceModeLabel,
getUserLanguageChoices,
UserAiContextSizeChoice,
UserAiProviderChoice,
UserAiResponseLanguage,
UserAiVoiceMode,
UserInterfaceLanguage,
} from "./user-ai-settings";
export const USER_SETTINGS_CALLBACK_PREFIX = "/settings";
export type UserSettingsScreen = "main" | "provider" | "interfaceLanguage" | "responseLanguage" | "contextSize" | "voiceMode";
function tierLabel(tier: EffectiveUserAiSettings["tier"]): string {
const labels: Record<EffectiveUserAiSettings["tier"], string> = {
creator: Environment.userSettingsCreatorTierText,
admin: Environment.userSettingsAdminTierText,
user: Environment.userSettingsUserTierText,
};
return labels[tier];
}
function callbackData(settings: EffectiveUserAiSettings, screen: UserSettingsScreen, value?: string): string {
return [USER_SETTINGS_CALLBACK_PREFIX, String(settings.userId), screen, value].filter(Boolean).join(" ");
}
function selectedText(selected: boolean, text: string): string {
return selected ? Environment.getUserSettingsSelectedText(text) : text;
}
function currentProviderText(settings: EffectiveUserAiSettings): string {
if (settings.providerChoice !== DEFAULT_AI_PROVIDER_CHOICE) {
return getProviderChoiceLabel(settings.providerChoice);
}
return `${getProviderChoiceLabel(DEFAULT_AI_PROVIDER_CHOICE)} (${getProviderChoiceLabel(Environment.DEFAULT_AI_PROVIDER)})`;
}
export function formatUserSettingsText(settings: EffectiveUserAiSettings, screen: UserSettingsScreen = "main"): string {
const title = Environment.getUserSettingsTitle(screen);
return [
title,
"",
Environment.getUserSettingsFieldText(Environment.userSettingsTierLabel, tierLabel(settings.tier)),
Environment.getUserSettingsFieldText(Environment.userSettingsAiProviderLabel, currentProviderText(settings)),
Environment.getUserSettingsFieldText(Environment.userSettingsInterfaceLanguageLabel, getInterfaceLanguageLabel(settings.interfaceLanguage)),
Environment.getUserSettingsFieldText(Environment.userSettingsResponseLanguageLabel, getResponseLanguageLabel(settings.responseLanguage)),
Environment.getUserSettingsFieldText(Environment.userSettingsContextSizeLabel, getContextSizeLabel(settings.contextSizeChoice)),
Environment.getUserSettingsFieldText(Environment.userSettingsVoiceModeLabel, getVoiceModeLabel(settings.voiceMode)),
].join("\n");
}
export function buildUserSettingsKeyboard(settings: EffectiveUserAiSettings, screen: UserSettingsScreen = "main"): InlineKeyboardMarkup {
if (screen === "provider") {
return {
inline_keyboard: [
...settings.availableProviderChoices.map(choice => {
const text = choice === DEFAULT_AI_PROVIDER_CHOICE
? currentProviderText({...settings, providerChoice: DEFAULT_AI_PROVIDER_CHOICE})
: getProviderChoiceLabel(choice);
return [{
text: selectedText(settings.providerChoice === choice, text),
callback_data: callbackData(settings, "provider", choice),
}];
}),
[{text: Environment.userSettingsBackButtonText, callback_data: callbackData(settings, "main")}],
],
};
}
if (screen === "interfaceLanguage") {
return {
inline_keyboard: [
...getUserLanguageChoices().map(language => [{
text: selectedText(settings.interfaceLanguage === language, getInterfaceLanguageLabel(language)),
callback_data: callbackData(settings, "interfaceLanguage", language),
}]),
[{text: Environment.userSettingsBackButtonText, callback_data: callbackData(settings, "main")}],
],
};
}
if (screen === "responseLanguage") {
return {
inline_keyboard: [
...getUserLanguageChoices().map(language => [{
text: selectedText(settings.responseLanguage === language, getResponseLanguageLabel(language)),
callback_data: callbackData(settings, "responseLanguage", language),
}]),
[{text: Environment.userSettingsBackButtonText, callback_data: callbackData(settings, "main")}],
],
};
}
if (screen === "contextSize") {
return {
inline_keyboard: [
...settings.availableContextSizeChoices.map(choice => [{
text: selectedText(settings.contextSizeChoice === choice, getContextSizeLabel(choice)),
callback_data: callbackData(settings, "contextSize", String(choice)),
}]),
[{text: Environment.userSettingsBackButtonText, callback_data: callbackData(settings, "main")}],
],
};
}
if (screen === "voiceMode") {
return {
inline_keyboard: [
...settings.availableVoiceModes.map(mode => [{
text: selectedText(settings.voiceMode === mode, getVoiceModeLabel(mode)),
callback_data: callbackData(settings, "voiceMode", mode),
}]),
[{text: Environment.userSettingsBackButtonText, callback_data: callbackData(settings, "main")}],
],
};
}
return {
inline_keyboard: [
[{
text: Environment.getUserSettingsFieldText(Environment.userSettingsAiProviderButtonPrefix, currentProviderText(settings)),
callback_data: callbackData(settings, "provider")
}],
[{
text: Environment.getUserSettingsFieldText(Environment.userSettingsInterfaceLanguageButtonPrefix, getInterfaceLanguageLabel(settings.interfaceLanguage)),
callback_data: callbackData(settings, "interfaceLanguage")
}],
[{
text: Environment.getUserSettingsFieldText(Environment.userSettingsResponseLanguageButtonPrefix, getResponseLanguageLabel(settings.responseLanguage)),
callback_data: callbackData(settings, "responseLanguage")
}],
[{
text: Environment.getUserSettingsFieldText(Environment.userSettingsContextSizeButtonPrefix, getContextSizeLabel(settings.contextSizeChoice)),
callback_data: callbackData(settings, "contextSize")
}],
[{
text: Environment.getUserSettingsFieldText(Environment.userSettingsVoiceModeButtonPrefix, getVoiceModeLabel(settings.voiceMode)),
callback_data: callbackData(settings, "voiceMode")
}],
],
};
}
export function parseUserSettingsCallbackData(data: string | undefined): {
userId: number;
screen: UserSettingsScreen;
providerChoice?: UserAiProviderChoice;
interfaceLanguage?: UserInterfaceLanguage;
responseLanguage?: UserAiResponseLanguage;
contextSizeChoice?: UserAiContextSizeChoice | string;
voiceMode?: UserAiVoiceMode;
} | null {
if (!data?.startsWith(USER_SETTINGS_CALLBACK_PREFIX)) return null;
const [, userIdValue, screenValue, value] = data.split(" ");
const userId = Number(userIdValue);
const screen = (screenValue === "language" ? "responseLanguage" : screenValue || "main") as UserSettingsScreen;
if (!Number.isSafeInteger(userId)) {
return null;
}
if (
screen !== "main"
&& screen !== "provider"
&& screen !== "interfaceLanguage"
&& screen !== "responseLanguage"
&& screen !== "contextSize"
&& screen !== "voiceMode"
) {
return null;
}
return {
userId,
screen,
providerChoice: screen === "provider" ? value as UserAiProviderChoice | undefined : undefined,
interfaceLanguage: screen === "interfaceLanguage" ? value as UserInterfaceLanguage | undefined : undefined,
responseLanguage: screen === "responseLanguage" ? value as UserAiResponseLanguage | undefined : undefined,
contextSizeChoice: screen === "contextSize" ? value as UserAiContextSizeChoice | string | undefined : undefined,
voiceMode: screen === "voiceMode" ? value as UserAiVoiceMode | undefined : undefined,
};
}
+23 -3
View File
@@ -1,6 +1,9 @@
import {User} from "typescript-telegram-bot-api";
import {userDao} from "../index";
import {StoredUser} from "../model/stored-user";
import {getLruMapValue, setLruMapValue} from "../util/lru-map";
const USER_CACHE_MAX_ENTRIES = 5_000;
export class UserStore {
private static map = new Map<number, StoredUser>();
@@ -10,6 +13,7 @@ export class UserStore {
}
static async put(u: User): Promise<StoredUser> {
const current = getLruMapValue(this.map, u.id);
const user: StoredUser = {
id: u.id,
isBot: u.is_bot,
@@ -17,23 +21,39 @@ export class UserStore {
lastName: u.last_name,
userName: u.username,
isPremium: u.is_premium,
langCode: u.language_code,
interfaceLanguage: current?.interfaceLanguage,
aiProvider: current?.aiProvider,
aiResponseLanguage: current?.aiResponseLanguage,
aiContextSize: current?.aiContextSize,
aiVoiceMode: current?.aiVoiceMode,
};
this.map.set(u.id, user);
setLruMapValue(this.map, u.id, user, USER_CACHE_MAX_ENTRIES);
await userDao.insert(userDao.mapTo([u]));
return user;
}
static async updateSettings(
id: number,
settings: Partial<Pick<StoredUser, "interfaceLanguage" | "aiProvider" | "aiResponseLanguage" | "aiContextSize" | "aiVoiceMode">>
): Promise<StoredUser | null> {
await userDao.updateSettings(id, settings);
const user = await userDao.getById({id});
if (user) setLruMapValue(this.map, id, user, USER_CACHE_MAX_ENTRIES);
return user;
}
static async get(id: number): Promise<StoredUser | null> {
const user = await userDao.getById({id: id});
if (!user) return null;
this.map.set(id, user);
setLruMapValue(this.map, id, user, USER_CACHE_MAX_ENTRIES);
return user;
}
static clear() {
this.map.clear();
}
}
}