Skip to content

Commit 41fee35

Browse files
Malex14EdJoPaTo
andauthored
Orderunterstützung für Veranstaltungsauswahl, Integration von myHAW Scraper (#177)
Signed-off-by: Malex14 <[email protected]> Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
1 parent 79a0729 commit 41fee35

File tree

19 files changed

+444
-245
lines changed

19 files changed

+444
-245
lines changed

init-debug-environment.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
set -e
33

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

88
mkdir -p userconfig

source/lib/all-events.ts

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,113 @@
11
import {readFile} from 'node:fs/promises';
2+
import {pull} from './git.ts';
3+
import {typedEntries} from './javascript-helper.ts';
4+
import type {EventDirectory, EventEntry, EventId} from './types.ts';
25

3-
async function getAll(): Promise<string[]> {
4-
const data = await readFile('eventfiles/all.txt', 'utf8');
5-
const list = data.split('\n').filter(element => element !== '');
6-
return list;
7-
}
6+
let directory: EventDirectory = {};
7+
let namesOfEvents: Readonly<Record<EventId, string>> = {};
8+
9+
setInterval(async () => update(), 1000 * 60 * 30); // Every 30 minutes
10+
await update();
11+
console.log(new Date(), 'eventfiles loaded');
812

9-
export async function count(): Promise<number> {
10-
const allEvents = await getAll();
11-
return allEvents.length;
13+
async function update() {
14+
await pull(
15+
'eventfiles',
16+
'https://github.com/HAWHHCalendarBot/eventfiles.git',
17+
);
18+
const directoryString = await readFile('eventfiles/directory.json', 'utf8');
19+
directory = JSON.parse(directoryString) as EventDirectory;
20+
namesOfEvents = await generateMapping();
1221
}
1322

14-
export async function exists(name: string): Promise<boolean> {
15-
const allEvents = await getAll();
16-
return allEvents.includes(name);
23+
async function generateMapping(): Promise<Readonly<Record<EventId, string>>> {
24+
const namesOfEvents: Record<EventId, string> = {};
25+
26+
function collect(directory: EventDirectory) {
27+
for (const subDirectory of Object.values(directory.subDirectories ?? {})) {
28+
collect(subDirectory);
29+
}
30+
31+
Object.assign(namesOfEvents, directory.events ?? {});
32+
}
33+
34+
collect(directory);
35+
return namesOfEvents;
1736
}
1837

19-
export async function nonExisting(names: readonly string[]): Promise<string[]> {
20-
const allEvents = new Set(await getAll());
21-
const result: string[] = [];
22-
for (const event of names) {
23-
if (!allEvents.has(event)) {
24-
result.push(event);
38+
function getSubdirectory(path: string[]): EventDirectory | undefined {
39+
let resolvedDirectory = directory;
40+
41+
for (const part of path) {
42+
const subDirectory = resolvedDirectory.subDirectories?.[part];
43+
if (subDirectory === undefined) {
44+
return undefined;
2545
}
46+
47+
resolvedDirectory = subDirectory;
48+
}
49+
50+
return resolvedDirectory;
51+
}
52+
53+
export function directoryHasContent(directory: EventDirectory): boolean {
54+
const events = Object.keys(directory.events ?? {}).length;
55+
const subDirectories = Object.keys(directory.subDirectories ?? {}).length;
56+
return events > 0 || subDirectories > 0;
57+
}
58+
59+
export function directoryExists(path: string[]): boolean {
60+
if (path.length === 0) {
61+
// Toplevel always exists
62+
return true;
2663
}
2764

28-
return result;
65+
const directory = getSubdirectory(path);
66+
return Boolean(directory && directoryHasContent(directory));
2967
}
3068

31-
export async function find(
32-
pattern: string | RegExp,
33-
ignore: readonly string[] = [],
34-
): Promise<readonly string[]> {
35-
const allEvents = await getAll();
69+
export function getEventName(id: EventId): string {
70+
return namesOfEvents[id] ?? id;
71+
}
72+
73+
export function count(): number {
74+
return Object.keys(namesOfEvents).length;
75+
}
76+
77+
export function exists(id: EventId): boolean {
78+
return id in namesOfEvents;
79+
}
80+
81+
export function find(
82+
path: string[],
83+
pattern: string | RegExp | undefined,
84+
): EventDirectory {
85+
if (!pattern) {
86+
return getSubdirectory(path) ?? {};
87+
}
88+
3689
const regex = new RegExp(pattern, 'i');
37-
const filtered = allEvents.filter(event =>
38-
regex.test(event) && !ignore.includes(event));
39-
return filtered;
90+
const accumulator: Record<EventId, string> = {};
91+
92+
function collect(directory: EventDirectory) {
93+
for (const [eventId, name] of typedEntries(directory.events ?? {})) {
94+
if (regex.test(name)) {
95+
accumulator[eventId] = name;
96+
}
97+
}
98+
99+
for (const subDirectory of Object.values(directory.subDirectories ?? {})) {
100+
collect(subDirectory);
101+
}
102+
}
103+
104+
collect(getSubdirectory(path) ?? {});
105+
return {
106+
events: Object.fromEntries(typedEntries(accumulator).sort((a, b) => a[1].localeCompare(b[1]))),
107+
};
108+
}
109+
110+
export async function loadEvents(eventId: EventId): Promise<EventEntry[]> {
111+
const content = await readFile(`eventfiles/events/${eventId}.json`, 'utf8');
112+
return JSON.parse(content) as EventEntry[];
40113
}

source/lib/change-helper.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {readFile} from 'node:fs/promises';
21
import {html as format} from 'telegram-format';
3-
import type {Change, EventEntry, NaiveDateTime} from './types.ts';
2+
import {getEventName} from './all-events.ts';
3+
import type {Change, EventId, NaiveDateTime} from './types.ts';
44

55
export function generateChangeDescription(change: Change): string {
66
let text = '';
@@ -28,11 +28,11 @@ export function generateChangeDescription(change: Change): string {
2828
}
2929

3030
export function generateChangeText(
31-
name: string,
31+
eventId: EventId,
3232
date: NaiveDateTime | undefined,
3333
change: Change,
3434
): string {
35-
let text = generateChangeTextHeader(name, date);
35+
let text = generateChangeTextHeader(eventId, date);
3636

3737
if (Object.keys(change).length > 0) {
3838
text += '\nÄnderungen:\n';
@@ -43,13 +43,13 @@ export function generateChangeText(
4343
}
4444

4545
export function generateChangeTextHeader(
46-
name: string,
46+
eventId: EventId,
4747
date: NaiveDateTime | undefined,
4848
): string {
4949
let text = '';
5050
text += format.bold('Veranstaltungsänderung');
5151
text += '\n';
52-
text += format.bold(format.escape(name));
52+
text += format.bold(format.escape(getEventName(eventId)));
5353
if (date) {
5454
text += ` ${date}`;
5555
}
@@ -59,19 +59,8 @@ export function generateChangeTextHeader(
5959
}
6060

6161
export function generateShortChangeText(
62-
name: string,
62+
eventId: EventId,
6363
date: NaiveDateTime,
6464
): string {
65-
return `${name} ${date}`;
66-
}
67-
68-
export async function loadEvents(eventname: string): Promise<EventEntry[]> {
69-
try {
70-
const filename = eventname.replaceAll('/', '-');
71-
const content = await readFile(`eventfiles/${filename}.json`, 'utf8');
72-
return JSON.parse(content) as EventEntry[];
73-
} catch (error) {
74-
console.error('ERROR while loading events for change date picker', error);
75-
return [];
76-
}
65+
return `${getEventName(eventId)} ${date}`;
7766
}

source/lib/git.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {exec} from 'node:child_process';
2+
import {existsSync} from 'node:fs';
3+
import {promisify} from 'node:util';
4+
5+
const run = promisify(exec);
6+
7+
export async function pull(
8+
directory: string,
9+
remoteUrl: string,
10+
): Promise<void> {
11+
try {
12+
await (existsSync(`${directory}/.git`)
13+
? run(`git -C ${directory} pull`)
14+
: run(`git clone -q --depth 1 ${remoteUrl} ${directory}`));
15+
} catch {}
16+
}

source/lib/javascript-helper.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function typedKeys<K extends keyof any>(record: Readonly<Partial<Record<K, unknown>>>): K[] {
2+
return Object.keys(record) as K[];
3+
}
4+
5+
export function typedEntries<K extends keyof any, V>(record: Readonly<Partial<Record<K, V>>>): Array<[K, V]> {
6+
if (!record) {
7+
return [];
8+
}
9+
10+
return (Object.entries(record) as unknown[]) as Array<[K, V]>;
11+
}

source/lib/mensa-git.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

source/lib/mensa-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {arrayFilterUnique} from 'array-filter-unique';
2+
import {typedEntries} from './javascript-helper.ts';
23
import type {Meal} from './meal.ts';
34
import type {MealWishes, MensaPriceClass, MensaSettings} from './types.ts';
45

@@ -74,7 +75,7 @@ export function mealNameToHtml(
7475
export function mealAdditivesToHtml(meals: readonly Meal[]): string {
7576
return meals
7677
.flatMap(meal =>
77-
Object.entries(meal.Additives).map(([short, full]) => `${short}: ${full}`))
78+
typedEntries(meal.Additives).map(([short, full]) => `${short}: ${full}`))
7879
.sort()
7980
.filter(arrayFilterUnique())
8081
.join('\n');

source/lib/mensa-meals.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import {readdir, readFile} from 'node:fs/promises';
2+
import {pull} from './git.ts';
23
import type {Meal} from './meal.ts';
34

5+
setInterval(async () => pullMensaData(), 1000 * 60 * 30); // Every 30 minutes
6+
await pullMensaData();
7+
console.log(new Date(), 'mensa-data loaded');
8+
9+
async function pullMensaData(): Promise<void> {
10+
await pull(
11+
'mensa-data',
12+
'https://github.com/HAWHHCalendarBot/mensa-data.git',
13+
);
14+
}
15+
416
export async function getCanteenList(): Promise<string[]> {
517
const found = await readdir('mensa-data', {withFileTypes: true});
618
const dirs = found

source/lib/types.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ export type MyContext =
1919
& ContextFlavour;
2020

2121
export type Session = {
22-
adminBroadcast?: number; // Message ID
23-
adminuserquicklook?: number; // User ID
22+
/** Message ID */
23+
adminBroadcast?: number;
24+
/** User ID */
25+
adminuserquicklook?: number;
2426
adminuserquicklookfilter?: string;
25-
eventfilter?: string;
26-
generateChangeName?: string;
27+
eventAdd?: {
28+
filter?: string;
29+
/** Currently selected subdirectory */
30+
path: string[];
31+
};
32+
generateChangeEventId?: EventId;
2733
generateChangeDate?: NaiveDateTime;
2834
generateChange?: Change;
2935
page?: number;
@@ -37,7 +43,7 @@ export type Session = {
3743
export type Userconfig = {
3844
readonly admin?: true;
3945
calendarfileSuffix: string;
40-
events: Record<string, EventDetails>;
46+
events: Record<EventId, EventDetails>;
4147
mensa: MensaSettings;
4248
removedEvents?: RemovedEventsDisplayStyle;
4349
};
@@ -82,6 +88,15 @@ export type MensaSettings = MealWishes & {
8288
showAdditives?: boolean;
8389
};
8490

91+
export type EventId = `${number}_${number | string}`;
92+
93+
export type EventDirectory = {
94+
/** Maps the directory name to its content */
95+
readonly subDirectories?: Readonly<Record<string, EventDirectory>>;
96+
/** Maps `EventId` to the human-readable name */
97+
readonly events?: Readonly<Record<EventId, string>>;
98+
};
99+
85100
export type EventEntry = {
86101
readonly name: string;
87102
readonly location: string;

source/menu/about.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const menu = new MenuTemplate<MyContext>(async ctx => {
1010

1111
const canteens = await getCanteenList();
1212
const canteenCount = canteens.length;
13-
const eventCount = await allEvents.count();
13+
const eventCount = allEvents.count();
1414

1515
const websiteLink = format.url(
1616
'hawhh.de/calendarbot/',

0 commit comments

Comments
 (0)