Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f7853e1
feature(events): extended event list to support directories, save eve…
Malex14 Oct 5, 2025
3dfe1d5
fixed module load hanging
Malex14 Oct 28, 2025
a841a1b
handle non-existing directories better
Malex14 Oct 28, 2025
6259f1d
added log messages when directory gets (re)loaded
Malex14 Oct 28, 2025
7c76653
use generic function to extract typed keys
Malex14 Oct 28, 2025
5f913e4
improved readability of string interpolation
Malex14 Oct 28, 2025
bc4f2fa
added typedEntries function
Malex14 Oct 28, 2025
04539f1
removed Events type
Malex14 Oct 28, 2025
43bb683
use fewer empty lines
Malex14 Oct 28, 2025
369aaec
Make records in EventDirectory readonly
Malex14 Oct 28, 2025
897c716
unified types of js helpers
Malex14 Oct 28, 2025
c0fd0fb
use even fewer blank lines
Malex14 Oct 28, 2025
851bab9
improved comment explaining session fields
Malex14 Oct 28, 2025
9a5d210
Specify encoding
Malex14 Nov 7, 2025
7fbaefe
Removed empty line
Malex14 Nov 7, 2025
ce6d364
cleanedup comment
Malex14 Nov 8, 2025
920ae7b
comment EventDirectory fields
Malex14 Nov 8, 2025
b4be207
made empty dir logic clearer
Malex14 Nov 8, 2025
532f45f
cleanedup session fields
Malex14 Nov 8, 2025
75ebc27
changed resolvePath to getSubdirectory and simplified logic
Malex14 Nov 8, 2025
6d40243
made eventPath in session non-optional
Malex14 Nov 8, 2025
9200175
added git-support for eventfiles
Malex14 Nov 8, 2025
e059872
fixed directory updating
Malex14 Nov 8, 2025
10f0a23
refactored filter button text logic
Malex14 Nov 8, 2025
4701568
refactor(admin): simplify filter logic
EdJoPaTo Nov 9, 2025
f4a441b
refactor(git): simplify imports
EdJoPaTo Nov 9, 2025
042540f
refactor: EventDirectory is already Readonly by itself
EdJoPaTo Nov 9, 2025
7e4d6dd
refactor(all-events): stricter type for namesOfEvents
EdJoPaTo Nov 9, 2025
b5bbf17
refactor: use typedKeys/typedEntries to prevent type issues
EdJoPaTo Nov 9, 2025
824a27e
Merge branch 'main' into malex-main
EdJoPaTo Nov 9, 2025
e1bbb1c
fix(events): correctly display non existing event id
EdJoPaTo Nov 9, 2025
22c0a88
refactor: use same method name as before
EdJoPaTo Nov 9, 2025
646aba0
store events in subdirectory
Malex14 Nov 12, 2025
1ae8294
fixup: store events in subdirectory
Malex14 Nov 12, 2025
f157e8f
make xo happy
Malex14 Nov 12, 2025
42256b9
chore: adapt init-debug-environment.sh
EdJoPaTo Nov 14, 2025
7652dd3
fixup! chore: adapt init-debug-environment.sh
EdJoPaTo Nov 14, 2025
a22f6c9
refactor: always Partial EventDirectory
EdJoPaTo Nov 14, 2025
c9e375c
feat(event-add): improve filter and path handling
EdJoPaTo Nov 16, 2025
e6d4af0
fix(event-add): prevent clicking non existing subdirectories
EdJoPaTo Nov 16, 2025
52ec85e
refactor(events): easier to read find function
EdJoPaTo Nov 16, 2025
b49dd5e
fix(events): sort event buttons by name
EdJoPaTo Nov 16, 2025
57425b1
refactor: use in keyword over typedKeys helper
EdJoPaTo Nov 16, 2025
479d14d
refactor: replace file watcher with interval
EdJoPaTo Nov 16, 2025
6483e2d
refactor: import from ts extension
EdJoPaTo Nov 16, 2025
4f5be80
fixup! refactor: import from ts extension
EdJoPaTo Nov 16, 2025
8a7f954
refactor: remove async from not anymore async methods
EdJoPaTo Nov 16, 2025
ff51132
refactor: move more often changing argument to the right
EdJoPaTo Nov 16, 2025
2cdbf1f
refactor(events): reuse exists for nonExisting
EdJoPaTo Nov 16, 2025
153770e
refactor: inline single use constants
EdJoPaTo Nov 16, 2025
6f98bb7
refactor: move folder handling to their respective modules
EdJoPaTo Nov 17, 2025
d9348ab
refactor: inline DIRECTORY constant
EdJoPaTo Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion init-debug-environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -e

