From f7853e1e5d8afff8def08cf0e710f81dd6d81696 Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:58:17 +0200 Subject: [PATCH 01/51] feature(events): extended event list to support directories, save events by their id This commit adds the ability to add events sorted by directories generated by https://github.com/Malex14/hio_timetable_extractor. Each directory consists of a name and potentially containing events and/or subdirectories. Events are stored in the userconfig by their id. The event's name is now being saved in the corresponding eventDetails. The filter functionality has been updated to support directories as well. --- source/lib/all-events.ts | 120 +++++++++++++++++----- source/lib/calendar-helper.ts | 6 +- source/lib/change-helper.ts | 22 +++-- source/lib/inline-menu-filter.ts | 2 +- source/lib/types.ts | 15 ++- source/menu/about.ts | 2 +- source/menu/events/add.ts | 126 ++++++++++++++++-------- source/menu/events/changes/add/index.ts | 44 ++++----- source/menu/events/changes/details.ts | 26 ++--- source/menu/events/changes/index.ts | 11 ++- source/menu/events/details.ts | 50 +++++----- source/menu/events/index.ts | 36 ++++--- source/parts/changes-inline.ts | 47 ++++----- 13 files changed, 322 insertions(+), 185 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 563ac4ad..94f3116f 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,40 +1,106 @@ -import {readFile} from 'node:fs/promises'; +import {readFile, watch} from 'node:fs/promises'; +import type { + EventDirectory, EventId, Events, +} from './types.ts'; -async function getAll(): Promise { - const data = await readFile('eventfiles/all.txt', 'utf8'); - const list = data.split('\n').filter(element => element !== ''); - return list; +const DIRECTORY_FILE = 'eventfiles/directory.json'; + +const directory = await loadDirectory(); +const namesOfEvents: Record = await generateMapping(); + +async function watchForDirectoryChanges() { + const watcher = watch(DIRECTORY_FILE); + for await (const event of watcher) { + console.log(event); + if (event.eventType === 'change') { + await loadDirectory(); + await generateMapping(); + } + } } -export async function count(): Promise { - const allEvents = await getAll(); - return allEvents.length; +await watchForDirectoryChanges(); + +async function loadDirectory(): Promise> { + const directoryString = await readFile(DIRECTORY_FILE); + const directory = JSON.parse(directoryString.toString()) as Partial; + return directory; } -export async function exists(name: string): Promise { - const allEvents = await getAll(); - return allEvents.includes(name); +async function generateMapping(): Promise> { + const namesOfEvents: Record = {}; + + function collect(directory: Partial) { + for (const subDirectory of Object.values(directory.subDirectories ?? {})) { + collect(subDirectory); + } + + Object.assign(namesOfEvents, directory.events ?? {}); + } + + collect(directory); + + return namesOfEvents; } -export async function nonExisting(names: readonly string[]): Promise { - const allEvents = new Set(await getAll()); - const result: string[] = []; - for (const event of names) { - if (!allEvents.has(event)) { - result.push(event); +function resolvePath(path: string[]): Partial { + let resolvedDirectory = directory; + + for (const part of path) { + if (resolvedDirectory.subDirectories === undefined || !(part in resolvedDirectory.subDirectories)) { + throw new Error('Ungültiger Pfad'); } + + resolvedDirectory = resolvedDirectory.subDirectories[part]!; } - return result; + return resolvedDirectory; } -export async function find( - pattern: string | RegExp, - ignore: readonly string[] = [], -): Promise { - const allEvents = await getAll(); - const regex = new RegExp(pattern, 'i'); - const filtered = allEvents.filter(event => - regex.test(event) && !ignore.includes(event)); - return filtered; +export function getEventName(id: EventId): string { + return namesOfEvents[id] ?? id; +} + +export function count(): number { + return Object.keys(namesOfEvents).length; +} + +export function nonExisting(ids: readonly EventId[]): readonly EventId[] { + return ids.filter(id => !(id in namesOfEvents)); +} + +export function find( + pattern: string | RegExp | undefined, + startAt: string[] = [], +): Readonly { + if (pattern !== undefined) { + const regex = new RegExp(pattern, 'i'); + const accumulator: Events = {}; + + function collect(directory: Partial) { + for (const [eventId, name] of Object.entries(directory.events ?? {})) { + if (regex.test(name)) { + accumulator[eventId as EventId] = name; + } + } + + for (const subDirectory of Object.values(directory.subDirectories ?? {})) { + collect(subDirectory); + } + } + + collect(resolvePath(startAt)); + + return { + subDirectories: {}, + events: Object.fromEntries(Object.entries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), + }; + } + + const directory = resolvePath(startAt); + + return { + subDirectories: directory.subDirectories ?? {}, + events: directory.events ?? {}, + }; } diff --git a/source/lib/calendar-helper.ts b/source/lib/calendar-helper.ts index c85e1080..8fb6a436 100644 --- a/source/lib/calendar-helper.ts +++ b/source/lib/calendar-helper.ts @@ -1,4 +1,4 @@ -import type {MyContext, Userconfig} from './types.ts'; +import type {EventId, MyContext, Userconfig} from './types.ts'; export function getUrl(id: number, userconfig: Userconfig): string { let filename = `${id}`; @@ -14,3 +14,7 @@ export function getUrl(id: number, userconfig: Userconfig): string { export function getUrlFromContext(ctx: MyContext): string { return getUrl(ctx.from!.id, ctx.userconfig.mine); } + +export function getUserEventIdsFromContext(ctx: MyContext): EventId[] { + return Object.keys(ctx.userconfig.mine.events) as EventId[]; +} diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 892ebf76..4b675bbb 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -1,6 +1,9 @@ import {readFile} from 'node:fs/promises'; import {html as format} from 'telegram-format'; -import type {Change, EventEntry, NaiveDateTime} from './types.ts'; +import type { + Change, EventEntry, EventId, NaiveDateTime, +} from './types.ts'; +import {getEventName} from './all-events.js'; export function generateChangeDescription(change: Change): string { let text = ''; @@ -28,11 +31,11 @@ export function generateChangeDescription(change: Change): string { } export function generateChangeText( - name: string, + eventId: EventId, date: NaiveDateTime | undefined, change: Change, ): string { - let text = generateChangeTextHeader(name, date); + let text = generateChangeTextHeader(eventId, date); if (Object.keys(change).length > 2) { text += '\nÄnderungen:\n'; @@ -43,13 +46,13 @@ export function generateChangeText( } export function generateChangeTextHeader( - name: string, + eventId: EventId, date: NaiveDateTime | undefined, ): string { let text = ''; text += format.bold('Veranstaltungsänderung'); text += '\n'; - text += format.bold(format.escape(name)); + text += format.bold(format.escape(getEventName(eventId))); if (date) { text += ` ${date}`; } @@ -59,16 +62,15 @@ export function generateChangeTextHeader( } export function generateShortChangeText( - name: string, + eventId: EventId, date: NaiveDateTime, ): string { - return `${name} ${date}`; + return `${getEventName(eventId)} ${date}`; } -export async function loadEvents(eventname: string): Promise { +export async function loadEvents(eventId: EventId): Promise { try { - const filename = eventname.replaceAll('/', '-'); - const content = await readFile(`eventfiles/${filename}.json`, 'utf8'); + const content = await readFile(`eventfiles/${eventId}.json`, 'utf8'); return JSON.parse(content) as EventEntry[]; } catch (error) { console.error('ERROR while loading events for change date picker', error); diff --git a/source/lib/inline-menu-filter.ts b/source/lib/inline-menu-filter.ts index 340a2df2..de982fb7 100644 --- a/source/lib/inline-menu-filter.ts +++ b/source/lib/inline-menu-filter.ts @@ -2,7 +2,7 @@ export const DEFAULT_FILTER = '.+'; export function filterButtonText(getCurrentFilterFunction: (ctx: T) => string | undefined): (ctx: T) => string { return ctx => { - let text = '🔎 Filter'; + let text = '🔎 Ab hier filtern'; const currentFilter = getCurrentFilterFunction(ctx); if (currentFilter && currentFilter !== '.+') { text += ': ' + currentFilter; diff --git a/source/lib/types.ts b/source/lib/types.ts index ca4c8cf6..ac664422 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -23,7 +23,9 @@ export type Session = { adminuserquicklook?: number; // User ID adminuserquicklookfilter?: string; eventfilter?: string; - generateChangeName?: string; + eventPath?: string[]; // Path to the selected subdirectory + eventDirectorySubDirectoryItems?: string[]; // Subdirectory item keys of the directory selected by eventPath + generateChangeEventId?: EventId; generateChangeDate?: NaiveDateTime; generateChange?: Partial; page?: number; @@ -37,7 +39,7 @@ export type Session = { export type Userconfig = { readonly admin?: true; calendarfileSuffix: string; - events: Record; + events: Record; mensa: MensaSettings; removedEvents?: RemovedEventsDisplayStyle; }; @@ -82,6 +84,15 @@ export type MensaSettings = MealWishes & { showAdditives?: boolean; }; +export type EventId = `${number}_${number | string}`; + +export type Events = Record; + +export type EventDirectory = { + readonly subDirectories: Record>; + readonly events: Events; +}; + export type EventEntry = { readonly name: string; readonly location: string; diff --git a/source/menu/about.ts b/source/menu/about.ts index a3fd912d..9bfa5cae 100644 --- a/source/menu/about.ts +++ b/source/menu/about.ts @@ -10,7 +10,7 @@ export const menu = new MenuTemplate(async ctx => { const canteens = await getCanteenList(); const canteenCount = canteens.length; - const eventCount = await allEvents.count(); + const eventCount = allEvents.count(); const websiteLink = format.url( 'hawhh.de/calendarbot/', diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 501fa887..f4fce1e8 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -1,42 +1,34 @@ import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Composer} from 'grammy'; import { - deleteMenuFromContext, - getMenuOfPath, - MenuTemplate, - replyMenuToContext, + deleteMenuFromContext, getMenuOfPath, MenuTemplate, replyMenuToContext, } from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; -import { - count as allEventsCount, - exists as allEventsExists, - find as allEventsFind, -} from '../../lib/all-events.ts'; -import { - DEFAULT_FILTER, - filterButtonText, -} from '../../lib/inline-menu-filter.ts'; -import {backMainButtons} from '../../lib/inline-menu.ts'; -import type {MyContext} from '../../lib/types.ts'; +import {count as allEventsCount, find as allEventsFind, getEventName} from '../../lib/all-events.ts'; +import {filterButtonText} from '../../lib/inline-menu-filter.ts'; +import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; +import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; +import {getUserEventIdsFromContext} from '../../lib/calendar-helper.js'; const MAX_RESULT_ROWS = 10; -const RESULT_COLUMNS = 2; +const RESULT_COLUMNS = 1; export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { - const total = await allEventsCount(); + const total = allEventsCount(); + ctx.session.eventPath ??= []; let text = format.bold('Veranstaltungen'); text += '\nWelche Events möchtest du hinzufügen?'; text += '\n\n'; try { - const filteredEvents = await findEvents(ctx); + const filteredEvents = findEvents(ctx); - const filter = ctx.session.eventfilter ?? DEFAULT_FILTER; - text += filter === DEFAULT_FILTER + const filter = ctx.session.eventfilter; + text += filter === undefined ? `Ich habe ${total} Veranstaltungen. Nutze den Filter um die Auswahl einzugrenzen.` - : `Mit deinem Filter konnte ich ${filteredEvents.length} passende Veranstaltungen finden.`; + : `Mit deinem Filter konnte ich ${Object.keys(filteredEvents.events).length} passende Veranstaltungen und ${Object.keys(filteredEvents.subDirectories).length} Ordner finden.`; } catch (error) { const errorText = error instanceof Error ? error.message : String(error); text += 'Filter Error: '; @@ -46,10 +38,9 @@ export const menu = new MenuTemplate(async ctx => { return {text, parse_mode: format.parse_mode}; }); -async function findEvents(ctx: MyContext): Promise { - const filter = ctx.session.eventfilter ?? DEFAULT_FILTER; - const ignore = Object.keys(ctx.userconfig.mine.events); - return allEventsFind(filter, ignore); +function findEvents(ctx: MyContext): Readonly { + const filter = ctx.session.eventfilter; + return allEventsFind(filter, ctx.session.eventPath); } const question = new StatelessQuestion( @@ -70,7 +61,7 @@ menu.interact('filter', { async do(ctx, path) { await question.replyWithHTML( ctx, - 'Wonach möchtest du die Veranstaltungen filtern?', + 'Wonach möchtest du die Veranstaltungen in diesem Verzeichnis filtern?', getMenuOfPath(path), ); await deleteMenuFromContext(ctx); @@ -81,7 +72,7 @@ menu.interact('filter', { menu.interact('filter-clear', { text: 'Filter aufheben', joinLastRow: true, - hide: ctx => (ctx.session.eventfilter ?? DEFAULT_FILTER) === DEFAULT_FILTER, + hide: ctx => ctx.session.eventfilter === undefined, do(ctx) { delete ctx.session.eventfilter; return true; @@ -93,31 +84,63 @@ menu.choose('a', { columns: RESULT_COLUMNS, async choices(ctx) { try { - const all = await findEvents(ctx); - return Object.fromEntries(all.map(event => [event.replaceAll('/', ';'), event])); + const filteredEvents = findEvents(ctx); + const alreadySelected = Object.keys(ctx.userconfig.mine.events); + + ctx.session.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); + const subDirectoryItems = Object.entries(filteredEvents.subDirectories) + .map(([name, directory], i) => + directory.subDirectories !== undefined || directory.events !== undefined + ? ['d' + i, '🗂️ ' + name] + : ['x' + i, '🚫 ' + name]); + + const eventItems = Object.entries(filteredEvents.events) + .map(([eventId, name]) => + alreadySelected.includes(eventId) + ? ['e' + eventId, '✅ ' + name] + : ['e' + eventId, '📅 ' + name]); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Object.fromEntries([ + ...subDirectoryItems, + ...eventItems, + ]); } catch { return {}; } }, async do(ctx, key) { - const event = key.replaceAll(';', '/'); - const isExisting = await allEventsExists(event); - const isAlreadyInCalendar = Object.keys(ctx.userconfig.mine.events) - .includes(event); - - if (!isExisting) { - await ctx.answerCallbackQuery(`${event} existiert nicht!`); + if (key.startsWith('e')) { + const eventId = key.slice(1) as EventId; + const eventName = getEventName(eventId); + const isAlreadyInCalendar = getUserEventIdsFromContext(ctx).includes(eventId); + + if (eventName === undefined) { + await ctx.answerCallbackQuery(`Event mit Id ${eventId} existiert nicht!`); + return true; + } + + if (isAlreadyInCalendar) { + await ctx.answerCallbackQuery(`${eventName} ist bereits in deinem Kalender!`); + return true; + } + + ctx.userconfig.mine.events[eventId] = {}; + await ctx.answerCallbackQuery(`${eventName} wurde zu deinem Kalender hinzugefügt.`); return true; } - if (isAlreadyInCalendar) { - await ctx.answerCallbackQuery(`${event} ist bereits in deinem Kalender!`); + if (key.startsWith('d')) { + const chosenSubDirectory = key.slice(1); + ctx.session.eventPath?.push(chosenSubDirectory); + delete ctx.session.eventDirectorySubDirectoryItems; + return true; } - ctx.userconfig.mine.events[event] = {}; - await ctx.answerCallbackQuery(`${event} wurde zu deinem Kalender hinzugefügt.`); - return true; + await ctx.answerCallbackQuery('Dieses Verzeichnis ist leer.'); + + return false; }, getCurrentPage: ctx => ctx.session.page, setPage(ctx, page) { @@ -125,4 +148,23 @@ menu.choose('a', { }, }); -menu.manualRow(backMainButtons); +menu.interact('back', { + text: BACK_BUTTON_TEXT, + async do(ctx) { + if (ctx.session.eventfilter !== undefined) { + delete ctx.session.eventfilter; + + return true; + } + + if (ctx.session.eventPath?.length === 0) { + delete ctx.session.eventPath; + delete ctx.session.eventDirectorySubDirectoryItems; + + return '..'; + } + + ctx.session.eventPath?.pop(); + return true; + }, +}); diff --git a/source/menu/events/changes/add/index.ts b/source/menu/events/changes/add/index.ts index b03c9fe3..085d0a98 100644 --- a/source/menu/events/changes/add/index.ts +++ b/source/menu/events/changes/add/index.ts @@ -12,7 +12,7 @@ import { loadEvents, } from '../../../../lib/change-helper.ts'; import type { - Change, + Change, EventId, MyContext, NaiveDateTime, } from '../../../../lib/types.ts'; @@ -23,23 +23,23 @@ export const menu = new MenuTemplate(ctx => { ctx.session.generateChange ??= {}; if (ctx.match) { - ctx.session.generateChangeName = ctx.match[1]!.replaceAll(';', '/'); + ctx.session.generateChangeEventId = ctx.match[1]! as EventId; } - if (!ctx.session.generateChangeName) { + if (!ctx.session.generateChangeEventId) { throw new Error('Something fishy'); } - const name = ctx.session.generateChangeName; + const eventId = ctx.session.generateChangeEventId; let text = ''; if (!ctx.session.generateChangeDate) { text = 'Zu welchem Termin willst du eine Änderung hinzufügen?'; - const changeDates = Object.keys(ctx.userconfig.mine.events[name]?.changes ?? {}); + const changeDates = Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); if (changeDates.length > 0) { text - += '\n\nFolgende Termine habe bereits eine Veränderung. Entferne die Veränderung zuerst, bevor du eine neue erstellen kannst.'; + += '\n\nFolgende Termine haben bereits eine Veränderung. Entferne die Veränderung zuerst, bevor du eine neue erstellen kannst.'; text += '\n'; changeDates.sort(); @@ -47,9 +47,9 @@ export const menu = new MenuTemplate(ctx => { } } - if (ctx.session.generateChangeName && ctx.session.generateChangeDate) { + if (ctx.session.generateChangeEventId && ctx.session.generateChangeDate) { text = generateChangeText( - ctx.session.generateChangeName, + ctx.session.generateChangeEventId, ctx.session.generateChangeDate, ctx.session.generateChange as Change, ); @@ -60,21 +60,21 @@ export const menu = new MenuTemplate(ctx => { }); function hidePickDateStep(ctx: MyContext): boolean { - const name = ctx.session.generateChangeName; + const eventId = ctx.session.generateChangeEventId; const date = ctx.session.generateChangeDate; - return !name || Boolean(date); + return !eventId || Boolean(date); } function hideGenerateChangeStep(ctx: MyContext): boolean { - const name = ctx.session.generateChangeName; + const eventId = ctx.session.generateChangeEventId; const date = ctx.session.generateChangeDate; - return !name || !date; + return !eventId || !date; } function generationDataIsValid(ctx: MyContext): boolean { - const name = ctx.session.generateChangeName; + const eventId = ctx.session.generateChangeEventId; const date = ctx.session.generateChangeDate; - if (!name || !date) { + if (!eventId || !date) { return false; } @@ -87,14 +87,14 @@ menu.choose('date', { columns: 2, hide: hidePickDateStep, async choices(ctx) { - const name = ctx.match![1]!.replaceAll(';', '/'); + const eventId = ctx.match![1]! as EventId; if (ctx.session.generateChangeDate) { // Date already selected return {}; } - const existingChangeDates = new Set(Object.keys(ctx.userconfig.mine.events[name]?.changes ?? {})); - const events = await loadEvents(name); + const existingChangeDates = new Set(Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {})); + const events = await loadEvents(eventId); const dates = events .map(o => o.startTime) .filter(o => !existingChangeDates.has(o)) @@ -200,14 +200,14 @@ menu.interact('finish', { }); async function finish(ctx: MyContext): Promise { - const name = ctx.match![1]!.replaceAll(';', '/'); + const eventId = ctx.match![1]! as EventId; const date = ctx.session.generateChangeDate!; const change = ctx.session.generateChange!; - ctx.userconfig.mine.events[name] ??= {}; - ctx.userconfig.mine.events[name].changes ??= {}; + ctx.userconfig.mine.events[eventId] ??= {}; + ctx.userconfig.mine.events[eventId].changes ??= {}; - const alreadyExists = ctx.userconfig.mine.events[name].changes[date]; + const alreadyExists = ctx.userconfig.mine.events[eventId].changes[date]; if (alreadyExists) { // Dont do something when there is already a change for the date // This shouldn't occour but it can when the user adds a shared change @@ -216,7 +216,7 @@ async function finish(ctx: MyContext): Promise { return true; } - ctx.userconfig.mine.events[name].changes[date] = change as Change; + ctx.userconfig.mine.events[eventId].changes[date] = change as Change; delete ctx.session.generateChange; return `../d:${date}/`; diff --git a/source/menu/events/changes/details.ts b/source/menu/events/changes/details.ts index 19bc8071..966967ea 100644 --- a/source/menu/events/changes/details.ts +++ b/source/menu/events/changes/details.ts @@ -4,41 +4,43 @@ import { generateShortChangeText, } from '../../../lib/change-helper.ts'; import {backMainButtons} from '../../../lib/inline-menu.ts'; -import type {Change, MyContext, NaiveDateTime} from '../../../lib/types.ts'; +import type { + Change, EventId, MyContext, NaiveDateTime, +} from '../../../lib/types.ts'; -function getChangeFromContext(ctx: MyContext): [string, NaiveDateTime, Change | undefined] { - const name = ctx.match![1]!.replaceAll(';', '/'); +function getChangeFromContext(ctx: MyContext): [EventId, NaiveDateTime, Change | undefined] { + const eventId = ctx.match![1]! as EventId; const date = ctx.match![2]! as NaiveDateTime; - const details = ctx.userconfig.mine.events[name]?.changes?.[date]; - return [name, date, details]; + const details = ctx.userconfig.mine.events[eventId]?.changes?.[date]; + return [eventId, date, details]; } export const menu = new MenuTemplate(ctx => { - const [name, date, change] = getChangeFromContext(ctx); + const [eventId, date, change] = getChangeFromContext(ctx); if (!change) { return 'Change does not exist anymore'; } - const text = generateChangeText(name, date, change); + const text = generateChangeText(eventId, date, change); return {text, parse_mode: 'HTML'}; }); menu.switchToChat({ text: 'Teilen…', query(ctx) { - const [name, date] = getChangeFromContext(ctx); - return generateShortChangeText(name, date); + const [eventId, date] = getChangeFromContext(ctx); + return generateShortChangeText(eventId, date); }, hide(ctx) { - const [_name, _date, change] = getChangeFromContext(ctx); + const [_eventId, _date, change] = getChangeFromContext(ctx); return !change; }, }); menu.interact('r', { text: '⚠️ Änderung entfernen', async do(ctx) { - const [name, date] = getChangeFromContext(ctx); - delete ctx.userconfig.mine.events[name]?.changes?.[date]; + const [eventId, date] = getChangeFromContext(ctx); + delete ctx.userconfig.mine.events[eventId]?.changes?.[date]; await ctx.answerCallbackQuery('Änderung wurde entfernt.'); return '..'; }, diff --git a/source/menu/events/changes/index.ts b/source/menu/events/changes/index.ts index a51e9d24..7e48f501 100644 --- a/source/menu/events/changes/index.ts +++ b/source/menu/events/changes/index.ts @@ -2,18 +2,19 @@ import {Composer} from 'grammy'; import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import {backMainButtons} from '../../../lib/inline-menu.ts'; -import type {MyContext} from '../../../lib/types.ts'; +import type {EventId, MyContext} from '../../../lib/types.ts'; +import {getEventName} from '../../../lib/all-events.js'; import * as changeAdd from './add/index.ts'; import * as changeDetails from './details.ts'; export const bot = new Composer(); export const menu = new MenuTemplate(ctx => { - const event = ctx.match![1]!.replaceAll(';', '/'); + const eventId = ctx.match![1]! as EventId; let text = ''; text += format.bold('Veranstaltungsänderungen'); text += '\n'; - text += format.escape(event); + text += format.escape(getEventName(eventId)); text += '\n\n'; text += format.escape(ctx.t('changes-help')); @@ -27,8 +28,8 @@ menu.submenu('a', changeAdd.menu, {text: '➕ Änderung hinzufügen'}); menu.chooseIntoSubmenu('d', changeDetails.menu, { columns: 1, choices(ctx) { - const event = ctx.match![1]!.replaceAll(';', '/'); - return Object.keys(ctx.userconfig.mine.events[event]?.changes ?? {}); + const eventId = ctx.match![1]! as EventId; + return Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); }, getCurrentPage: ctx => ctx.session.page, setPage(ctx, page) { diff --git a/source/menu/events/details.ts b/source/menu/events/details.ts index b059f21d..e6e8d12b 100644 --- a/source/menu/events/details.ts +++ b/source/menu/events/details.ts @@ -8,25 +8,26 @@ import { } from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import {backMainButtons} from '../../lib/inline-menu.ts'; -import type {MyContext} from '../../lib/types.ts'; +import type {EventId, MyContext} from '../../lib/types.ts'; +import {getEventName} from '../../lib/all-events.js'; import * as changesMenu from './changes/index.ts'; -function getNameFromPath(path: string): string { +function getIdFromPath(path: string): EventId { const match = /\/d:([^/]+)\//.exec(path)!; - return match[1]!.replaceAll(';', '/'); + return match[1]! as EventId; } export const bot = new Composer(); bot.use(changesMenu.bot); export const menu = new MenuTemplate((ctx, path) => { - const name = getNameFromPath(path); - const event = ctx.userconfig.mine.events[name]!; + const eventId = getIdFromPath(path); + const event = ctx.userconfig.mine.events[eventId]!; const changes = Object.keys(event.changes ?? {}).length; let text = format.bold('Veranstaltung'); text += '\n'; - text += name; + text += getEventName(eventId); text += '\n'; if (changes > 0) { @@ -69,15 +70,15 @@ menu.submenu('c', changesMenu.menu, { }); const alertMenu = new MenuTemplate((_, path) => { - const name = getNameFromPath(path); + const name = getEventName(getIdFromPath(path)); return `Wie lange im vorraus möchtest du an einen Termin der Veranstaltung ${name} erinnert werden?`; }); alertMenu.interact('nope', { text: '🔕 Garnicht', do(ctx, path) { - const name = getNameFromPath(path); - delete ctx.userconfig.mine.events[name]!.alertMinutesBefore; + const eventId = getIdFromPath(path); + delete ctx.userconfig.mine.events[eventId]!.alertMinutesBefore; return '..'; }, }); @@ -100,9 +101,9 @@ alertMenu.choose('t', { throw new Error('how?'); } - const name = getNameFromPath(ctx.callbackQuery.data); + const eventId = getIdFromPath(ctx.callbackQuery.data); const minutes = Number(key); - ctx.userconfig.mine.events[name]!.alertMinutesBefore = minutes; + ctx.userconfig.mine.events[eventId]!.alertMinutesBefore = minutes; return '..'; }, }); @@ -114,11 +115,11 @@ menu.submenu('alert', alertMenu, {text: '⏰ Erinnerung'}); const noteQuestion = new StatelessQuestion( 'event-notes', async (ctx, path) => { - const name = getNameFromPath(path); + const eventId = getIdFromPath(path); if (ctx.message.text) { const notes = ctx.message.text; - ctx.userconfig.mine.events[name]!.notes = notes; + ctx.userconfig.mine.events[eventId]!.notes = notes; } await replyMenuToContext(menu, ctx, path); @@ -130,9 +131,9 @@ bot.use(noteQuestion.middleware()); menu.interact('set-notes', { text: '🗒 Schreibe Notiz', async do(ctx, path) { - const name = getNameFromPath(path); + const eventId = getIdFromPath(path); const text = `Welche Notizen möchtest du an den Kalendereinträgen von ${ - format.escape(name) + format.escape(getEventName(eventId)) } stehen haben?`; await noteQuestion.replyWithHTML(ctx, text, getMenuOfPath(path)); await deleteMenuFromContext(ctx); @@ -144,31 +145,32 @@ menu.interact('remove-notes', { text: 'Notiz löschen', joinLastRow: true, hide(ctx, path) { - const name = getNameFromPath(path); - return !ctx.userconfig.mine.events[name]!.notes; + const eventId = getIdFromPath(path); + return !ctx.userconfig.mine.events[eventId]!.notes; }, do(ctx, path) { - const name = getNameFromPath(path); - delete ctx.userconfig.mine.events[name]!.notes; + const eventId = getIdFromPath(path); + delete ctx.userconfig.mine.events[eventId]!.notes; return true; }, }); const removeMenu = new MenuTemplate(ctx => { - const event = ctx.match![1]!.replaceAll(';', '/'); + const eventId = ctx.match![1]! as EventId; return ( - event + getEventName(eventId) + '\n\nBist du dir sicher, dass du diese Veranstaltung entfernen möchtest?' ); }); removeMenu.interact('y', { text: 'Ja ich will!', async do(ctx) { - const event = ctx.match![1]!.replaceAll(';', '/'); + const eventId = ctx.match![1]! as EventId; + const eventName = getEventName(eventId); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete ctx.userconfig.mine.events[event]; + delete ctx.userconfig.mine.events[eventId]; - await ctx.answerCallbackQuery(`${event} wurde aus deinem Kalender entfernt.`); + await ctx.answerCallbackQuery(`${eventName} wurde aus deinem Kalender entfernt.`); return true; }, }); diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index d5b22790..c24d1b3c 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -3,31 +3,35 @@ import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import * as allEvents from '../../lib/all-events.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; -import type {MyContext} from '../../lib/types.ts'; +import type {EventId, MyContext} from '../../lib/types.ts'; +import {getEventName} from '../../lib/all-events.ts'; +import {getUserEventIdsFromContext} from '../../lib/calendar-helper.js'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { + delete ctx.session.eventPath; + let text = format.bold('Veranstaltungen'); text += '\n\n'; - const events = Object.keys(ctx.userconfig.mine.events); - events.sort(); - if (events.length > 0) { - const nonExisting = new Set(await allEvents.nonExisting(events)); + const eventIds = getUserEventIdsFromContext(ctx); + if (eventIds.length > 0) { + const nonExisting = new Set(allEvents.nonExisting(eventIds)); text += 'Du hast folgende Veranstaltungen im Kalender:'; text += '\n'; - text += events - .map(o => { + text += eventIds + .map(eventId => { let line = '- '; - if (nonExisting.has(o)) { + if (nonExisting.has(eventId)) { line += '⚠️ '; } - line += format.escape(o); + line += format.escape(getEventName(eventId)); return line; }) + .sort((a, b) => a.localeCompare(b)) .join('\n'); if (nonExisting.size > 0) { @@ -59,14 +63,14 @@ bot.use(detailsMenu.bot); menu.interact('remove-old', { text: '🗑 Entferne nicht mehr Existierende', async hide(ctx) { - const nonExisting = await allEvents.nonExisting(Object.keys(ctx.userconfig.mine.events)); + const nonExisting = allEvents.nonExisting(getUserEventIdsFromContext(ctx)); return nonExisting.length === 0; }, async do(ctx) { - const nonExisting = new Set(await allEvents.nonExisting(Object.keys(ctx.userconfig.mine.events))); - for (const name of nonExisting) { + const nonExisting = allEvents.nonExisting(getUserEventIdsFromContext(ctx)); + for (const eventId of nonExisting) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete ctx.userconfig.mine.events[name]; + delete ctx.userconfig.mine.events[eventId]; } return true; @@ -80,8 +84,8 @@ menu.chooseIntoSubmenu('d', detailsMenu.menu, { choices(ctx) { const result: Record = {}; - for (const [name, details] of Object.entries(ctx.userconfig.mine.events)) { - let title = name + ' '; + for (const [eventId, details] of Object.entries(ctx.userconfig.mine.events)) { + let title = getEventName(eventId as EventId) + ' '; if (Object.keys(details.changes ?? {}).length > 0) { title += '✏️'; @@ -95,7 +99,7 @@ menu.chooseIntoSubmenu('d', detailsMenu.menu, { title += '🗒'; } - result[name.replaceAll('/', ';')] = title.trim(); + result[eventId] = title.trim(); } return result; diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index 7708490c..4692f4f3 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -7,22 +7,25 @@ import { generateChangeTextHeader, generateShortChangeText, } from '../lib/change-helper.ts'; -import type {Change, MyContext, NaiveDateTime} from '../lib/types.ts'; +import type { + Change, EventDetails, EventId, MyContext, NaiveDateTime, +} from '../lib/types.ts'; +import {getUserEventIdsFromContext} from '../lib/calendar-helper.js'; export const bot = new Composer(); function generateInlineQueryResultFromChange( - name: string, + eventId: EventId, date: NaiveDateTime, change: Change, from: User, ): InlineQueryResultArticle { - const id = `${name}#${date}#${from.id}`; + const id = `${eventId}#${date}#${from.id}`; return { description: generateChangeDescription(change), id, input_message_content: { - message_text: generateChangeText(name, date, change), + message_text: generateChangeText(eventId, date, change), parse_mode: format.parse_mode, }, reply_markup: { @@ -30,7 +33,7 @@ function generateInlineQueryResultFromChange( [{text: 'zu meinem Kalender hinzufügen', callback_data: 'c:a:' + id}], ], }, - title: generateShortChangeText(name, date), + title: generateShortChangeText(eventId, date), type: 'article', }; } @@ -51,15 +54,15 @@ bot.on('inline_query', async ctx => { const results: InlineQueryResultArticle[] = []; - for (const [event, details] of Object.entries(ctx.userconfig.mine.events)) { + for (const [eventId, details] of Object.entries(ctx.userconfig.mine.events) as Array<[EventId, EventDetails]>) { for (const [dateKey, change] of Object.entries(details.changes ?? {})) { const date = dateKey as NaiveDateTime; - const isMatched = regex.test(generateShortChangeText(event, date)); + const isMatched = regex.test(generateShortChangeText(eventId, date)); if (!isMatched) { continue; } - results.push(generateInlineQueryResultFromChange(event, date, change, ctx.from)); + results.push(generateInlineQueryResultFromChange(eventId, date, change, ctx.from)); } } @@ -74,31 +77,31 @@ bot.on('inline_query', async ctx => { }); type ChangeRelatedInfos = { - name: string; + eventId: EventId; date: NaiveDateTime; fromId: number; change: Change; }; async function getChangeFromContextMatch(ctx: MyContext): Promise { - const name = ctx.match![1]!; + const eventId = ctx.match![1]! as EventId; const date = ctx.match![2]! as NaiveDateTime; const fromId = Number(ctx.match![3]!); - if (!Object.keys(ctx.userconfig.mine.events).includes(name)) { + if (!getUserEventIdsFromContext(ctx).includes(eventId)) { await ctx.answerCallbackQuery('Du besuchst diese Veranstaltung garnicht. 🤔'); return undefined; } try { const fromconfig = await ctx.userconfig.loadConfig(fromId); - const searchedChange = fromconfig.events[name]?.changes?.[date]; + const searchedChange = fromconfig.events[eventId]?.changes?.[date]; if (!searchedChange) { throw new Error('User does not have this change'); } return { - name, + eventId, date, fromId, change: searchedChange, @@ -115,7 +118,7 @@ bot.callbackQuery(/^c:a:(.+)#(.+)#(.+)$/, async ctx => { return; } - const {name, date, fromId, change} = meta; + const {eventId, date, fromId, change} = meta; if (ctx.from?.id === Number(fromId)) { await ctx.answerCallbackQuery('Das ist deine eigene Änderung 😉'); @@ -123,7 +126,7 @@ bot.callbackQuery(/^c:a:(.+)#(.+)#(.+)$/, async ctx => { } // Prüfen ob man bereits eine Änderung mit dem Namen und dem Datum hat. - const currentChange = ctx.userconfig.mine.events[name]?.changes?.[date]; + const currentChange = ctx.userconfig.mine.events[eventId]?.changes?.[date]; if (currentChange) { const warning @@ -131,7 +134,7 @@ bot.callbackQuery(/^c:a:(.+)#(.+)#(.+)$/, async ctx => { await ctx.answerCallbackQuery(warning); let text = warning + '\n'; - text += generateChangeTextHeader(name, date); + text += generateChangeTextHeader(eventId, date); text += '\nDiese Veränderung ist bereits in deinem Kalender:'; text += '\n' + format.escape(generateChangeDescription(currentChange)); @@ -143,7 +146,7 @@ bot.callbackQuery(/^c:a:(.+)#(.+)#(.+)$/, async ctx => { [ { text: 'Überschreiben', - callback_data: `c:af:${name}#${date}#${fromId}`, + callback_data: `c:af:${eventId}#${date}#${fromId}`, }, {text: 'Abbrechen', callback_data: 'c:cancel'}, ], @@ -156,8 +159,8 @@ bot.callbackQuery(/^c:a:(.+)#(.+)#(.+)$/, async ctx => { return; } - ctx.userconfig.mine.events[name]!.changes ??= {}; - ctx.userconfig.mine.events[name]!.changes[date] = change; + ctx.userconfig.mine.events[eventId]!.changes ??= {}; + ctx.userconfig.mine.events[eventId]!.changes[date] = change; await ctx.answerCallbackQuery('Die Änderung wurde hinzugefügt'); }); @@ -173,8 +176,8 @@ bot.callbackQuery(/^c:af:(.+)#(.+)#(.+)$/, async ctx => { return; } - const {name, date, change} = meta; - ctx.userconfig.mine.events[name]!.changes ??= {}; - ctx.userconfig.mine.events[name]!.changes[date] = change; + const {eventId, date, change} = meta; + ctx.userconfig.mine.events[eventId]!.changes ??= {}; + ctx.userconfig.mine.events[eventId]!.changes[date] = change; return ctx.editMessageText('Die Änderung wurde hinzugefügt.'); }); From 3dfe1d54ad56417fd5c106ac26d1a0551045cf46 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:44:20 +0100 Subject: [PATCH 02/51] fixed module load hanging --- source/lib/all-events.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 94f3116f..1a30c085 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -19,7 +19,9 @@ async function watchForDirectoryChanges() { } } -await watchForDirectoryChanges(); +// We do not want to await this Promise, since it will never resolve and would cause the module to hang on load. +// eslint-disable-next-line unicorn/prefer-top-level-await +void watchForDirectoryChanges(); async function loadDirectory(): Promise> { const directoryString = await readFile(DIRECTORY_FILE); From a841a1bfa37de6c743032c4c2de1fb686310127d Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:19:00 +0100 Subject: [PATCH 03/51] handle non-existing directories better --- source/lib/all-events.ts | 10 ++++++++++ source/menu/events/add.ts | 28 ++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 1a30c085..3b9add4e 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -106,3 +106,13 @@ export function find( events: directory.events ?? {}, }; } + +export function directoryExists(path: string[]): boolean { + try { + resolvePath(path); + + return true; + } catch { + return false; + } +} diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index f4fce1e8..e3bae9cb 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -4,7 +4,9 @@ import { deleteMenuFromContext, getMenuOfPath, MenuTemplate, replyMenuToContext, } from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; -import {count as allEventsCount, find as allEventsFind, getEventName} from '../../lib/all-events.ts'; +import { + count as allEventsCount, directoryExists, find as allEventsFind, getEventName, +} from '../../lib/all-events.ts'; import {filterButtonText} from '../../lib/inline-menu-filter.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; @@ -131,9 +133,22 @@ menu.choose('a', { } if (key.startsWith('d')) { - const chosenSubDirectory = key.slice(1); - ctx.session.eventPath?.push(chosenSubDirectory); - delete ctx.session.eventDirectorySubDirectoryItems; + if (ctx.session.eventDirectorySubDirectoryItems !== undefined) { + const chosenSubDirectory = ctx.session.eventDirectorySubDirectoryItems[Number(key.slice(1))]; + delete ctx.session.eventDirectorySubDirectoryItems; + + if (chosenSubDirectory !== undefined) { + ctx.session.eventPath ??= []; + ctx.session.eventPath.push(chosenSubDirectory); + + if (directoryExists(ctx.session.eventPath)) { + return true; + } + } + } + + await ctx.answerCallbackQuery('Dieses Verzeichnis gibt es nicht mehr.'); + delete ctx.session.eventPath; return true; } @@ -165,6 +180,11 @@ menu.interact('back', { } ctx.session.eventPath?.pop(); + if (ctx.session.eventPath !== undefined && !directoryExists(ctx.session.eventPath)) { + delete ctx.session.eventPath; + delete ctx.session.eventDirectorySubDirectoryItems; + } + return true; }, }); From 6259f1d4bb75c95516aa1fe6e3f2c11c78bfe8ec Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:38:52 +0100 Subject: [PATCH 04/51] added log messages when directory gets (re)loaded --- source/lib/all-events.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 3b9add4e..984a2a80 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,7 +1,5 @@ import {readFile, watch} from 'node:fs/promises'; -import type { - EventDirectory, EventId, Events, -} from './types.ts'; +import type {EventDirectory, EventId, Events} from './types.ts'; const DIRECTORY_FILE = 'eventfiles/directory.json'; @@ -11,8 +9,9 @@ const namesOfEvents: Record = await generateMapping(); async function watchForDirectoryChanges() { const watcher = watch(DIRECTORY_FILE); for await (const event of watcher) { - console.log(event); if (event.eventType === 'change') { + console.log(new Date(), 'Detected file change. Reloading...'); + await loadDirectory(); await generateMapping(); } @@ -24,6 +23,8 @@ async function watchForDirectoryChanges() { void watchForDirectoryChanges(); async function loadDirectory(): Promise> { + console.log(new Date(), 'Loading directory'); + const directoryString = await readFile(DIRECTORY_FILE); const directory = JSON.parse(directoryString.toString()) as Partial; return directory; From 7c7665322bf178f60ef3f2d5e464dd6ebae6d65b Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:34:18 +0100 Subject: [PATCH 05/51] use generic function to extract typed keys --- source/lib/calendar-helper.ts | 6 +----- source/lib/javascript-helper.ts | 4 ++++ source/menu/events/add.ts | 4 ++-- source/menu/events/index.ts | 8 ++++---- source/parts/changes-inline.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 source/lib/javascript-helper.ts diff --git a/source/lib/calendar-helper.ts b/source/lib/calendar-helper.ts index 8fb6a436..c85e1080 100644 --- a/source/lib/calendar-helper.ts +++ b/source/lib/calendar-helper.ts @@ -1,4 +1,4 @@ -import type {EventId, MyContext, Userconfig} from './types.ts'; +import type {MyContext, Userconfig} from './types.ts'; export function getUrl(id: number, userconfig: Userconfig): string { let filename = `${id}`; @@ -14,7 +14,3 @@ export function getUrl(id: number, userconfig: Userconfig): string { export function getUrlFromContext(ctx: MyContext): string { return getUrl(ctx.from!.id, ctx.userconfig.mine); } - -export function getUserEventIdsFromContext(ctx: MyContext): EventId[] { - return Object.keys(ctx.userconfig.mine.events) as EventId[]; -} diff --git a/source/lib/javascript-helper.ts b/source/lib/javascript-helper.ts new file mode 100644 index 00000000..ab090fe6 --- /dev/null +++ b/source/lib/javascript-helper.ts @@ -0,0 +1,4 @@ + +export function typedKeys>(obj: T): Array { + return Object.keys(obj) as Array; +} diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index e3bae9cb..74d464e8 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -10,7 +10,7 @@ import { import {filterButtonText} from '../../lib/inline-menu-filter.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; -import {getUserEventIdsFromContext} from '../../lib/calendar-helper.js'; +import {typedKeys} from '../../lib/javascript-helper.js'; const MAX_RESULT_ROWS = 10; const RESULT_COLUMNS = 1; @@ -115,7 +115,7 @@ menu.choose('a', { if (key.startsWith('e')) { const eventId = key.slice(1) as EventId; const eventName = getEventName(eventId); - const isAlreadyInCalendar = getUserEventIdsFromContext(ctx).includes(eventId); + const isAlreadyInCalendar = typedKeys(ctx.userconfig.mine.events).includes(eventId); if (eventName === undefined) { await ctx.answerCallbackQuery(`Event mit Id ${eventId} existiert nicht!`); diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index c24d1b3c..9cd48a12 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -5,7 +5,7 @@ import * as allEvents from '../../lib/all-events.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; import type {EventId, MyContext} from '../../lib/types.ts'; import {getEventName} from '../../lib/all-events.ts'; -import {getUserEventIdsFromContext} from '../../lib/calendar-helper.js'; +import {typedKeys} from '../../lib/javascript-helper.js'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; @@ -16,7 +16,7 @@ export const menu = new MenuTemplate(async ctx => { let text = format.bold('Veranstaltungen'); text += '\n\n'; - const eventIds = getUserEventIdsFromContext(ctx); + const eventIds = typedKeys(ctx.userconfig.mine.events); if (eventIds.length > 0) { const nonExisting = new Set(allEvents.nonExisting(eventIds)); text += 'Du hast folgende Veranstaltungen im Kalender:'; @@ -63,11 +63,11 @@ bot.use(detailsMenu.bot); menu.interact('remove-old', { text: '🗑 Entferne nicht mehr Existierende', async hide(ctx) { - const nonExisting = allEvents.nonExisting(getUserEventIdsFromContext(ctx)); + const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); return nonExisting.length === 0; }, async do(ctx) { - const nonExisting = allEvents.nonExisting(getUserEventIdsFromContext(ctx)); + const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); for (const eventId of nonExisting) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete ctx.userconfig.mine.events[eventId]; diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index 4692f4f3..9b8e87d5 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -10,7 +10,7 @@ import { import type { Change, EventDetails, EventId, MyContext, NaiveDateTime, } from '../lib/types.ts'; -import {getUserEventIdsFromContext} from '../lib/calendar-helper.js'; +import {typedKeys} from '../lib/javascript-helper.js'; export const bot = new Composer(); @@ -88,7 +88,7 @@ async function getChangeFromContextMatch(ctx: MyContext): Promise Date: Tue, 28 Oct 2025 19:48:26 +0100 Subject: [PATCH 06/51] improved readability of string interpolation --- source/menu/events/add.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 74d464e8..31be81a2 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -28,9 +28,13 @@ export const menu = new MenuTemplate(async ctx => { const filteredEvents = findEvents(ctx); const filter = ctx.session.eventfilter; - text += filter === undefined - ? `Ich habe ${total} Veranstaltungen. Nutze den Filter um die Auswahl einzugrenzen.` - : `Mit deinem Filter konnte ich ${Object.keys(filteredEvents.events).length} passende Veranstaltungen und ${Object.keys(filteredEvents.subDirectories).length} Ordner finden.`; + if (filter === undefined) { + text += `Ich habe ${total} Veranstaltungen. Nutze den Filter um die Auswahl einzugrenzen.`; + } else { + const eventCount = Object.keys(filteredEvents.events).length; + + text += `Mit deinem Filter konnte ich ${eventCount} passende Veranstaltungen finden.`; + } } catch (error) { const errorText = error instanceof Error ? error.message : String(error); text += 'Filter Error: '; From bc4f2fa31f79894a8886303c27945352e8f02f71 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:56:26 +0100 Subject: [PATCH 07/51] added typedEntries function --- source/lib/all-events.ts | 5 +++-- source/lib/javascript-helper.ts | 8 ++++++++ source/menu/events/index.ts | 8 ++++---- source/parts/changes-inline.ts | 9 ++++----- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 984a2a80..9505c872 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,5 +1,6 @@ import {readFile, watch} from 'node:fs/promises'; import type {EventDirectory, EventId, Events} from './types.ts'; +import {typedEntries} from './javascript-helper.js'; const DIRECTORY_FILE = 'eventfiles/directory.json'; @@ -81,9 +82,9 @@ export function find( const accumulator: Events = {}; function collect(directory: Partial) { - for (const [eventId, name] of Object.entries(directory.events ?? {})) { + for (const [eventId, name] of typedEntries(directory.events ?? {})) { if (regex.test(name)) { - accumulator[eventId as EventId] = name; + accumulator[eventId] = name; } } diff --git a/source/lib/javascript-helper.ts b/source/lib/javascript-helper.ts index ab090fe6..e940e7d0 100644 --- a/source/lib/javascript-helper.ts +++ b/source/lib/javascript-helper.ts @@ -2,3 +2,11 @@ export function typedKeys>(obj: T): Array { return Object.keys(obj) as Array; } + +export function typedEntries(record: Readonly>>): Array<[K, V]> { + if (!record) { + return []; + } + + return (Object.entries(record) as unknown[]) as Array<[K, V]>; +} diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index 9cd48a12..2b592d6d 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -3,9 +3,9 @@ import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import * as allEvents from '../../lib/all-events.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; -import type {EventId, MyContext} from '../../lib/types.ts'; +import type {MyContext} from '../../lib/types.ts'; import {getEventName} from '../../lib/all-events.ts'; -import {typedKeys} from '../../lib/javascript-helper.js'; +import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; @@ -84,8 +84,8 @@ menu.chooseIntoSubmenu('d', detailsMenu.menu, { choices(ctx) { const result: Record = {}; - for (const [eventId, details] of Object.entries(ctx.userconfig.mine.events)) { - let title = getEventName(eventId as EventId) + ' '; + for (const [eventId, details] of typedEntries(ctx.userconfig.mine.events)) { + let title = getEventName(eventId) + ' '; if (Object.keys(details.changes ?? {}).length > 0) { title += '✏️'; diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index 9b8e87d5..61209ee1 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -8,9 +8,9 @@ import { generateShortChangeText, } from '../lib/change-helper.ts'; import type { - Change, EventDetails, EventId, MyContext, NaiveDateTime, + Change, EventId, MyContext, NaiveDateTime, } from '../lib/types.ts'; -import {typedKeys} from '../lib/javascript-helper.js'; +import {typedEntries, typedKeys} from '../lib/javascript-helper.js'; export const bot = new Composer(); @@ -54,9 +54,8 @@ bot.on('inline_query', async ctx => { const results: InlineQueryResultArticle[] = []; - for (const [eventId, details] of Object.entries(ctx.userconfig.mine.events) as Array<[EventId, EventDetails]>) { - for (const [dateKey, change] of Object.entries(details.changes ?? {})) { - const date = dateKey as NaiveDateTime; + for (const [eventId, details] of typedEntries(ctx.userconfig.mine.events)) { + for (const [date, change] of typedEntries(details.changes ?? {})) { const isMatched = regex.test(generateShortChangeText(eventId, date)); if (!isMatched) { continue; From 04539f12e8840fa7ffd0e761de48609d4edf8ae9 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:02:03 +0100 Subject: [PATCH 08/51] removed Events type --- source/lib/all-events.ts | 4 ++-- source/lib/types.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 9505c872..2bb78cc6 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,5 +1,5 @@ import {readFile, watch} from 'node:fs/promises'; -import type {EventDirectory, EventId, Events} from './types.ts'; +import type {EventDirectory, EventId} from './types.ts'; import {typedEntries} from './javascript-helper.js'; const DIRECTORY_FILE = 'eventfiles/directory.json'; @@ -79,7 +79,7 @@ export function find( ): Readonly { if (pattern !== undefined) { const regex = new RegExp(pattern, 'i'); - const accumulator: Events = {}; + const accumulator: Record = {}; function collect(directory: Partial) { for (const [eventId, name] of typedEntries(directory.events ?? {})) { diff --git a/source/lib/types.ts b/source/lib/types.ts index ac664422..cb3c46fb 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -86,11 +86,9 @@ export type MensaSettings = MealWishes & { export type EventId = `${number}_${number | string}`; -export type Events = Record; - export type EventDirectory = { readonly subDirectories: Record>; - readonly events: Events; + readonly events: Record; }; export type EventEntry = { From 43bb6839f5b3f5f818fa459ffd1a764cc67062cd Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:11:16 +0100 Subject: [PATCH 09/51] use fewer empty lines Co-authored-by: EdJoPaTo Signed-off-by: Malex14 <39774812+Malex14@users.noreply.github.com> --- source/menu/events/add.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 31be81a2..2cedfead 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -172,14 +172,12 @@ menu.interact('back', { async do(ctx) { if (ctx.session.eventfilter !== undefined) { delete ctx.session.eventfilter; - return true; } if (ctx.session.eventPath?.length === 0) { delete ctx.session.eventPath; delete ctx.session.eventDirectorySubDirectoryItems; - return '..'; } From 369aaec903f8559633ea5b6870e405e3598d4b1d Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:14:04 +0100 Subject: [PATCH 10/51] Make records in EventDirectory readonly Co-authored-by: EdJoPaTo Signed-off-by: Malex14 <39774812+Malex14@users.noreply.github.com> --- source/lib/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index cb3c46fb..2fd83ba9 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -87,8 +87,8 @@ export type MensaSettings = MealWishes & { export type EventId = `${number}_${number | string}`; export type EventDirectory = { - readonly subDirectories: Record>; - readonly events: Record; + readonly subDirectories: Readonly>>; + readonly events: Readonly>; }; export type EventEntry = { From 897c71647da048bcbbeb08f058f818e284286d7c Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:25:15 +0100 Subject: [PATCH 11/51] unified types of js helpers --- source/lib/javascript-helper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/lib/javascript-helper.ts b/source/lib/javascript-helper.ts index e940e7d0..03e0b551 100644 --- a/source/lib/javascript-helper.ts +++ b/source/lib/javascript-helper.ts @@ -1,6 +1,6 @@ -export function typedKeys>(obj: T): Array { - return Object.keys(obj) as Array; +export function typedKeys(record: Readonly>>): K[] { + return Object.keys(record) as K[]; } export function typedEntries(record: Readonly>>): Array<[K, V]> { From c0fd0fb22646b3a5e80f4ba44df19202ef7bd938 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:29:55 +0100 Subject: [PATCH 12/51] use even fewer blank lines --- source/lib/all-events.ts | 6 ------ source/menu/events/add.ts | 2 -- 2 files changed, 8 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 2bb78cc6..46d1efde 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -12,7 +12,6 @@ async function watchForDirectoryChanges() { for await (const event of watcher) { if (event.eventType === 'change') { console.log(new Date(), 'Detected file change. Reloading...'); - await loadDirectory(); await generateMapping(); } @@ -25,7 +24,6 @@ void watchForDirectoryChanges(); async function loadDirectory(): Promise> { console.log(new Date(), 'Loading directory'); - const directoryString = await readFile(DIRECTORY_FILE); const directory = JSON.parse(directoryString.toString()) as Partial; return directory; @@ -43,7 +41,6 @@ async function generateMapping(): Promise> { } collect(directory); - return namesOfEvents; } @@ -94,7 +91,6 @@ export function find( } collect(resolvePath(startAt)); - return { subDirectories: {}, events: Object.fromEntries(Object.entries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), @@ -102,7 +98,6 @@ export function find( } const directory = resolvePath(startAt); - return { subDirectories: directory.subDirectories ?? {}, events: directory.events ?? {}, @@ -112,7 +107,6 @@ export function find( export function directoryExists(path: string[]): boolean { try { resolvePath(path); - return true; } catch { return false; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 2cedfead..bbd9ee13 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -153,12 +153,10 @@ menu.choose('a', { await ctx.answerCallbackQuery('Dieses Verzeichnis gibt es nicht mehr.'); delete ctx.session.eventPath; - return true; } await ctx.answerCallbackQuery('Dieses Verzeichnis ist leer.'); - return false; }, getCurrentPage: ctx => ctx.session.page, From 851bab986571a96f04c7e240a01089abe2dd84c7 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:57:36 +0100 Subject: [PATCH 13/51] improved comment explaining session fields --- source/lib/types.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index 2fd83ba9..720ec869 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -19,12 +19,25 @@ export type MyContext = & ContextFlavour; export type Session = { - adminBroadcast?: number; // Message ID - adminuserquicklook?: number; // User ID + /** Message ID */ + adminBroadcast?: number; + /** User ID */ + adminuserquicklook?: number; adminuserquicklookfilter?: string; eventfilter?: string; - eventPath?: string[]; // Path to the selected subdirectory - eventDirectorySubDirectoryItems?: string[]; // Subdirectory item keys of the directory selected by eventPath + /** Path to the currently selected subdirectory on the add events screen. + * + * The entries of this array are the keys of the (sub)directories. + * */ + eventPath?: string[]; + /** Subdirectory item keys of the directory selected by eventPath + * + * This array stores the keys of subdirectories in the currently selected directory. + * On the event-adding screen these keys are used to navigate into subdirectories. + * Telegram callback data restricts allowed characters and length, so we store the + * keys here and use the array index as the callback payload. + */ + eventDirectorySubDirectoryItems?: string[]; generateChangeEventId?: EventId; generateChangeDate?: NaiveDateTime; generateChange?: Partial; From 9a5d2103c98ff139fa7cd686ee4c75fc68c3ff44 Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:16:38 +0100 Subject: [PATCH 14/51] Specify encoding Co-authored-by: EdJoPaTo Signed-off-by: Malex14 <39774812+Malex14@users.noreply.github.com> --- source/lib/all-events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 46d1efde..2a5dcbbf 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -24,8 +24,8 @@ void watchForDirectoryChanges(); async function loadDirectory(): Promise> { console.log(new Date(), 'Loading directory'); - const directoryString = await readFile(DIRECTORY_FILE); - const directory = JSON.parse(directoryString.toString()) as Partial; + const directoryString = await readFile(DIRECTORY_FILE, 'utf8'); + const directory = JSON.parse(directoryString) as Partial; return directory; } From 7fbaefefb01cd9a14e305867ad586ab5b61cd5fb Mon Sep 17 00:00:00 2001 From: Malex14 <39774812+Malex14@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:19:33 +0100 Subject: [PATCH 15/51] Removed empty line Signed-off-by: Malex14 <39774812+Malex14@users.noreply.github.com> --- source/lib/javascript-helper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/source/lib/javascript-helper.ts b/source/lib/javascript-helper.ts index 03e0b551..be4a03df 100644 --- a/source/lib/javascript-helper.ts +++ b/source/lib/javascript-helper.ts @@ -1,4 +1,3 @@ - export function typedKeys(record: Readonly>>): K[] { return Object.keys(record) as K[]; } From ce6d364010c7f073c7ab1948d57812c9d032c6e7 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:33:56 +0100 Subject: [PATCH 16/51] cleanedup comment --- source/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index 720ec869..3122468a 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -28,7 +28,7 @@ export type Session = { /** Path to the currently selected subdirectory on the add events screen. * * The entries of this array are the keys of the (sub)directories. - * */ + */ eventPath?: string[]; /** Subdirectory item keys of the directory selected by eventPath * From 920ae7b925dd4ba04e4705446c21b1175f3c4577 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:36:35 +0100 Subject: [PATCH 17/51] comment EventDirectory fields --- source/lib/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/lib/types.ts b/source/lib/types.ts index 3122468a..2aba37fc 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -100,7 +100,9 @@ export type MensaSettings = MealWishes & { export type EventId = `${number}_${number | string}`; export type EventDirectory = { + /** Maps the directory name to its content */ readonly subDirectories: Readonly>>; + /** Maps `EventId` to the human-readable name */ readonly events: Readonly>; }; From b4be2075ab945b87900296560b77f2ae7276fab6 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:42:00 +0100 Subject: [PATCH 18/51] made empty dir logic clearer --- source/menu/events/add.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index bbd9ee13..33680f87 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -96,7 +96,8 @@ menu.choose('a', { ctx.session.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); const subDirectoryItems = Object.entries(filteredEvents.subDirectories) .map(([name, directory], i) => - directory.subDirectories !== undefined || directory.events !== undefined + Object.keys(directory.subDirectories ?? {}).length > 0 + || Object.keys(directory.events ?? {}).length > 0 ? ['d' + i, '🗂️ ' + name] : ['x' + i, '🚫 ' + name]); From 532f45fbd05b3b4a3a7d9f43573e684aeb9471ba Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:58:12 +0100 Subject: [PATCH 19/51] cleanedup session fields --- source/lib/types.ts | 28 +++++++++++++++------------- source/menu/events/add.ts | 37 ++++++++++++++++++++----------------- source/menu/events/index.ts | 2 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index 2aba37fc..2e54990b 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -25,19 +25,21 @@ export type Session = { adminuserquicklook?: number; adminuserquicklookfilter?: string; eventfilter?: string; - /** Path to the currently selected subdirectory on the add events screen. - * - * The entries of this array are the keys of the (sub)directories. - */ - eventPath?: string[]; - /** Subdirectory item keys of the directory selected by eventPath - * - * This array stores the keys of subdirectories in the currently selected directory. - * On the event-adding screen these keys are used to navigate into subdirectories. - * Telegram callback data restricts allowed characters and length, so we store the - * keys here and use the array index as the callback payload. - */ - eventDirectorySubDirectoryItems?: string[]; + eventAdd?: { + /** Path to the currently selected subdirectory on the add events screen. + * + * The entries of this array are the keys of the (sub)directories. + */ + eventPath?: string[]; + /** Subdirectory item keys of the directory selected by eventPath + * + * This array stores the keys of subdirectories in the currently selected directory. + * On the event-adding screen these keys are used to navigate into subdirectories. + * Telegram callback data restricts allowed characters and length, so we store the + * keys here and use the array index as the callback payload. + */ + eventDirectorySubDirectoryItems?: string[]; + }; generateChangeEventId?: EventId; generateChangeDate?: NaiveDateTime; generateChange?: Partial; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 33680f87..709aa4c5 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -18,7 +18,8 @@ const RESULT_COLUMNS = 1; export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { const total = allEventsCount(); - ctx.session.eventPath ??= []; + ctx.session.eventAdd ??= {}; + ctx.session.eventAdd.eventPath ??= []; let text = format.bold('Veranstaltungen'); text += '\nWelche Events möchtest du hinzufügen?'; @@ -46,7 +47,8 @@ export const menu = new MenuTemplate(async ctx => { function findEvents(ctx: MyContext): Readonly { const filter = ctx.session.eventfilter; - return allEventsFind(filter, ctx.session.eventPath); + ctx.session.eventAdd ??= {}; + return allEventsFind(filter, ctx.session.eventAdd.eventPath); } const question = new StatelessQuestion( @@ -93,7 +95,8 @@ menu.choose('a', { const filteredEvents = findEvents(ctx); const alreadySelected = Object.keys(ctx.userconfig.mine.events); - ctx.session.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); + ctx.session.eventAdd ??= {}; + ctx.session.eventAdd.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); const subDirectoryItems = Object.entries(filteredEvents.subDirectories) .map(([name, directory], i) => Object.keys(directory.subDirectories ?? {}).length > 0 @@ -138,22 +141,23 @@ menu.choose('a', { } if (key.startsWith('d')) { - if (ctx.session.eventDirectorySubDirectoryItems !== undefined) { - const chosenSubDirectory = ctx.session.eventDirectorySubDirectoryItems[Number(key.slice(1))]; - delete ctx.session.eventDirectorySubDirectoryItems; + ctx.session.eventAdd ??= {}; + if (ctx.session.eventAdd.eventDirectorySubDirectoryItems !== undefined) { + const chosenSubDirectory = ctx.session.eventAdd.eventDirectorySubDirectoryItems[Number(key.slice(1))]; + delete ctx.session.eventAdd.eventDirectorySubDirectoryItems; if (chosenSubDirectory !== undefined) { - ctx.session.eventPath ??= []; - ctx.session.eventPath.push(chosenSubDirectory); + ctx.session.eventAdd.eventPath ??= []; + ctx.session.eventAdd.eventPath.push(chosenSubDirectory); - if (directoryExists(ctx.session.eventPath)) { + if (directoryExists(ctx.session.eventAdd.eventPath)) { return true; } } } await ctx.answerCallbackQuery('Dieses Verzeichnis gibt es nicht mehr.'); - delete ctx.session.eventPath; + delete ctx.session.eventAdd; return true; } @@ -174,16 +178,15 @@ menu.interact('back', { return true; } - if (ctx.session.eventPath?.length === 0) { - delete ctx.session.eventPath; - delete ctx.session.eventDirectorySubDirectoryItems; + if (ctx.session.eventAdd?.eventPath === undefined + || ctx.session.eventAdd?.eventPath.length === 0) { + delete ctx.session.eventAdd; return '..'; } - ctx.session.eventPath?.pop(); - if (ctx.session.eventPath !== undefined && !directoryExists(ctx.session.eventPath)) { - delete ctx.session.eventPath; - delete ctx.session.eventDirectorySubDirectoryItems; + ctx.session.eventAdd.eventPath.pop(); + if (!directoryExists(ctx.session.eventAdd.eventPath)) { + delete ctx.session.eventAdd; } return true; diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index 2b592d6d..c73631a4 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -11,7 +11,7 @@ import * as detailsMenu from './details.ts'; export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { - delete ctx.session.eventPath; + delete ctx.session.eventAdd; let text = format.bold('Veranstaltungen'); text += '\n\n'; From 75ebc27a858e52c3dab74f6448ba440a32dd4665 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:11:03 +0100 Subject: [PATCH 20/51] changed resolvePath to getSubdirectory and simplified logic --- source/lib/all-events.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 2a5dcbbf..a7108e6e 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -44,15 +44,16 @@ async function generateMapping(): Promise> { return namesOfEvents; } -function resolvePath(path: string[]): Partial { +function getSubdirectory(path: string[]): Partial | undefined { let resolvedDirectory = directory; for (const part of path) { - if (resolvedDirectory.subDirectories === undefined || !(part in resolvedDirectory.subDirectories)) { - throw new Error('Ungültiger Pfad'); + const subDirectory = resolvedDirectory.subDirectories?.[part]; + if (subDirectory === undefined) { + return undefined; } - resolvedDirectory = resolvedDirectory.subDirectories[part]!; + resolvedDirectory = subDirectory; } return resolvedDirectory; @@ -90,14 +91,14 @@ export function find( } } - collect(resolvePath(startAt)); + collect(getSubdirectory(startAt) ?? {}); return { subDirectories: {}, events: Object.fromEntries(Object.entries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), }; } - const directory = resolvePath(startAt); + const directory = getSubdirectory(startAt) ?? {}; return { subDirectories: directory.subDirectories ?? {}, events: directory.events ?? {}, @@ -105,10 +106,5 @@ export function find( } export function directoryExists(path: string[]): boolean { - try { - resolvePath(path); - return true; - } catch { - return false; - } + return getSubdirectory(path) !== undefined; } From 6d40243f30748cbaf1bfe0113dfe8aefe5e46a32 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:41:38 +0100 Subject: [PATCH 21/51] made eventPath in session non-optional --- source/lib/types.ts | 2 +- source/menu/events/add.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index 2e54990b..da273008 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -30,7 +30,7 @@ export type Session = { * * The entries of this array are the keys of the (sub)directories. */ - eventPath?: string[]; + eventPath: string[]; /** Subdirectory item keys of the directory selected by eventPath * * This array stores the keys of subdirectories in the currently selected directory. diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 709aa4c5..edf98eef 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -18,8 +18,7 @@ const RESULT_COLUMNS = 1; export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { const total = allEventsCount(); - ctx.session.eventAdd ??= {}; - ctx.session.eventAdd.eventPath ??= []; + ctx.session.eventAdd ??= {eventPath: []}; let text = format.bold('Veranstaltungen'); text += '\nWelche Events möchtest du hinzufügen?'; @@ -47,8 +46,7 @@ export const menu = new MenuTemplate(async ctx => { function findEvents(ctx: MyContext): Readonly { const filter = ctx.session.eventfilter; - ctx.session.eventAdd ??= {}; - return allEventsFind(filter, ctx.session.eventAdd.eventPath); + return allEventsFind(filter, ctx.session.eventAdd?.eventPath); } const question = new StatelessQuestion( @@ -92,10 +90,10 @@ menu.choose('a', { columns: RESULT_COLUMNS, async choices(ctx) { try { + ctx.session.eventAdd ??= {eventPath: []}; const filteredEvents = findEvents(ctx); const alreadySelected = Object.keys(ctx.userconfig.mine.events); - ctx.session.eventAdd ??= {}; ctx.session.eventAdd.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); const subDirectoryItems = Object.entries(filteredEvents.subDirectories) .map(([name, directory], i) => @@ -141,13 +139,16 @@ menu.choose('a', { } if (key.startsWith('d')) { - ctx.session.eventAdd ??= {}; + if (ctx.session.eventAdd === undefined) { + await ctx.answerCallbackQuery('Interner Zustand ungültig.'); + return true; + } + if (ctx.session.eventAdd.eventDirectorySubDirectoryItems !== undefined) { const chosenSubDirectory = ctx.session.eventAdd.eventDirectorySubDirectoryItems[Number(key.slice(1))]; delete ctx.session.eventAdd.eventDirectorySubDirectoryItems; if (chosenSubDirectory !== undefined) { - ctx.session.eventAdd.eventPath ??= []; ctx.session.eventAdd.eventPath.push(chosenSubDirectory); if (directoryExists(ctx.session.eventAdd.eventPath)) { From 920017521ef78bd681ed5c4e54951207d4ee8ba4 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:09:21 +0100 Subject: [PATCH 22/51] added git-support for eventfiles --- source/lib/git-helper.ts | 26 ++++++++++++++++++++++++++ source/lib/mensa-git.ts | 13 ------------- source/lib/mensa-meals.ts | 5 +++-- source/menu/events/index.ts | 4 ++++ source/menu/mensa/index.ts | 7 +++---- 5 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 source/lib/git-helper.ts delete mode 100644 source/lib/mensa-git.ts diff --git a/source/lib/git-helper.ts b/source/lib/git-helper.ts new file mode 100644 index 00000000..3002bbb2 --- /dev/null +++ b/source/lib/git-helper.ts @@ -0,0 +1,26 @@ +import {exec} from 'node:child_process'; +import {existsSync} from 'node:fs'; +import {promisify} from 'node:util'; + +const MENSA_REPO = 'https://github.com/HAWHHCalendarBot/mensa-data.git'; +export const MENSA_DIR = 'mensa-data'; +const EVENT_FILES_REPO = 'https://github.com/HAWHHCalendarBot/eventfiles.git'; +export const EVENT_FILES_DIR = 'eventfiles'; + +const run = promisify(exec); + +async function pull(repoUrl: string, repoDir: string): Promise { + try { + await (existsSync(`${repoDir}/.git`) + ? run(`git -C ${repoDir} pull`) + : run(`git clone -q --depth 1 ${repoUrl} ${repoDir}`)); + } catch {} +} + +export async function pullMensaData(): Promise { + await pull(MENSA_REPO, MENSA_DIR); +} + +export async function pullEventFiles(): Promise { + await pull(EVENT_FILES_REPO, EVENT_FILES_DIR); +} diff --git a/source/lib/mensa-git.ts b/source/lib/mensa-git.ts deleted file mode 100644 index 4992066a..00000000 --- a/source/lib/mensa-git.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {exec} from 'node:child_process'; -import {existsSync} from 'node:fs'; -import {promisify} from 'node:util'; - -const run = promisify(exec); - -export async function pull(): Promise { - try { - await (existsSync('mensa-data/.git') - ? run('git -C mensa-data pull') - : run('git clone -q --depth 1 https://github.com/HAWHHCalendarBot/mensa-data.git mensa-data')); - } catch {} -} diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index c40c3e42..86d7e23b 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -1,8 +1,9 @@ import {readdir, readFile} from 'node:fs/promises'; import type {Meal} from './meal.ts'; +import {MENSA_DIR} from './git-helper.js'; export async function getCanteenList(): Promise { - const found = await readdir('mensa-data', {withFileTypes: true}); + const found = await readdir(MENSA_DIR, {withFileTypes: true}); const dirs = found .filter(o => o.isDirectory()) .map(o => o.name) @@ -22,7 +23,7 @@ function getFilename( }); const m = month.toLocaleString(undefined, {minimumIntegerDigits: 2}); const d = day.toLocaleString(undefined, {minimumIntegerDigits: 2}); - return `mensa-data/${mensa}/${y}/${m}/${d}.json`; + return `${MENSA_DIR}/${mensa}/${y}/${m}/${d}.json`; } export async function getMealsOfDay( diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index c73631a4..089ff816 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -6,9 +6,13 @@ import {backMainButtons} from '../../lib/inline-menu.ts'; import type {MyContext} from '../../lib/types.ts'; import {getEventName} from '../../lib/all-events.ts'; import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; +import * as gitHelper from '../../lib/git-helper.js'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; +setInterval(async () => gitHelper.pullEventFiles(), 1000 * 60 * 30); // Every 30 minutes +void gitHelper.pullEventFiles(); + export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { delete ctx.session.eventAdd; diff --git a/source/menu/mensa/index.ts b/source/menu/mensa/index.ts index 77018b74..37f696aa 100644 --- a/source/menu/mensa/index.ts +++ b/source/menu/mensa/index.ts @@ -1,5 +1,5 @@ import {MenuTemplate} from 'grammy-inline-menu'; -import * as mensaGit from '../../lib/mensa-git.ts'; +import * as gitHelper from '../../lib/git-helper.ts'; import {generateMealText} from '../../lib/mensa-helper.ts'; import {getMealsOfDay} from '../../lib/mensa-meals.ts'; import type {MyContext} from '../../lib/types.ts'; @@ -16,9 +16,8 @@ const WEEKDAYS = [ ] as const; const DAY_IN_MS = 1000 * 60 * 60 * 24; -setInterval(async () => mensaGit.pull(), 1000 * 60 * 30); // Every 30 minutes -// eslint-disable-next-line @typescript-eslint/no-floating-promises -mensaGit.pull(); +setInterval(async () => gitHelper.pullMensaData(), 1000 * 60 * 30); // Every 30 minutes +void gitHelper.pullMensaData(); function getYearMonthDay(date: Readonly): Readonly<{year: number; month: number; day: number}> { const year = date.getFullYear(); From e0598721b7d5c59d5e9c8acb7990cc94cce604ac Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:10:01 +0100 Subject: [PATCH 23/51] fixed directory updating --- source/lib/all-events.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index a7108e6e..7cb61ea3 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,19 +1,20 @@ import {readFile, watch} from 'node:fs/promises'; import type {EventDirectory, EventId} from './types.ts'; import {typedEntries} from './javascript-helper.js'; +import {EVENT_FILES_DIR} from './git-helper.js'; -const DIRECTORY_FILE = 'eventfiles/directory.json'; +const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; -const directory = await loadDirectory(); -const namesOfEvents: Record = await generateMapping(); +let directory = await loadDirectory(); +let namesOfEvents: Record = await generateMapping(); async function watchForDirectoryChanges() { const watcher = watch(DIRECTORY_FILE); for await (const event of watcher) { if (event.eventType === 'change') { console.log(new Date(), 'Detected file change. Reloading...'); - await loadDirectory(); - await generateMapping(); + directory = await loadDirectory(); + namesOfEvents = await generateMapping(); } } } From 10f0a230cb3ff85deaf6a58133a90b4021fca903 Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:24:03 +0100 Subject: [PATCH 24/51] refactored filter button text logic --- source/lib/inline-menu-filter.ts | 13 ------------- source/menu/admin/user-quicklook.ts | 10 +++++----- source/menu/events/add.ts | 5 +++-- 3 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 source/lib/inline-menu-filter.ts diff --git a/source/lib/inline-menu-filter.ts b/source/lib/inline-menu-filter.ts deleted file mode 100644 index de982fb7..00000000 --- a/source/lib/inline-menu-filter.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const DEFAULT_FILTER = '.+'; - -export function filterButtonText(getCurrentFilterFunction: (ctx: T) => string | undefined): (ctx: T) => string { - return ctx => { - let text = '🔎 Ab hier filtern'; - const currentFilter = getCurrentFilterFunction(ctx); - if (currentFilter && currentFilter !== '.+') { - text += ': ' + currentFilter; - } - - return text; - }; -} diff --git a/source/menu/admin/user-quicklook.ts b/source/menu/admin/user-quicklook.ts index 1832b080..20843fb8 100644 --- a/source/menu/admin/user-quicklook.ts +++ b/source/menu/admin/user-quicklook.ts @@ -9,13 +9,11 @@ import { import type {User} from 'grammy/types'; import {html as format} from 'telegram-format'; import {getUrl} from '../../lib/calendar-helper.ts'; -import { - DEFAULT_FILTER, - filterButtonText, -} from '../../lib/inline-menu-filter.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; import type {MyContext} from '../../lib/types.ts'; +export const DEFAULT_FILTER = '.+'; + function nameOfUser({first_name, last_name, username}: User): string { let name = first_name; if (last_name) { @@ -70,7 +68,9 @@ const question = new StatelessQuestion( bot.use(question.middleware()); menu.interact('filter', { - text: filterButtonText(ctx => ctx.session.adminuserquicklookfilter), + text: ctx => ctx.session.adminuserquicklookfilter && ctx.session.adminuserquicklookfilter !== '.+' + ? `🔎 Filter: ${ctx.session.adminuserquicklookfilter}` + : '🔎 Filtern', async do(ctx, path) { await question.replyWithHTML( ctx, diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index edf98eef..c6d8338e 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -7,7 +7,6 @@ import {html as format} from 'telegram-format'; import { count as allEventsCount, directoryExists, find as allEventsFind, getEventName, } from '../../lib/all-events.ts'; -import {filterButtonText} from '../../lib/inline-menu-filter.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; import {typedKeys} from '../../lib/javascript-helper.js'; @@ -63,7 +62,9 @@ const question = new StatelessQuestion( bot.use(question.middleware()); menu.interact('filter', { - text: filterButtonText(ctx => ctx.session.eventfilter), + text: ctx => ctx.session.eventfilter && ctx.session.eventfilter !== '' + ? `🔎 Filter: ${ctx.session.eventfilter}` + : '🔎 Ab hier filtern', async do(ctx, path) { await question.replyWithHTML( ctx, From 4701568b4f26cf7410fae8d6392c56d6f2cdd275 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 12:04:06 +0100 Subject: [PATCH 25/51] refactor(admin): simplify filter logic --- source/menu/admin/user-quicklook.ts | 13 +++---------- source/menu/events/add.ts | 6 +++--- source/menu/events/details.ts | 2 +- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/source/menu/admin/user-quicklook.ts b/source/menu/admin/user-quicklook.ts index 20843fb8..d5a4fe50 100644 --- a/source/menu/admin/user-quicklook.ts +++ b/source/menu/admin/user-quicklook.ts @@ -12,8 +12,6 @@ import {getUrl} from '../../lib/calendar-helper.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; import type {MyContext} from '../../lib/types.ts'; -export const DEFAULT_FILTER = '.+'; - function nameOfUser({first_name, last_name, username}: User): string { let name = first_name; if (last_name) { @@ -68,7 +66,7 @@ const question = new StatelessQuestion( bot.use(question.middleware()); menu.interact('filter', { - text: ctx => ctx.session.adminuserquicklookfilter && ctx.session.adminuserquicklookfilter !== '.+' + text: ctx => ctx.session.adminuserquicklookfilter ? `🔎 Filter: ${ctx.session.adminuserquicklookfilter}` : '🔎 Filtern', async do(ctx, path) { @@ -85,12 +83,7 @@ menu.interact('filter', { menu.interact('filter-clear', { joinLastRow: true, text: 'Filter aufheben', - hide(ctx) { - return ( - (ctx.session.adminuserquicklookfilter ?? DEFAULT_FILTER) - === DEFAULT_FILTER - ); - }, + hide: ctx => ctx.session.adminuserquicklookfilter === undefined, do(ctx) { delete ctx.session.adminuserquicklookfilter; delete ctx.session.adminuserquicklook; @@ -102,7 +95,7 @@ menu.select('u', { maxRows: 5, columns: 2, async choices(ctx) { - const filter = ctx.session.adminuserquicklookfilter ?? DEFAULT_FILTER; + const filter = ctx.session.adminuserquicklookfilter ?? '.+'; const filterRegex = new RegExp(filter, 'i'); const allConfigs = await ctx.userconfig.all(config => filterRegex.test(JSON.stringify(config))); diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index c6d8338e..113f614f 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -8,8 +8,8 @@ import { count as allEventsCount, directoryExists, find as allEventsFind, getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; -import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; import {typedKeys} from '../../lib/javascript-helper.js'; +import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; const MAX_RESULT_ROWS = 10; const RESULT_COLUMNS = 1; @@ -62,7 +62,7 @@ const question = new StatelessQuestion( bot.use(question.middleware()); menu.interact('filter', { - text: ctx => ctx.session.eventfilter && ctx.session.eventfilter !== '' + text: ctx => ctx.session.eventfilter ? `🔎 Filter: ${ctx.session.eventfilter}` : '🔎 Ab hier filtern', async do(ctx, path) { @@ -77,8 +77,8 @@ menu.interact('filter', { }); menu.interact('filter-clear', { - text: 'Filter aufheben', joinLastRow: true, + text: 'Filter aufheben', hide: ctx => ctx.session.eventfilter === undefined, do(ctx) { delete ctx.session.eventfilter; diff --git a/source/menu/events/details.ts b/source/menu/events/details.ts index e6e8d12b..c0b1af10 100644 --- a/source/menu/events/details.ts +++ b/source/menu/events/details.ts @@ -142,8 +142,8 @@ menu.interact('set-notes', { }); menu.interact('remove-notes', { - text: 'Notiz löschen', joinLastRow: true, + text: 'Notiz löschen', hide(ctx, path) { const eventId = getIdFromPath(path); return !ctx.userconfig.mine.events[eventId]!.notes; From f4a441b4e305a5b8774e1f843f3954dd2360740e Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 12:16:09 +0100 Subject: [PATCH 26/51] refactor(git): simplify imports --- source/lib/all-events.ts | 2 +- source/lib/git-helper.ts | 26 -------------------------- source/lib/git.ts | 27 +++++++++++++++++++++++++++ source/lib/mensa-meals.ts | 2 +- source/menu/events/index.ts | 10 +++++----- source/menu/mensa/index.ts | 6 +++--- 6 files changed, 37 insertions(+), 36 deletions(-) delete mode 100644 source/lib/git-helper.ts create mode 100644 source/lib/git.ts diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 7cb61ea3..76f608fe 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,7 +1,7 @@ import {readFile, watch} from 'node:fs/promises'; import type {EventDirectory, EventId} from './types.ts'; import {typedEntries} from './javascript-helper.js'; -import {EVENT_FILES_DIR} from './git-helper.js'; +import {EVENT_FILES_DIR} from './git.js'; const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; diff --git a/source/lib/git-helper.ts b/source/lib/git-helper.ts deleted file mode 100644 index 3002bbb2..00000000 --- a/source/lib/git-helper.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {exec} from 'node:child_process'; -import {existsSync} from 'node:fs'; -import {promisify} from 'node:util'; - -const MENSA_REPO = 'https://github.com/HAWHHCalendarBot/mensa-data.git'; -export const MENSA_DIR = 'mensa-data'; -const EVENT_FILES_REPO = 'https://github.com/HAWHHCalendarBot/eventfiles.git'; -export const EVENT_FILES_DIR = 'eventfiles'; - -const run = promisify(exec); - -async function pull(repoUrl: string, repoDir: string): Promise { - try { - await (existsSync(`${repoDir}/.git`) - ? run(`git -C ${repoDir} pull`) - : run(`git clone -q --depth 1 ${repoUrl} ${repoDir}`)); - } catch {} -} - -export async function pullMensaData(): Promise { - await pull(MENSA_REPO, MENSA_DIR); -} - -export async function pullEventFiles(): Promise { - await pull(EVENT_FILES_REPO, EVENT_FILES_DIR); -} diff --git a/source/lib/git.ts b/source/lib/git.ts new file mode 100644 index 00000000..6024eeeb --- /dev/null +++ b/source/lib/git.ts @@ -0,0 +1,27 @@ +import {exec} from 'node:child_process'; +import {existsSync} from 'node:fs'; +import {promisify} from 'node:util'; + +export const EVENT_FILES_DIR = 'eventfiles'; +export const MENSA_DIR = 'mensa-data'; + +const run = promisify(exec); + +async function pull(directory: string, remoteUrl: string): Promise { + try { + await (existsSync(`${directory}/.git`) + ? run(`git -C ${directory} pull`) + : run(`git clone -q --depth 1 ${remoteUrl} ${directory}`)); + } catch {} +} + +export async function pullEventFiles(): Promise { + await pull( + EVENT_FILES_DIR, + 'https://github.com/HAWHHCalendarBot/eventfiles.git', + ); +} + +export async function pullMensaData(): Promise { + await pull(MENSA_DIR, 'https://github.com/HAWHHCalendarBot/mensa-data.git'); +} diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index 86d7e23b..cb510460 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -1,6 +1,6 @@ import {readdir, readFile} from 'node:fs/promises'; import type {Meal} from './meal.ts'; -import {MENSA_DIR} from './git-helper.js'; +import {MENSA_DIR} from './git.js'; export async function getCanteenList(): Promise { const found = await readdir(MENSA_DIR, {withFileTypes: true}); diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index 089ff816..6a8bc8cb 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -2,16 +2,16 @@ import {Composer} from 'grammy'; import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import * as allEvents from '../../lib/all-events.ts'; -import {backMainButtons} from '../../lib/inline-menu.ts'; -import type {MyContext} from '../../lib/types.ts'; import {getEventName} from '../../lib/all-events.ts'; +import * as git from '../../lib/git.js'; +import {backMainButtons} from '../../lib/inline-menu.ts'; import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; -import * as gitHelper from '../../lib/git-helper.js'; +import type {MyContext} from '../../lib/types.ts'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; -setInterval(async () => gitHelper.pullEventFiles(), 1000 * 60 * 30); // Every 30 minutes -void gitHelper.pullEventFiles(); +setInterval(async () => git.pullEventFiles(), 1000 * 60 * 30); // Every 30 minutes +void git.pullEventFiles(); export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { diff --git a/source/menu/mensa/index.ts b/source/menu/mensa/index.ts index 37f696aa..96f95045 100644 --- a/source/menu/mensa/index.ts +++ b/source/menu/mensa/index.ts @@ -1,5 +1,5 @@ import {MenuTemplate} from 'grammy-inline-menu'; -import * as gitHelper from '../../lib/git-helper.ts'; +import * as git from '../../lib/git.ts'; import {generateMealText} from '../../lib/mensa-helper.ts'; import {getMealsOfDay} from '../../lib/mensa-meals.ts'; import type {MyContext} from '../../lib/types.ts'; @@ -16,8 +16,8 @@ const WEEKDAYS = [ ] as const; const DAY_IN_MS = 1000 * 60 * 60 * 24; -setInterval(async () => gitHelper.pullMensaData(), 1000 * 60 * 30); // Every 30 minutes -void gitHelper.pullMensaData(); +setInterval(async () => git.pullMensaData(), 1000 * 60 * 30); // Every 30 minutes +void git.pullMensaData(); function getYearMonthDay(date: Readonly): Readonly<{year: number; month: number; day: number}> { const year = date.getFullYear(); From 042540fe12c630a1546a4e02299e417cf6201ee5 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 13:01:33 +0100 Subject: [PATCH 27/51] refactor: EventDirectory is already Readonly by itself --- source/lib/all-events.ts | 6 +++--- source/menu/events/add.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 76f608fe..7d9bff38 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,7 +1,7 @@ import {readFile, watch} from 'node:fs/promises'; -import type {EventDirectory, EventId} from './types.ts'; -import {typedEntries} from './javascript-helper.js'; import {EVENT_FILES_DIR} from './git.js'; +import {typedEntries} from './javascript-helper.js'; +import type {EventDirectory, EventId} from './types.ts'; const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; @@ -75,7 +75,7 @@ export function nonExisting(ids: readonly EventId[]): readonly EventId[] { export function find( pattern: string | RegExp | undefined, startAt: string[] = [], -): Readonly { +): EventDirectory { if (pattern !== undefined) { const regex = new RegExp(pattern, 'i'); const accumulator: Record = {}; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 113f614f..53125070 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -43,7 +43,7 @@ export const menu = new MenuTemplate(async ctx => { return {text, parse_mode: format.parse_mode}; }); -function findEvents(ctx: MyContext): Readonly { +function findEvents(ctx: MyContext): EventDirectory { const filter = ctx.session.eventfilter; return allEventsFind(filter, ctx.session.eventAdd?.eventPath); } From 7e4d6dd82acf195311237317589f54bc3d9cae38 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 13:16:01 +0100 Subject: [PATCH 28/51] refactor(all-events): stricter type for namesOfEvents --- source/lib/all-events.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 7d9bff38..5a6de873 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -6,7 +6,7 @@ import type {EventDirectory, EventId} from './types.ts'; const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; let directory = await loadDirectory(); -let namesOfEvents: Record = await generateMapping(); +let namesOfEvents: Readonly> = await generateMapping(); async function watchForDirectoryChanges() { const watcher = watch(DIRECTORY_FILE); @@ -30,8 +30,8 @@ async function loadDirectory(): Promise> { return directory; } -async function generateMapping(): Promise> { - const namesOfEvents: Record = {}; +async function generateMapping(): Promise>> { + const namesOfEvents: Record = {}; function collect(directory: Partial) { for (const subDirectory of Object.values(directory.subDirectories ?? {})) { From b5bbf17f159e9b094ce9f71efd6357f9cde4bed7 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 13:43:06 +0100 Subject: [PATCH 29/51] refactor: use typedKeys/typedEntries to prevent type issues --- source/lib/all-events.ts | 2 +- source/lib/mensa-helper.ts | 3 ++- source/menu/events/add.ts | 35 +++++++++++-------------- source/menu/events/changes/add/index.ts | 11 ++++---- source/menu/events/changes/index.ts | 5 ++-- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 5a6de873..8ff8cb82 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -95,7 +95,7 @@ export function find( collect(getSubdirectory(startAt) ?? {}); return { subDirectories: {}, - events: Object.fromEntries(Object.entries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), + events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), }; } diff --git a/source/lib/mensa-helper.ts b/source/lib/mensa-helper.ts index 573bf675..58ef2820 100644 --- a/source/lib/mensa-helper.ts +++ b/source/lib/mensa-helper.ts @@ -1,4 +1,5 @@ import {arrayFilterUnique} from 'array-filter-unique'; +import {typedEntries} from './javascript-helper.ts'; import type {Meal} from './meal.ts'; import type {MealWishes, MensaPriceClass, MensaSettings} from './types.ts'; @@ -74,7 +75,7 @@ export function mealNameToHtml( export function mealAdditivesToHtml(meals: readonly Meal[]): string { return meals .flatMap(meal => - Object.entries(meal.Additives).map(([short, full]) => `${short}: ${full}`)) + typedEntries(meal.Additives).map(([short, full]) => `${short}: ${full}`)) .sort() .filter(arrayFilterUnique()) .join('\n'); diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 53125070..2d80c3c5 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -8,7 +8,7 @@ import { count as allEventsCount, directoryExists, find as allEventsFind, getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; -import {typedKeys} from '../../lib/javascript-helper.js'; +import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; const MAX_RESULT_ROWS = 10; @@ -93,27 +93,22 @@ menu.choose('a', { try { ctx.session.eventAdd ??= {eventPath: []}; const filteredEvents = findEvents(ctx); - const alreadySelected = Object.keys(ctx.userconfig.mine.events); - - ctx.session.eventAdd.eventDirectorySubDirectoryItems = Object.keys(filteredEvents.subDirectories); - const subDirectoryItems = Object.entries(filteredEvents.subDirectories) - .map(([name, directory], i) => - Object.keys(directory.subDirectories ?? {}).length > 0 - || Object.keys(directory.events ?? {}).length > 0 - ? ['d' + i, '🗂️ ' + name] - : ['x' + i, '🚫 ' + name]); - - const eventItems = Object.entries(filteredEvents.events) - .map(([eventId, name]) => - alreadySelected.includes(eventId) - ? ['e' + eventId, '✅ ' + name] - : ['e' + eventId, '📅 ' + name]); + const alreadySelected = typedKeys(ctx.userconfig.mine.events); + + ctx.session.eventAdd.eventDirectorySubDirectoryItems = typedKeys(filteredEvents.subDirectories); + const subDirectoryItems = typedEntries(filteredEvents.subDirectories).map(([name, directory], i) => + Object.keys(directory.subDirectories ?? {}).length > 0 + || Object.keys(directory.events ?? {}).length > 0 + ? ['d' + i, '🗂️ ' + name] + : ['x' + i, '🚫 ' + name]); + + const eventItems = typedEntries(filteredEvents.events).map(([eventId, name]) => + alreadySelected.includes(eventId) + ? ['e' + eventId, '✅ ' + name] + : ['e' + eventId, '📅 ' + name]); // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return Object.fromEntries([ - ...subDirectoryItems, - ...eventItems, - ]); + return Object.fromEntries([...subDirectoryItems, ...eventItems]); } catch { return {}; } diff --git a/source/menu/events/changes/add/index.ts b/source/menu/events/changes/add/index.ts index 085d0a98..fc7f7780 100644 --- a/source/menu/events/changes/add/index.ts +++ b/source/menu/events/changes/add/index.ts @@ -11,8 +11,10 @@ import { generateChangeText, loadEvents, } from '../../../../lib/change-helper.ts'; +import {typedKeys} from '../../../../lib/javascript-helper.ts'; import type { - Change, EventId, + Change, + EventId, MyContext, NaiveDateTime, } from '../../../../lib/types.ts'; @@ -35,7 +37,7 @@ export const menu = new MenuTemplate(ctx => { if (!ctx.session.generateChangeDate) { text = 'Zu welchem Termin willst du eine Änderung hinzufügen?'; - const changeDates = Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); + const changeDates = typedKeys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); if (changeDates.length > 0) { text @@ -78,9 +80,8 @@ function generationDataIsValid(ctx: MyContext): boolean { return false; } - const keys = Object.keys(ctx.session.generateChange ?? []); // There have to some changes than that in order to do something. - return keys.length > 0; + return Object.keys(ctx.session.generateChange ?? []).length > 0; } menu.choose('date', { @@ -93,7 +94,7 @@ menu.choose('date', { return {}; } - const existingChangeDates = new Set(Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {})); + const existingChangeDates = new Set(typedKeys(ctx.userconfig.mine.events[eventId]?.changes ?? {})); const events = await loadEvents(eventId); const dates = events .map(o => o.startTime) diff --git a/source/menu/events/changes/index.ts b/source/menu/events/changes/index.ts index 7e48f501..2f7fb895 100644 --- a/source/menu/events/changes/index.ts +++ b/source/menu/events/changes/index.ts @@ -1,9 +1,10 @@ import {Composer} from 'grammy'; import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; +import {getEventName} from '../../../lib/all-events.js'; import {backMainButtons} from '../../../lib/inline-menu.ts'; +import {typedKeys} from '../../../lib/javascript-helper.ts'; import type {EventId, MyContext} from '../../../lib/types.ts'; -import {getEventName} from '../../../lib/all-events.js'; import * as changeAdd from './add/index.ts'; import * as changeDetails from './details.ts'; @@ -29,7 +30,7 @@ menu.chooseIntoSubmenu('d', changeDetails.menu, { columns: 1, choices(ctx) { const eventId = ctx.match![1]! as EventId; - return Object.keys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); + return typedKeys(ctx.userconfig.mine.events[eventId]?.changes ?? {}); }, getCurrentPage: ctx => ctx.session.page, setPage(ctx, page) { From e1bbb1cc389796ed6dfec2599ca4e9f15380ea17 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 15:03:24 +0100 Subject: [PATCH 30/51] fix(events): correctly display non existing event id --- source/lib/all-events.ts | 4 ++++ source/menu/events/add.ts | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 8ff8cb82..65e2b273 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -68,6 +68,10 @@ export function count(): number { return Object.keys(namesOfEvents).length; } +export function exists(id: EventId): boolean { + return id in namesOfEvents; +} + export function nonExisting(ids: readonly EventId[]): readonly EventId[] { return ids.filter(id => !(id in namesOfEvents)); } diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index e0989399..e92f3c75 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -1,11 +1,18 @@ import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Composer} from 'grammy'; import { - deleteMenuFromContext, getMenuOfPath, MenuTemplate, replyMenuToContext, + deleteMenuFromContext, + getMenuOfPath, + MenuTemplate, + replyMenuToContext, } from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import { - count as allEventsCount, directoryExists, find as allEventsFind, getEventName, + count as allEventsCount, + directoryExists, + exists, + find as allEventsFind, + getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; @@ -115,14 +122,13 @@ menu.choose('a', { async do(ctx, key) { if (key.startsWith('e')) { const eventId = key.slice(1) as EventId; - const eventName = getEventName(eventId); - const isAlreadyInCalendar = typedKeys(ctx.userconfig.mine.events).includes(eventId); - - if (eventName === undefined) { + if (!exists(eventId)) { await ctx.answerCallbackQuery(`Event mit Id ${eventId} existiert nicht!`); return true; } + const eventName = getEventName(eventId); + const isAlreadyInCalendar = typedKeys(ctx.userconfig.mine.events).includes(eventId); if (isAlreadyInCalendar) { await ctx.answerCallbackQuery(`${eventName} ist bereits in deinem Kalender!`); return true; From 22c0a88061808acc64b5cf76a0bde1a43906da1e Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 9 Nov 2025 15:07:14 +0100 Subject: [PATCH 31/51] refactor: use same method name as before --- source/menu/events/add.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index e92f3c75..143daab8 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -10,7 +10,7 @@ import {html as format} from 'telegram-format'; import { count as allEventsCount, directoryExists, - exists, + exists as allEventsExists, find as allEventsFind, getEventName, } from '../../lib/all-events.ts'; @@ -122,7 +122,7 @@ menu.choose('a', { async do(ctx, key) { if (key.startsWith('e')) { const eventId = key.slice(1) as EventId; - if (!exists(eventId)) { + if (!allEventsExists(eventId)) { await ctx.answerCallbackQuery(`Event mit Id ${eventId} existiert nicht!`); return true; } From 646aba05c0cda42d119b518d090372e6b59dcf4a Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:25:26 +0100 Subject: [PATCH 32/51] store events in subdirectory --- source/lib/change-helper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 3bc762fa..90a345ac 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -4,6 +4,7 @@ import type { Change, EventEntry, EventId, NaiveDateTime, } from './types.ts'; import {getEventName} from './all-events.js'; +import {EVENT_FILES_DIR} from "./git-helper.js"; export function generateChangeDescription(change: Change): string { let text = ''; @@ -70,7 +71,7 @@ export function generateShortChangeText( export async function loadEvents(eventId: EventId): Promise { try { - const content = await readFile(`eventfiles/${eventId}.json`, 'utf8'); + const content = await readFile(`${EVENT_FILES_DIR}/events/${eventId}.json`, 'utf8'); return JSON.parse(content) as EventEntry[]; } catch (error) { console.error('ERROR while loading events for change date picker', error); From 1ae8294e55fe9baa967be51e919c2d3c0662f8bf Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:26:47 +0100 Subject: [PATCH 33/51] fixup: store events in subdirectory --- source/lib/change-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 90a345ac..1dd8181e 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -4,7 +4,7 @@ import type { Change, EventEntry, EventId, NaiveDateTime, } from './types.ts'; import {getEventName} from './all-events.js'; -import {EVENT_FILES_DIR} from "./git-helper.js"; +import {EVENT_FILES_DIR} from "./git.js"; export function generateChangeDescription(change: Change): string { let text = ''; From f157e8fdd3d9fba36cad2b6b59f81b9b1c2f720a Mon Sep 17 00:00:00 2001 From: malex_14 <39774812+Malex14@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:00:29 +0100 Subject: [PATCH 34/51] make xo happy --- source/lib/change-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 1dd8181e..733c7c20 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -4,7 +4,7 @@ import type { Change, EventEntry, EventId, NaiveDateTime, } from './types.ts'; import {getEventName} from './all-events.js'; -import {EVENT_FILES_DIR} from "./git.js"; +import {EVENT_FILES_DIR} from './git.js'; export function generateChangeDescription(change: Change): string { let text = ''; From 42256b9465a76f16a9d97041127573a34c5acacd Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 14 Nov 2025 14:05:41 +0100 Subject: [PATCH 35/51] chore: adapt init-debug-environment.sh --- init-debug-environment.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/init-debug-environment.sh b/init-debug-environment.sh index 0a61009a..de66082a 100755 --- a/init-debug-environment.sh +++ b/init-debug-environment.sh @@ -2,7 +2,6 @@ set -e # assumes other repos were cloned next to this repo (and executed) -ln -rfs ../downloader/eventfiles . ln -rfs ../mensa-data . mkdir -p userconfig From 7652dd3f1a40c5f497589db615f0819a7b1a4b9f Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 14 Nov 2025 14:06:48 +0100 Subject: [PATCH 36/51] fixup! chore: adapt init-debug-environment.sh --- init-debug-environment.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/init-debug-environment.sh b/init-debug-environment.sh index de66082a..3d0f35a6 100755 --- a/init-debug-environment.sh +++ b/init-debug-environment.sh @@ -2,6 +2,7 @@ set -e # assumes other repos were cloned next to this repo (and executed) +ln -rfs ../eventfiles . ln -rfs ../mensa-data . mkdir -p userconfig From a22f6c92f645088e5b52097f90780508b0dc6e0c Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 14 Nov 2025 16:31:59 +0100 Subject: [PATCH 37/51] refactor: always Partial EventDirectory --- source/lib/all-events.ts | 16 ++++++---------- source/lib/types.ts | 4 ++-- source/menu/events/add.ts | 8 ++++---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 65e2b273..23d78aef 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -23,17 +23,17 @@ async function watchForDirectoryChanges() { // eslint-disable-next-line unicorn/prefer-top-level-await void watchForDirectoryChanges(); -async function loadDirectory(): Promise> { +async function loadDirectory(): Promise { console.log(new Date(), 'Loading directory'); const directoryString = await readFile(DIRECTORY_FILE, 'utf8'); - const directory = JSON.parse(directoryString) as Partial; + const directory = JSON.parse(directoryString) as EventDirectory; return directory; } async function generateMapping(): Promise>> { const namesOfEvents: Record = {}; - function collect(directory: Partial) { + function collect(directory: EventDirectory) { for (const subDirectory of Object.values(directory.subDirectories ?? {})) { collect(subDirectory); } @@ -45,7 +45,7 @@ async function generateMapping(): Promise>> { return namesOfEvents; } -function getSubdirectory(path: string[]): Partial | undefined { +function getSubdirectory(path: string[]): EventDirectory | undefined { let resolvedDirectory = directory; for (const part of path) { @@ -84,7 +84,7 @@ export function find( const regex = new RegExp(pattern, 'i'); const accumulator: Record = {}; - function collect(directory: Partial) { + function collect(directory: EventDirectory) { for (const [eventId, name] of typedEntries(directory.events ?? {})) { if (regex.test(name)) { accumulator[eventId] = name; @@ -103,11 +103,7 @@ export function find( }; } - const directory = getSubdirectory(startAt) ?? {}; - return { - subDirectories: directory.subDirectories ?? {}, - events: directory.events ?? {}, - }; + return getSubdirectory(startAt) ?? {}; } export function directoryExists(path: string[]): boolean { diff --git a/source/lib/types.ts b/source/lib/types.ts index 5ecb5088..532d05fd 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -103,9 +103,9 @@ export type EventId = `${number}_${number | string}`; export type EventDirectory = { /** Maps the directory name to its content */ - readonly subDirectories: Readonly>>; + readonly subDirectories?: Readonly>; /** Maps `EventId` to the human-readable name */ - readonly events: Readonly>; + readonly events?: Readonly>; }; export type EventEntry = { diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 143daab8..62114428 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -35,7 +35,7 @@ export const menu = new MenuTemplate(async ctx => { text += `Ich habe ${total} Veranstaltungen. Nutze den Filter um die Auswahl einzugrenzen.`; } else { const filteredEvents = findEvents(ctx); - const eventCount = Object.keys(filteredEvents.events).length; + const eventCount = Object.keys(filteredEvents.events ?? {}).length; text += `Mit deinem Filter konnte ich ${eventCount} passende Veranstaltungen finden.`; } } catch (error) { @@ -101,14 +101,14 @@ menu.choose('a', { const filteredEvents = findEvents(ctx); const alreadySelected = typedKeys(ctx.userconfig.mine.events); - ctx.session.eventAdd.eventDirectorySubDirectoryItems = typedKeys(filteredEvents.subDirectories); - const subDirectoryItems = typedEntries(filteredEvents.subDirectories).map(([name, directory], i) => + ctx.session.eventAdd.eventDirectorySubDirectoryItems = typedKeys(filteredEvents.subDirectories ?? {}); + const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => Object.keys(directory.subDirectories ?? {}).length > 0 || Object.keys(directory.events ?? {}).length > 0 ? ['d' + i, '🗂️ ' + name] : ['x' + i, '🚫 ' + name]); - const eventItems = typedEntries(filteredEvents.events).map(([eventId, name]) => + const eventItems = typedEntries(filteredEvents.events ?? {}).map(([eventId, name]) => alreadySelected.includes(eventId) ? ['e' + eventId, '✅ ' + name] : ['e' + eventId, '📅 ' + name]); From c9e375ceb4508132ea192403e92b84a54f853001 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 17:21:32 +0100 Subject: [PATCH 38/51] feat(event-add): improve filter and path handling --- source/lib/all-events.ts | 22 +++++++-- source/lib/types.ts | 11 ++--- source/menu/events/add.ts | 96 +++++++++++++++++++++------------------ 3 files changed, 74 insertions(+), 55 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 23d78aef..10fb448c 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -60,6 +60,22 @@ function getSubdirectory(path: string[]): EventDirectory | undefined { return resolvedDirectory; } +export function directoryHasContent(directory: EventDirectory): boolean { + const events = Object.keys(directory.events ?? {}).length; + const subDirectories = Object.keys(directory.subDirectories ?? {}).length; + return events > 0 || subDirectories > 0; +} + +export function directoryExists(path: string[]): boolean { + if (path.length === 0) { + // Toplevel always exists + return true; + } + + const directory = getSubdirectory(path); + return Boolean(directory && directoryHasContent(directory)); +} + export function getEventName(id: EventId): string { return namesOfEvents[id] ?? id; } @@ -78,7 +94,7 @@ export function nonExisting(ids: readonly EventId[]): readonly EventId[] { export function find( pattern: string | RegExp | undefined, - startAt: string[] = [], + startAt: string[], ): EventDirectory { if (pattern !== undefined) { const regex = new RegExp(pattern, 'i'); @@ -105,7 +121,3 @@ export function find( return getSubdirectory(startAt) ?? {}; } - -export function directoryExists(path: string[]): boolean { - return getSubdirectory(path) !== undefined; -} diff --git a/source/lib/types.ts b/source/lib/types.ts index 532d05fd..3786603e 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -24,13 +24,10 @@ export type Session = { /** User ID */ adminuserquicklook?: number; adminuserquicklookfilter?: string; - eventfilter?: string; eventAdd?: { - /** Path to the currently selected subdirectory on the add events screen. - * - * The entries of this array are the keys of the (sub)directories. - */ - eventPath: string[]; + filter?: string; + /** Currently selected subdirectory */ + path: string[]; /** Subdirectory item keys of the directory selected by eventPath * * This array stores the keys of subdirectories in the currently selected directory. @@ -38,7 +35,7 @@ export type Session = { * Telegram callback data restricts allowed characters and length, so we store the * keys here and use the array index as the callback payload. */ - eventDirectorySubDirectoryItems?: string[]; + subDirectoryItems?: string[]; }; generateChangeEventId?: EventId; generateChangeDate?: NaiveDateTime; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 62114428..025bd960 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -10,33 +10,46 @@ import {html as format} from 'telegram-format'; import { count as allEventsCount, directoryExists, + directoryHasContent, exists as allEventsExists, find as allEventsFind, getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; -import type {EventDirectory, EventId, MyContext} from '../../lib/types.ts'; +import type {EventId, MyContext} from '../../lib/types.ts'; const MAX_RESULT_ROWS = 10; const RESULT_COLUMNS = 1; export const bot = new Composer(); -export const menu = new MenuTemplate(async ctx => { - const total = allEventsCount(); - ctx.session.eventAdd ??= {eventPath: []}; +export const menu = new MenuTemplate(ctx => { + ctx.session.eventAdd ??= {path: []}; + while (!directoryExists(ctx.session.eventAdd.path)) { + ctx.session.eventAdd.path.pop(); + } let text = format.bold('Veranstaltungen'); - text += '\nWelche Events möchtest du hinzufügen?'; + for (const segment of ctx.session.eventAdd.path) { + text += '\n🗂️ ' + segment; + } + + text += '\n\nWelche Events möchtest du hinzufügen?'; text += '\n\n'; try { - if (ctx.session.eventfilter === undefined) { - text += `Ich habe ${total} Veranstaltungen. Nutze den Filter um die Auswahl einzugrenzen.`; - } else { - const filteredEvents = findEvents(ctx); + if (ctx.session.eventAdd.filter) { + const filteredEvents = allEventsFind( + ctx.session.eventAdd.filter, + ctx.session.eventAdd.path, + ); const eventCount = Object.keys(filteredEvents.events ?? {}).length; text += `Mit deinem Filter konnte ich ${eventCount} passende Veranstaltungen finden.`; + } else if (ctx.session.eventAdd.path.length === 0) { + const total = allEventsCount(); + text += `Ich habe ${total} Veranstaltungen. Nutze den Filter oder die Ordner um die Auswahl einzugrenzen.`; + } else { + text += 'Nutze den Filter oder die Ordner um die Auswahl einzugrenzen.'; } } catch (error) { const errorText = error instanceof Error ? error.message : String(error); @@ -47,16 +60,12 @@ export const menu = new MenuTemplate(async ctx => { return {text, parse_mode: format.parse_mode}; }); -function findEvents(ctx: MyContext): EventDirectory { - const filter = ctx.session.eventfilter; - return allEventsFind(filter, ctx.session.eventAdd?.eventPath); -} - const question = new StatelessQuestion( 'events-add-filter', async (ctx, path) => { if (ctx.message.text) { - ctx.session.eventfilter = ctx.message.text; + ctx.session.eventAdd ??= {path: []}; + ctx.session.eventAdd.filter = ctx.message.text; } await replyMenuToContext(menu, ctx, path); @@ -67,8 +76,8 @@ bot.use(question.middleware()); menu.interact('filter', { text(ctx) { - return ctx.session.eventfilter - ? `🔎 Filter: ${ctx.session.eventfilter}` + return ctx.session.eventAdd?.filter + ? `🔎 Filter: ${ctx.session.eventAdd.filter}` : '🔎 Ab hier filtern'; }, async do(ctx, path) { @@ -85,9 +94,9 @@ menu.interact('filter', { menu.interact('filter-clear', { joinLastRow: true, text: 'Filter aufheben', - hide: ctx => ctx.session.eventfilter === undefined, + hide: ctx => ctx.session.eventAdd?.filter === undefined, do(ctx) { - delete ctx.session.eventfilter; + delete ctx.session.eventAdd?.filter; return true; }, }); @@ -95,16 +104,18 @@ menu.interact('filter-clear', { menu.choose('a', { maxRows: MAX_RESULT_ROWS, columns: RESULT_COLUMNS, - async choices(ctx) { + choices(ctx) { try { - ctx.session.eventAdd ??= {eventPath: []}; - const filteredEvents = findEvents(ctx); + ctx.session.eventAdd ??= {path: []}; + const filteredEvents = allEventsFind( + ctx.session.eventAdd.filter, + ctx.session.eventAdd.path, + ); const alreadySelected = typedKeys(ctx.userconfig.mine.events); - ctx.session.eventAdd.eventDirectorySubDirectoryItems = typedKeys(filteredEvents.subDirectories ?? {}); + ctx.session.eventAdd.subDirectoryItems = typedKeys(filteredEvents.subDirectories ?? {}); const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => - Object.keys(directory.subDirectories ?? {}).length > 0 - || Object.keys(directory.events ?? {}).length > 0 + directoryHasContent(directory) ? ['d' + i, '🗂️ ' + name] : ['x' + i, '🚫 ' + name]); @@ -145,14 +156,14 @@ menu.choose('a', { return true; } - if (ctx.session.eventAdd.eventDirectorySubDirectoryItems !== undefined) { - const chosenSubDirectory = ctx.session.eventAdd.eventDirectorySubDirectoryItems[Number(key.slice(1))]; - delete ctx.session.eventAdd.eventDirectorySubDirectoryItems; + if (ctx.session.eventAdd.subDirectoryItems !== undefined) { + const chosenSubDirectory = ctx.session.eventAdd.subDirectoryItems[Number(key.slice(1))]; + delete ctx.session.eventAdd.subDirectoryItems; if (chosenSubDirectory !== undefined) { - ctx.session.eventAdd.eventPath.push(chosenSubDirectory); + ctx.session.eventAdd.path.push(chosenSubDirectory); - if (directoryExists(ctx.session.eventAdd.eventPath)) { + if (directoryExists(ctx.session.eventAdd.path)) { return true; } } @@ -174,23 +185,22 @@ menu.choose('a', { menu.interact('back', { text: BACK_BUTTON_TEXT, - async do(ctx) { - if (ctx.session.eventfilter !== undefined) { - delete ctx.session.eventfilter; - return true; - } - - if (ctx.session.eventAdd?.eventPath === undefined - || ctx.session.eventAdd?.eventPath.length === 0) { + do(ctx) { + if (!ctx.session.eventAdd || ctx.session.eventAdd.path.length === 0) { delete ctx.session.eventAdd; return '..'; } - ctx.session.eventAdd.eventPath.pop(); - if (!directoryExists(ctx.session.eventAdd.eventPath)) { - delete ctx.session.eventAdd; - } - + ctx.session.eventAdd.path.pop(); return true; }, }); + +menu.interact('top', { + joinLastRow: true, + text: '🔝 zur Übersicht…', + do(ctx) { + delete ctx.session.eventAdd; + return '..'; + }, +}); From e6d4af064dcd8cb9d639ca8bdc18a9d4eb8e0370 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 18:50:34 +0100 Subject: [PATCH 39/51] fix(event-add): prevent clicking non existing subdirectories --- source/lib/types.ts | 8 ------ source/menu/events/add.ts | 59 +++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/source/lib/types.ts b/source/lib/types.ts index 3786603e..93f1571e 100644 --- a/source/lib/types.ts +++ b/source/lib/types.ts @@ -28,14 +28,6 @@ export type Session = { filter?: string; /** Currently selected subdirectory */ path: string[]; - /** Subdirectory item keys of the directory selected by eventPath - * - * This array stores the keys of subdirectories in the currently selected directory. - * On the event-adding screen these keys are used to navigate into subdirectories. - * Telegram callback data restricts allowed characters and length, so we store the - * keys here and use the array index as the callback payload. - */ - subDirectoryItems?: string[]; }; generateChangeEventId?: EventId; generateChangeDate?: NaiveDateTime; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 025bd960..62aa56d4 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -101,7 +101,7 @@ menu.interact('filter-clear', { }, }); -menu.choose('a', { +menu.choose('list', { maxRows: MAX_RESULT_ROWS, columns: RESULT_COLUMNS, choices(ctx) { @@ -113,11 +113,16 @@ menu.choose('a', { ); const alreadySelected = typedKeys(ctx.userconfig.mine.events); - ctx.session.eventAdd.subDirectoryItems = typedKeys(filteredEvents.subDirectories ?? {}); - const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => - directoryHasContent(directory) - ? ['d' + i, '🗂️ ' + name] - : ['x' + i, '🚫 ' + name]); + const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => { + if (!directoryHasContent(directory)) { + return ['x' + i, '🚫 ' + name]; + } + + return [ + 'd' + i + ' ' + name.replaceAll('/', '').slice(0, 48), + '🗂️ ' + name, + ]; + }); const eventItems = typedEntries(filteredEvents.events ?? {}).map(([eventId, name]) => alreadySelected.includes(eventId) @@ -150,32 +155,38 @@ menu.choose('a', { return true; } - if (key.startsWith('d')) { - if (ctx.session.eventAdd === undefined) { - await ctx.answerCallbackQuery('Interner Zustand ungültig.'); - return true; - } + if (key.startsWith('x')) { + await ctx.answerCallbackQuery('Dieses Verzeichnis ist leer.'); + return false; + } - if (ctx.session.eventAdd.subDirectoryItems !== undefined) { - const chosenSubDirectory = ctx.session.eventAdd.subDirectoryItems[Number(key.slice(1))]; - delete ctx.session.eventAdd.subDirectoryItems; + const directoryMatch = /^d(\d+) (.+)$/.exec(key); + if (directoryMatch) { + const index = Number(directoryMatch[1]); + const prefix = directoryMatch[2]; - if (chosenSubDirectory !== undefined) { - ctx.session.eventAdd.path.push(chosenSubDirectory); + // Inline-menu choices() ensures that the clicked key still exists. As the name is included in the prefix part of the key this can only fail if an event with exactly the same prefix is placed on the same index. This will prevent clicks on not anymore existing choices like directory.json changed or ctx.session lost after bot restart. + if (!ctx.session.eventAdd || !prefix) { + // Will never happen as choices() is called first to ensure only existing choices are clicked + return true; + } - if (directoryExists(ctx.session.eventAdd.path)) { - return true; - } - } + const filteredEvents = allEventsFind( + ctx.session.eventAdd.filter, + ctx.session.eventAdd.path, + ); + const filteredSubDirectories = typedEntries(filteredEvents.subDirectories ?? {}); + const chosenSubDirectory = filteredSubDirectories[index]?.[0]; + if (!chosenSubDirectory) { + // Will never happen as choices() is called first to ensure only existing choices are clicked + return true; } - await ctx.answerCallbackQuery('Dieses Verzeichnis gibt es nicht mehr.'); - delete ctx.session.eventAdd; + ctx.session.eventAdd.path.push(chosenSubDirectory); return true; } - await ctx.answerCallbackQuery('Dieses Verzeichnis ist leer.'); - return false; + return true; // Unknown state }, getCurrentPage: ctx => ctx.session.page, setPage(ctx, page) { From 52ec85e4fb183b07333cc0d5cf16c5d309e0cbeb Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 19:21:45 +0100 Subject: [PATCH 40/51] refactor(events): easier to read find function move the simple path to the top as early abort --- source/lib/all-events.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 10fb448c..ce0dce27 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -96,28 +96,27 @@ export function find( pattern: string | RegExp | undefined, startAt: string[], ): EventDirectory { - if (pattern !== undefined) { - const regex = new RegExp(pattern, 'i'); - const accumulator: Record = {}; - - function collect(directory: EventDirectory) { - for (const [eventId, name] of typedEntries(directory.events ?? {})) { - if (regex.test(name)) { - accumulator[eventId] = name; - } - } + if (!pattern) { + return getSubdirectory(startAt) ?? {}; + } - for (const subDirectory of Object.values(directory.subDirectories ?? {})) { - collect(subDirectory); + const regex = new RegExp(pattern, 'i'); + const accumulator: Record = {}; + + function collect(directory: EventDirectory) { + for (const [eventId, name] of typedEntries(directory.events ?? {})) { + if (regex.test(name)) { + accumulator[eventId] = name; } } - collect(getSubdirectory(startAt) ?? {}); - return { - subDirectories: {}, - events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), - }; + for (const subDirectory of Object.values(directory.subDirectories ?? {})) { + collect(subDirectory); + } } - return getSubdirectory(startAt) ?? {}; + collect(getSubdirectory(startAt) ?? {}); + return { + events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), + }; } From b49dd5e860dbd8d05e709e257b854aa89f73a626 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 19:42:20 +0100 Subject: [PATCH 41/51] fix(events): sort event buttons by name --- source/menu/events/index.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index 6a8bc8cb..bdce192f 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -86,27 +86,26 @@ menu.submenu('a', addMenu.menu, {text: '➕ Veranstaltung hinzufügen'}); menu.chooseIntoSubmenu('d', detailsMenu.menu, { columns: 1, choices(ctx) { - const result: Record = {}; + const entries = typedEntries(ctx.userconfig.mine.events) + .map(([eventId, details]) => { + let title = getEventName(eventId) + ' '; - for (const [eventId, details] of typedEntries(ctx.userconfig.mine.events)) { - let title = getEventName(eventId) + ' '; - - if (Object.keys(details.changes ?? {}).length > 0) { - title += '✏️'; - } - - if (details.alertMinutesBefore !== undefined) { - title += '⏰'; - } + if (Object.keys(details.changes ?? {}).length > 0) { + title += '✏️'; + } - if (details.notes) { - title += '🗒'; - } + if (details.alertMinutesBefore !== undefined) { + title += '⏰'; + } - result[eventId] = title.trim(); - } + if (details.notes) { + title += '🗒'; + } - return result; + return [eventId, title.trim()] as const; + }) + .sort((a, b) => a[1]?.localeCompare(b[1])); + return Object.fromEntries(entries); }, getCurrentPage: ctx => ctx.session.page, setPage(ctx, page) { From 57425b1243d047f1b41e7d803e0a2e5e623c7821 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 19:55:11 +0100 Subject: [PATCH 42/51] refactor: use in keyword over typedKeys helper --- source/menu/events/add.ts | 11 +++-------- source/parts/changes-inline.ts | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 62aa56d4..bd8096be 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -16,7 +16,7 @@ import { getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; -import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; +import {typedEntries} from '../../lib/javascript-helper.js'; import type {EventId, MyContext} from '../../lib/types.ts'; const MAX_RESULT_ROWS = 10; @@ -111,8 +111,6 @@ menu.choose('list', { ctx.session.eventAdd.filter, ctx.session.eventAdd.path, ); - const alreadySelected = typedKeys(ctx.userconfig.mine.events); - const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => { if (!directoryHasContent(directory)) { return ['x' + i, '🚫 ' + name]; @@ -123,12 +121,10 @@ menu.choose('list', { '🗂️ ' + name, ]; }); - const eventItems = typedEntries(filteredEvents.events ?? {}).map(([eventId, name]) => - alreadySelected.includes(eventId) + eventId in ctx.userconfig.mine.events ? ['e' + eventId, '✅ ' + name] : ['e' + eventId, '📅 ' + name]); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Object.fromEntries([...subDirectoryItems, ...eventItems]); } catch { @@ -144,8 +140,7 @@ menu.choose('list', { } const eventName = getEventName(eventId); - const isAlreadyInCalendar = typedKeys(ctx.userconfig.mine.events).includes(eventId); - if (isAlreadyInCalendar) { + if (eventId in ctx.userconfig.mine.events) { await ctx.answerCallbackQuery(`${eventName} ist bereits in deinem Kalender!`); return true; } diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index 61209ee1..cd1e52bd 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -10,7 +10,7 @@ import { import type { Change, EventId, MyContext, NaiveDateTime, } from '../lib/types.ts'; -import {typedEntries, typedKeys} from '../lib/javascript-helper.js'; +import {typedEntries} from '../lib/javascript-helper.js'; export const bot = new Composer(); @@ -87,7 +87,7 @@ async function getChangeFromContextMatch(ctx: MyContext): Promise Date: Sun, 16 Nov 2025 20:29:06 +0100 Subject: [PATCH 43/51] refactor: replace file watcher with interval --- source/lib/all-events.ts | 33 +++++++++++---------------------- source/lib/mensa-meals.ts | 6 +++++- source/menu/events/index.ts | 4 ---- source/menu/mensa/index.ts | 4 ---- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index ce0dce27..32414e92 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,33 +1,22 @@ -import {readFile, watch} from 'node:fs/promises'; -import {EVENT_FILES_DIR} from './git.js'; +import {readFile} from 'node:fs/promises'; +import {EVENT_FILES_DIR, pullEventFiles} from './git.js'; import {typedEntries} from './javascript-helper.js'; import type {EventDirectory, EventId} from './types.ts'; const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; -let directory = await loadDirectory(); -let namesOfEvents: Readonly> = await generateMapping(); +let directory: EventDirectory = {}; +let namesOfEvents: Readonly> = {}; -async function watchForDirectoryChanges() { - const watcher = watch(DIRECTORY_FILE); - for await (const event of watcher) { - if (event.eventType === 'change') { - console.log(new Date(), 'Detected file change. Reloading...'); - directory = await loadDirectory(); - namesOfEvents = await generateMapping(); - } - } -} - -// We do not want to await this Promise, since it will never resolve and would cause the module to hang on load. -// eslint-disable-next-line unicorn/prefer-top-level-await -void watchForDirectoryChanges(); +setInterval(async () => update(), 1000 * 60 * 30); // Every 30 minutes +await update(); +console.log(new Date(), 'eventfiles loaded'); -async function loadDirectory(): Promise { - console.log(new Date(), 'Loading directory'); +async function update() { + await pullEventFiles(); const directoryString = await readFile(DIRECTORY_FILE, 'utf8'); - const directory = JSON.parse(directoryString) as EventDirectory; - return directory; + directory = JSON.parse(directoryString) as EventDirectory; + namesOfEvents = await generateMapping(); } async function generateMapping(): Promise>> { diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index cb510460..6cfd2dbd 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -1,6 +1,10 @@ import {readdir, readFile} from 'node:fs/promises'; +import {MENSA_DIR, pullMensaData} from './git.js'; import type {Meal} from './meal.ts'; -import {MENSA_DIR} from './git.js'; + +setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes +await pullMensaData(); +console.log(new Date(), 'mensa-data loaded'); export async function getCanteenList(): Promise { const found = await readdir(MENSA_DIR, {withFileTypes: true}); diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index bdce192f..d459af45 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -3,16 +3,12 @@ import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; import * as allEvents from '../../lib/all-events.ts'; import {getEventName} from '../../lib/all-events.ts'; -import * as git from '../../lib/git.js'; import {backMainButtons} from '../../lib/inline-menu.ts'; import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; import type {MyContext} from '../../lib/types.ts'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; -setInterval(async () => git.pullEventFiles(), 1000 * 60 * 30); // Every 30 minutes -void git.pullEventFiles(); - export const bot = new Composer(); export const menu = new MenuTemplate(async ctx => { delete ctx.session.eventAdd; diff --git a/source/menu/mensa/index.ts b/source/menu/mensa/index.ts index 96f95045..73069431 100644 --- a/source/menu/mensa/index.ts +++ b/source/menu/mensa/index.ts @@ -1,5 +1,4 @@ import {MenuTemplate} from 'grammy-inline-menu'; -import * as git from '../../lib/git.ts'; import {generateMealText} from '../../lib/mensa-helper.ts'; import {getMealsOfDay} from '../../lib/mensa-meals.ts'; import type {MyContext} from '../../lib/types.ts'; @@ -16,9 +15,6 @@ const WEEKDAYS = [ ] as const; const DAY_IN_MS = 1000 * 60 * 60 * 24; -setInterval(async () => git.pullMensaData(), 1000 * 60 * 30); // Every 30 minutes -void git.pullMensaData(); - function getYearMonthDay(date: Readonly): Readonly<{year: number; month: number; day: number}> { const year = date.getFullYear(); const month = date.getMonth() + 1; From 6483e2d1fb35917ad7cc673e4a05f4ddb3374ce2 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 20:34:01 +0100 Subject: [PATCH 44/51] refactor: import from ts extension --- source/lib/all-events.ts | 4 ++-- source/lib/change-helper.ts | 4 ++-- source/lib/mensa-meals.ts | 2 +- source/menu/events/add.ts | 2 +- source/menu/events/changes/details.ts | 5 ++++- source/menu/events/changes/index.ts | 2 +- source/menu/events/details.ts | 2 +- source/menu/events/index.ts | 2 +- source/parts/changes-inline.ts | 7 +++++-- 9 files changed, 18 insertions(+), 12 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 32414e92..8d5469cc 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,6 +1,6 @@ import {readFile} from 'node:fs/promises'; -import {EVENT_FILES_DIR, pullEventFiles} from './git.js'; -import {typedEntries} from './javascript-helper.js'; +import {EVENT_FILES_DIR, pullEventFiles} from './git.ts'; +import {typedEntries} from './javascript-helper.ts'; import type {EventDirectory, EventId} from './types.ts'; const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 733c7c20..b8910ca4 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -3,8 +3,8 @@ import {html as format} from 'telegram-format'; import type { Change, EventEntry, EventId, NaiveDateTime, } from './types.ts'; -import {getEventName} from './all-events.js'; -import {EVENT_FILES_DIR} from './git.js'; +import {getEventName} from './all-events.ts'; +import {EVENT_FILES_DIR} from './git.ts'; export function generateChangeDescription(change: Change): string { let text = ''; diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index 6cfd2dbd..9335c252 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -1,5 +1,5 @@ import {readdir, readFile} from 'node:fs/promises'; -import {MENSA_DIR, pullMensaData} from './git.js'; +import {MENSA_DIR, pullMensaData} from './git.ts'; import type {Meal} from './meal.ts'; setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index bd8096be..e2ea6228 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -16,7 +16,7 @@ import { getEventName, } from '../../lib/all-events.ts'; import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; -import {typedEntries} from '../../lib/javascript-helper.js'; +import {typedEntries} from '../../lib/javascript-helper.ts'; import type {EventId, MyContext} from '../../lib/types.ts'; const MAX_RESULT_ROWS = 10; diff --git a/source/menu/events/changes/details.ts b/source/menu/events/changes/details.ts index 966967ea..4ece2d97 100644 --- a/source/menu/events/changes/details.ts +++ b/source/menu/events/changes/details.ts @@ -5,7 +5,10 @@ import { } from '../../../lib/change-helper.ts'; import {backMainButtons} from '../../../lib/inline-menu.ts'; import type { - Change, EventId, MyContext, NaiveDateTime, + Change, + EventId, + MyContext, + NaiveDateTime, } from '../../../lib/types.ts'; function getChangeFromContext(ctx: MyContext): [EventId, NaiveDateTime, Change | undefined] { diff --git a/source/menu/events/changes/index.ts b/source/menu/events/changes/index.ts index 2f7fb895..305c8d62 100644 --- a/source/menu/events/changes/index.ts +++ b/source/menu/events/changes/index.ts @@ -1,7 +1,7 @@ import {Composer} from 'grammy'; import {MenuTemplate} from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; -import {getEventName} from '../../../lib/all-events.js'; +import {getEventName} from '../../../lib/all-events.ts'; import {backMainButtons} from '../../../lib/inline-menu.ts'; import {typedKeys} from '../../../lib/javascript-helper.ts'; import type {EventId, MyContext} from '../../../lib/types.ts'; diff --git a/source/menu/events/details.ts b/source/menu/events/details.ts index c0b1af10..28308de8 100644 --- a/source/menu/events/details.ts +++ b/source/menu/events/details.ts @@ -9,7 +9,7 @@ import { import {html as format} from 'telegram-format'; import {backMainButtons} from '../../lib/inline-menu.ts'; import type {EventId, MyContext} from '../../lib/types.ts'; -import {getEventName} from '../../lib/all-events.js'; +import {getEventName} from '../../lib/all-events.ts'; import * as changesMenu from './changes/index.ts'; function getIdFromPath(path: string): EventId { diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index d459af45..24210d7c 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -4,7 +4,7 @@ import {html as format} from 'telegram-format'; import * as allEvents from '../../lib/all-events.ts'; import {getEventName} from '../../lib/all-events.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; -import {typedEntries, typedKeys} from '../../lib/javascript-helper.js'; +import {typedEntries, typedKeys} from '../../lib/javascript-helper.ts'; import type {MyContext} from '../../lib/types.ts'; import * as addMenu from './add.ts'; import * as detailsMenu from './details.ts'; diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index cd1e52bd..36956a8a 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -8,9 +8,12 @@ import { generateShortChangeText, } from '../lib/change-helper.ts'; import type { - Change, EventId, MyContext, NaiveDateTime, + Change, + EventId, + MyContext, + NaiveDateTime, } from '../lib/types.ts'; -import {typedEntries} from '../lib/javascript-helper.js'; +import {typedEntries} from '../lib/javascript-helper.ts'; export const bot = new Composer(); From 4f5be802c8466b3562d2f0dfc29bfab77365cb81 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 20:47:03 +0100 Subject: [PATCH 45/51] fixup! refactor: import from ts extension --- source/menu/events/details.ts | 2 +- source/parts/changes-inline.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/menu/events/details.ts b/source/menu/events/details.ts index 28308de8..1ec6a417 100644 --- a/source/menu/events/details.ts +++ b/source/menu/events/details.ts @@ -7,9 +7,9 @@ import { replyMenuToContext, } from 'grammy-inline-menu'; import {html as format} from 'telegram-format'; +import {getEventName} from '../../lib/all-events.ts'; import {backMainButtons} from '../../lib/inline-menu.ts'; import type {EventId, MyContext} from '../../lib/types.ts'; -import {getEventName} from '../../lib/all-events.ts'; import * as changesMenu from './changes/index.ts'; function getIdFromPath(path: string): EventId { diff --git a/source/parts/changes-inline.ts b/source/parts/changes-inline.ts index 36956a8a..bc4deb38 100644 --- a/source/parts/changes-inline.ts +++ b/source/parts/changes-inline.ts @@ -7,13 +7,13 @@ import { generateChangeTextHeader, generateShortChangeText, } from '../lib/change-helper.ts'; +import {typedEntries} from '../lib/javascript-helper.ts'; import type { Change, EventId, MyContext, NaiveDateTime, } from '../lib/types.ts'; -import {typedEntries} from '../lib/javascript-helper.ts'; export const bot = new Composer(); From 8a7f954fe8a6d80cf0552379d14042de6ecb174f Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 20:51:08 +0100 Subject: [PATCH 46/51] refactor: remove async from not anymore async methods --- source/menu/admin/user-quicklook.ts | 2 +- source/menu/events/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/menu/admin/user-quicklook.ts b/source/menu/admin/user-quicklook.ts index d3f89059..30e80af6 100644 --- a/source/menu/admin/user-quicklook.ts +++ b/source/menu/admin/user-quicklook.ts @@ -110,7 +110,7 @@ menu.select('u', { return Object.fromEntries(allChats.map(chat => [chat.id, nameOfUser(chat)])); }, isSet: (ctx, selected) => ctx.session.adminuserquicklook === Number(selected), - async set(ctx, selected) { + set(ctx, selected) { ctx.session.adminuserquicklook = Number(selected); return true; }, diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index 24210d7c..ea06c2dc 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -62,11 +62,11 @@ bot.use(detailsMenu.bot); menu.interact('remove-old', { text: '🗑 Entferne nicht mehr Existierende', - async hide(ctx) { + hide(ctx) { const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); return nonExisting.length === 0; }, - async do(ctx) { + do(ctx) { const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); for (const eventId of nonExisting) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete From ff51132a1ccb534fcc7779d0100ecebaf4653698 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 21:05:12 +0100 Subject: [PATCH 47/51] refactor: move more often changing argument to the right search pattern inside path, so path is the more stable one --- source/lib/all-events.ts | 6 +++--- source/menu/events/add.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index 8d5469cc..bbac17a1 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -82,11 +82,11 @@ export function nonExisting(ids: readonly EventId[]): readonly EventId[] { } export function find( + path: string[], pattern: string | RegExp | undefined, - startAt: string[], ): EventDirectory { if (!pattern) { - return getSubdirectory(startAt) ?? {}; + return getSubdirectory(path) ?? {}; } const regex = new RegExp(pattern, 'i'); @@ -104,7 +104,7 @@ export function find( } } - collect(getSubdirectory(startAt) ?? {}); + collect(getSubdirectory(path) ?? {}); return { events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), }; diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index e2ea6228..5ac870b4 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -40,8 +40,8 @@ export const menu = new MenuTemplate(ctx => { try { if (ctx.session.eventAdd.filter) { const filteredEvents = allEventsFind( - ctx.session.eventAdd.filter, ctx.session.eventAdd.path, + ctx.session.eventAdd.filter, ); const eventCount = Object.keys(filteredEvents.events ?? {}).length; text += `Mit deinem Filter konnte ich ${eventCount} passende Veranstaltungen finden.`; @@ -108,8 +108,8 @@ menu.choose('list', { try { ctx.session.eventAdd ??= {path: []}; const filteredEvents = allEventsFind( - ctx.session.eventAdd.filter, ctx.session.eventAdd.path, + ctx.session.eventAdd.filter, ); const subDirectoryItems = typedEntries(filteredEvents.subDirectories ?? {}).map(([name, directory], i) => { if (!directoryHasContent(directory)) { @@ -167,8 +167,8 @@ menu.choose('list', { } const filteredEvents = allEventsFind( - ctx.session.eventAdd.filter, ctx.session.eventAdd.path, + ctx.session.eventAdd.filter, ); const filteredSubDirectories = typedEntries(filteredEvents.subDirectories ?? {}); const chosenSubDirectory = filteredSubDirectories[index]?.[0]; From 2cdbf1f51d4dba00cb2bdd36e43f38a6440cb456 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 21:18:36 +0100 Subject: [PATCH 48/51] refactor(events): reuse exists for nonExisting --- source/lib/all-events.ts | 4 ---- source/menu/events/index.ts | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index bbac17a1..bf354971 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -77,10 +77,6 @@ export function exists(id: EventId): boolean { return id in namesOfEvents; } -export function nonExisting(ids: readonly EventId[]): readonly EventId[] { - return ids.filter(id => !(id in namesOfEvents)); -} - export function find( path: string[], pattern: string | RegExp | undefined, diff --git a/source/menu/events/index.ts b/source/menu/events/index.ts index ea06c2dc..1f7c490e 100644 --- a/source/menu/events/index.ts +++ b/source/menu/events/index.ts @@ -18,13 +18,13 @@ export const menu = new MenuTemplate(async ctx => { const eventIds = typedKeys(ctx.userconfig.mine.events); if (eventIds.length > 0) { - const nonExisting = new Set(allEvents.nonExisting(eventIds)); + const nonExisting = new Set(eventIds.filter(eventId => !allEvents.exists(eventId))); text += 'Du hast folgende Veranstaltungen im Kalender:'; text += '\n'; text += eventIds .map(eventId => { let line = '- '; - if (nonExisting.has(eventId)) { + if (!allEvents.exists(eventId)) { line += '⚠️ '; } @@ -62,15 +62,15 @@ bot.use(detailsMenu.bot); menu.interact('remove-old', { text: '🗑 Entferne nicht mehr Existierende', - hide(ctx) { - const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); - return nonExisting.length === 0; - }, + hide: ctx => + typedKeys(ctx.userconfig.mine.events).every(eventId => + allEvents.exists(eventId)), do(ctx) { - const nonExisting = allEvents.nonExisting(typedKeys(ctx.userconfig.mine.events)); - for (const eventId of nonExisting) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete ctx.userconfig.mine.events[eventId]; + for (const eventId of typedKeys(ctx.userconfig.mine.events)) { + if (!allEvents.exists(eventId)) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete ctx.userconfig.mine.events[eventId]; + } } return true; From 153770e523fa8b8156ebeaa2124e2eecdf6ff6d2 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 16 Nov 2025 21:44:15 +0100 Subject: [PATCH 49/51] refactor: inline single use constants The variable name does not provide any benefit --- source/lib/all-events.ts | 7 ++++--- source/lib/change-helper.ts | 5 ++++- source/menu/events/add.ts | 7 ++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index bf354971..bba34289 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -3,8 +3,6 @@ import {EVENT_FILES_DIR, pullEventFiles} from './git.ts'; import {typedEntries} from './javascript-helper.ts'; import type {EventDirectory, EventId} from './types.ts'; -const DIRECTORY_FILE = `${EVENT_FILES_DIR}/directory.json`; - let directory: EventDirectory = {}; let namesOfEvents: Readonly> = {}; @@ -14,7 +12,10 @@ console.log(new Date(), 'eventfiles loaded'); async function update() { await pullEventFiles(); - const directoryString = await readFile(DIRECTORY_FILE, 'utf8'); + const directoryString = await readFile( + `${EVENT_FILES_DIR}/directory.json`, + 'utf8', + ); directory = JSON.parse(directoryString) as EventDirectory; namesOfEvents = await generateMapping(); } diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index b8910ca4..1ead12e6 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -71,7 +71,10 @@ export function generateShortChangeText( export async function loadEvents(eventId: EventId): Promise { try { - const content = await readFile(`${EVENT_FILES_DIR}/events/${eventId}.json`, 'utf8'); + const content = await readFile( + `${EVENT_FILES_DIR}/events/${eventId}.json`, + 'utf8', + ); return JSON.parse(content) as EventEntry[]; } catch (error) { console.error('ERROR while loading events for change date picker', error); diff --git a/source/menu/events/add.ts b/source/menu/events/add.ts index 5ac870b4..2da3ecb7 100644 --- a/source/menu/events/add.ts +++ b/source/menu/events/add.ts @@ -19,9 +19,6 @@ import {BACK_BUTTON_TEXT} from '../../lib/inline-menu.ts'; import {typedEntries} from '../../lib/javascript-helper.ts'; import type {EventId, MyContext} from '../../lib/types.ts'; -const MAX_RESULT_ROWS = 10; -const RESULT_COLUMNS = 1; - export const bot = new Composer(); export const menu = new MenuTemplate(ctx => { ctx.session.eventAdd ??= {path: []}; @@ -102,8 +99,8 @@ menu.interact('filter-clear', { }); menu.choose('list', { - maxRows: MAX_RESULT_ROWS, - columns: RESULT_COLUMNS, + maxRows: 10, + columns: 1, choices(ctx) { try { ctx.session.eventAdd ??= {path: []}; From 6f98bb74041d78ff1f4ed7864b5d5474c24b061f Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 18 Nov 2025 00:19:50 +0100 Subject: [PATCH 50/51] refactor: move folder handling to their respective modules --- source/lib/all-events.ts | 18 +++++++++++------- source/lib/change-helper.ts | 19 +------------------ source/lib/git.ts | 19 ++++--------------- source/lib/mensa-meals.ts | 12 +++++++++--- source/menu/events/changes/add/index.ts | 6 ++---- 5 files changed, 27 insertions(+), 47 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index bba34289..fd462368 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -1,7 +1,9 @@ import {readFile} from 'node:fs/promises'; -import {EVENT_FILES_DIR, pullEventFiles} from './git.ts'; +import {pull} from './git.ts'; import {typedEntries} from './javascript-helper.ts'; -import type {EventDirectory, EventId} from './types.ts'; +import type {EventDirectory, EventEntry, EventId} from './types.ts'; + +const DIRECTORY = 'eventfiles'; let directory: EventDirectory = {}; let namesOfEvents: Readonly> = {}; @@ -11,11 +13,8 @@ await update(); console.log(new Date(), 'eventfiles loaded'); async function update() { - await pullEventFiles(); - const directoryString = await readFile( - `${EVENT_FILES_DIR}/directory.json`, - 'utf8', - ); + await pull(DIRECTORY, 'https://github.com/HAWHHCalendarBot/eventfiles.git'); + const directoryString = await readFile(`${DIRECTORY}/directory.json`, 'utf8'); directory = JSON.parse(directoryString) as EventDirectory; namesOfEvents = await generateMapping(); } @@ -106,3 +105,8 @@ export function find( events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))), }; } + +export async function loadEvents(eventId: EventId): Promise { + const content = await readFile(`${DIRECTORY}/events/${eventId}.json`, 'utf8'); + return JSON.parse(content) as EventEntry[]; +} diff --git a/source/lib/change-helper.ts b/source/lib/change-helper.ts index 1ead12e6..63fec56e 100644 --- a/source/lib/change-helper.ts +++ b/source/lib/change-helper.ts @@ -1,10 +1,6 @@ -import {readFile} from 'node:fs/promises'; import {html as format} from 'telegram-format'; -import type { - Change, EventEntry, EventId, NaiveDateTime, -} from './types.ts'; import {getEventName} from './all-events.ts'; -import {EVENT_FILES_DIR} from './git.ts'; +import type {Change, EventId, NaiveDateTime} from './types.ts'; export function generateChangeDescription(change: Change): string { let text = ''; @@ -68,16 +64,3 @@ export function generateShortChangeText( ): string { return `${getEventName(eventId)} ${date}`; } - -export async function loadEvents(eventId: EventId): Promise { - try { - const content = await readFile( - `${EVENT_FILES_DIR}/events/${eventId}.json`, - 'utf8', - ); - return JSON.parse(content) as EventEntry[]; - } catch (error) { - console.error('ERROR while loading events for change date picker', error); - return []; - } -} diff --git a/source/lib/git.ts b/source/lib/git.ts index 6024eeeb..55a6b2f7 100644 --- a/source/lib/git.ts +++ b/source/lib/git.ts @@ -2,26 +2,15 @@ import {exec} from 'node:child_process'; import {existsSync} from 'node:fs'; import {promisify} from 'node:util'; -export const EVENT_FILES_DIR = 'eventfiles'; -export const MENSA_DIR = 'mensa-data'; - const run = promisify(exec); -async function pull(directory: string, remoteUrl: string): Promise { +export async function pull( + directory: string, + remoteUrl: string, +): Promise { try { await (existsSync(`${directory}/.git`) ? run(`git -C ${directory} pull`) : run(`git clone -q --depth 1 ${remoteUrl} ${directory}`)); } catch {} } - -export async function pullEventFiles(): Promise { - await pull( - EVENT_FILES_DIR, - 'https://github.com/HAWHHCalendarBot/eventfiles.git', - ); -} - -export async function pullMensaData(): Promise { - await pull(MENSA_DIR, 'https://github.com/HAWHHCalendarBot/mensa-data.git'); -} diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index 9335c252..513e19b7 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -1,13 +1,19 @@ import {readdir, readFile} from 'node:fs/promises'; -import {MENSA_DIR, pullMensaData} from './git.ts'; +import {pull} from './git.ts'; import type {Meal} from './meal.ts'; +const DIRECTORY = 'mensa-data'; + setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes await pullMensaData(); console.log(new Date(), 'mensa-data loaded'); +async function pullMensaData(): Promise { + await pull(DIRECTORY, 'https://github.com/HAWHHCalendarBot/mensa-data.git'); +} + export async function getCanteenList(): Promise { - const found = await readdir(MENSA_DIR, {withFileTypes: true}); + const found = await readdir(DIRECTORY, {withFileTypes: true}); const dirs = found .filter(o => o.isDirectory()) .map(o => o.name) @@ -27,7 +33,7 @@ function getFilename( }); const m = month.toLocaleString(undefined, {minimumIntegerDigits: 2}); const d = day.toLocaleString(undefined, {minimumIntegerDigits: 2}); - return `${MENSA_DIR}/${mensa}/${y}/${m}/${d}.json`; + return `${DIRECTORY}/${mensa}/${y}/${m}/${d}.json`; } export async function getMealsOfDay( diff --git a/source/menu/events/changes/add/index.ts b/source/menu/events/changes/add/index.ts index bec882c5..3d1c9a08 100644 --- a/source/menu/events/changes/add/index.ts +++ b/source/menu/events/changes/add/index.ts @@ -7,10 +7,8 @@ import { MenuTemplate, replyMenuToContext, } from 'grammy-inline-menu'; -import { - generateChangeText, - loadEvents, -} from '../../../../lib/change-helper.ts'; +import {loadEvents} from '../../../../lib/all-events.ts'; +import {generateChangeText} from '../../../../lib/change-helper.ts'; import {typedKeys} from '../../../../lib/javascript-helper.ts'; import type { EventId, From d9348ab6c6fcbce4713d17e5765c13feff70c11d Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 18 Nov 2025 00:41:05 +0100 Subject: [PATCH 51/51] refactor: inline DIRECTORY constant Having let directory and const DIRECTORY was weird. Writing the directory name into the paths which are only used in exactly one module is relatively fine. --- source/lib/all-events.ts | 11 ++++++----- source/lib/mensa-meals.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/source/lib/all-events.ts b/source/lib/all-events.ts index fd462368..33b077e9 100644 --- a/source/lib/all-events.ts +++ b/source/lib/all-events.ts @@ -3,8 +3,6 @@ import {pull} from './git.ts'; import {typedEntries} from './javascript-helper.ts'; import type {EventDirectory, EventEntry, EventId} from './types.ts'; -const DIRECTORY = 'eventfiles'; - let directory: EventDirectory = {}; let namesOfEvents: Readonly> = {}; @@ -13,8 +11,11 @@ await update(); console.log(new Date(), 'eventfiles loaded'); async function update() { - await pull(DIRECTORY, 'https://github.com/HAWHHCalendarBot/eventfiles.git'); - const directoryString = await readFile(`${DIRECTORY}/directory.json`, 'utf8'); + await pull( + 'eventfiles', + 'https://github.com/HAWHHCalendarBot/eventfiles.git', + ); + const directoryString = await readFile('eventfiles/directory.json', 'utf8'); directory = JSON.parse(directoryString) as EventDirectory; namesOfEvents = await generateMapping(); } @@ -107,6 +108,6 @@ export function find( } export async function loadEvents(eventId: EventId): Promise { - const content = await readFile(`${DIRECTORY}/events/${eventId}.json`, 'utf8'); + const content = await readFile(`eventfiles/events/${eventId}.json`, 'utf8'); return JSON.parse(content) as EventEntry[]; } diff --git a/source/lib/mensa-meals.ts b/source/lib/mensa-meals.ts index 513e19b7..e66913fd 100644 --- a/source/lib/mensa-meals.ts +++ b/source/lib/mensa-meals.ts @@ -2,18 +2,19 @@ import {readdir, readFile} from 'node:fs/promises'; import {pull} from './git.ts'; import type {Meal} from './meal.ts'; -const DIRECTORY = 'mensa-data'; - setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes await pullMensaData(); console.log(new Date(), 'mensa-data loaded'); async function pullMensaData(): Promise { - await pull(DIRECTORY, 'https://github.com/HAWHHCalendarBot/mensa-data.git'); + await pull( + 'mensa-data', + 'https://github.com/HAWHHCalendarBot/mensa-data.git', + ); } export async function getCanteenList(): Promise { - const found = await readdir(DIRECTORY, {withFileTypes: true}); + const found = await readdir('mensa-data', {withFileTypes: true}); const dirs = found .filter(o => o.isDirectory()) .map(o => o.name) @@ -33,7 +34,7 @@ function getFilename( }); const m = month.toLocaleString(undefined, {minimumIntegerDigits: 2}); const d = day.toLocaleString(undefined, {minimumIntegerDigits: 2}); - return `${DIRECTORY}/${mensa}/${y}/${m}/${d}.json`; + return `mensa-data/${mensa}/${y}/${m}/${d}.json`; } export async function getMealsOfDay(