# assumes other repos were cloned next to this repo (and executed)
ln -rfs ../downloader/eventfiles .
ln -rfs ../eventfiles .
ln -rfs ../mensa-data .

mkdir -p userconfig
125 changes: 99 additions & 26 deletions source/lib/all-events.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,113 @@
import {readFile} from 'node:fs/promises';
import {pull} from './git.ts';
import {typedEntries} from './javascript-helper.ts';
import type {EventDirectory, EventEntry, EventId} from './types.ts';

async function getAll(): Promise<string[]> {
const data = await readFile('eventfiles/all.txt', 'utf8');
const list = data.split('\n').filter(element => element !== '');
return list;
}
let directory: EventDirectory = {};
let namesOfEvents: Readonly<Record<EventId, string>> = {};

setInterval(async () => update(), 1000 * 60 * 30); // Every 30 minutes
await update();
console.log(new Date(), 'eventfiles loaded');

export async function count(): Promise<number> {
const allEvents = await getAll();
return allEvents.length;
async function update() {
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();
}

export async function exists(name: string): Promise<boolean> {
const allEvents = await getAll();
return allEvents.includes(name);
async function generateMapping(): Promise<Readonly<Record<EventId, string>>> {
const namesOfEvents: Record<EventId, string> = {};

function collect(directory: EventDirectory) {
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<string[]> {
const allEvents = new Set(await getAll());
const result: string[] = [];
for (const event of names) {
if (!allEvents.has(event)) {
result.push(event);
function getSubdirectory(path: string[]): EventDirectory | undefined {
let resolvedDirectory = directory;

for (const part of path) {
const subDirectory = resolvedDirectory.subDirectories?.[part];
if (subDirectory === undefined) {
return undefined;
}

resolvedDirectory = subDirectory;
}

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;
}

return result;
const directory = getSubdirectory(path);
return Boolean(directory && directoryHasContent(directory));
}

export async function find(
pattern: string | RegExp,
ignore: readonly string[] = [],
): Promise<readonly string[]> {
const allEvents = await getAll();
export function getEventName(id: EventId): string {
return namesOfEvents[id] ?? id;
}

export function count(): number {
return Object.keys(namesOfEvents).length;
}

export function exists(id: EventId): boolean {
return id in namesOfEvents;
}

export function find(
path: string[],
pattern: string | RegExp | undefined,
): EventDirectory {
if (!pattern) {
return getSubdirectory(path) ?? {};
}

const regex = new RegExp(pattern, 'i');
const filtered = allEvents.filter(event =>
regex.test(event) && !ignore.includes(event));
return filtered;
const accumulator: Record<EventId, string> = {};

function collect(directory: EventDirectory) {
for (const [eventId, name] of typedEntries(directory.events ?? {})) {
if (regex.test(name)) {
accumulator[eventId] = name;
}
}

for (const subDirectory of Object.values(directory.subDirectories ?? {})) {
collect(subDirectory);
}
}

collect(getSubdirectory(path) ?? {});
return {
events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))),
};
}

export async function loadEvents(eventId: EventId): Promise<EventEntry[]> {
const content = await readFile(`eventfiles/events/${eventId}.json`, 'utf8');
return JSON.parse(content) as EventEntry[];
}
27 changes: 8 additions & 19 deletions source/lib/change-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {readFile} from 'node:fs/promises';
import {html as format} from 'telegram-format';
import type {Change, EventEntry, NaiveDateTime} from './types.ts';
import {getEventName} from './all-events.ts';
import type {Change, EventId, NaiveDateTime} from './types.ts';

export function generateChangeDescription(change: Change): string {
let text = '';
Expand Down Expand Up @@ -28,11 +28,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 > 0) {
text += '\nÄnderungen:\n';
Expand All @@ -43,13 +43,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}`;
}
Expand All @@ -59,19 +59,8 @@ export function generateChangeTextHeader(
}

export function generateShortChangeText(
name: string,
eventId: EventId,
date: NaiveDateTime,
): string {
return `${name} ${date}`;
}

export async function loadEvents(eventname: string): Promise<EventEntry[]> {
try {
const filename = eventname.replaceAll('/', '-');
const content = await readFile(`eventfiles/${filename}.json`, 'utf8');
return JSON.parse(content) as EventEntry[];
} catch (error) {
console.error('ERROR while loading events for change date picker', error);
return [];
}
return `${getEventName(eventId)} ${date}`;
}
16 changes: 16 additions & 0 deletions source/lib/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {exec} from 'node:child_process';
import {existsSync} from 'node:fs';
import {promisify} from 'node:util';

const run = promisify(exec);

export async function pull(
directory: string,
remoteUrl: string,
): Promise<void> {
try {
await (existsSync(`${directory}/.git`)
? run(`git -C ${directory} pull`)
: run(`git clone -q --depth 1 ${remoteUrl} ${directory}`));
} catch {}
}
11 changes: 11 additions & 0 deletions source/lib/javascript-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function typedKeys<K extends keyof any>(record: Readonly<Partial<Record<K, unknown>>>): K[] {
return Object.keys(record) as K[];
}

export function typedEntries<K extends keyof any, V>(record: Readonly<Partial<Record<K, V>>>): Array<[K, V]> {
if (!record) {
return [];
}

return (Object.entries(record) as unknown[]) as Array<[K, V]>;
}
13 changes: 0 additions & 13 deletions source/lib/mensa-git.ts

This file was deleted.

3 changes: 2 additions & 1 deletion source/lib/mensa-helper.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand Down
12 changes: 12 additions & 0 deletions source/lib/mensa-meals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import {readdir, readFile} from 'node:fs/promises';
import {pull} from './git.ts';
import type {Meal} from './meal.ts';

setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes
await pullMensaData();
console.log(new Date(), 'mensa-data loaded');

async function pullMensaData(): Promise<void> {
await pull(
'mensa-data',
'https://github.com/HAWHHCalendarBot/mensa-data.git',
);
}

export async function getCanteenList(): Promise<string[]> {
const found = await readdir('mensa-data', {withFileTypes: true});
const dirs = found
Expand Down
25 changes: 20 additions & 5 deletions source/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ 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;
generateChangeName?: string;
eventAdd?: {
filter?: string;
/** Currently selected subdirectory */
path: string[];
};
generateChangeEventId?: EventId;
generateChangeDate?: NaiveDateTime;
generateChange?: Change;
page?: number;
Expand All @@ -37,7 +43,7 @@ export type Session = {
export type Userconfig = {
readonly admin?: true;
calendarfileSuffix: string;
events: Record<string, EventDetails>;
events: Record<EventId, EventDetails>;
mensa: MensaSettings;
removedEvents?: RemovedEventsDisplayStyle;
};
Expand Down Expand Up @@ -82,6 +88,15 @@ export type MensaSettings = MealWishes & {
showAdditives?: boolean;
};

export type EventId = `${number}_${number | string}`;

export type EventDirectory = {
/** Maps the directory name to its content */
readonly subDirectories?: Readonly<Record<string, EventDirectory>>;
/** Maps `EventId` to the human-readable name */
readonly events?: Readonly<Record<EventId, string>>;
};

export type EventEntry = {
readonly name: string;
readonly location: string;
Expand Down
2 changes: 1 addition & 1 deletion source/menu/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const menu = new MenuTemplate<MyContext>(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/',
Expand Down
2 changes: 1 addition & 1 deletion source/menu/admin/user-quicklook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
Loading