Skip to content

Commit c3598b9

Browse files
committed
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.
1 parent af6e1fb commit c3598b9

File tree

14 files changed

+312
-163
lines changed

14 files changed

+312
-163
lines changed

source/lib/all-events.ts

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,120 @@
11
import {readFile} from 'node:fs/promises';
2+
import type {EventDirectory, EventDirectoryEvent} from './types.ts';
23

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;
4+
async function getAll(): Promise<EventDirectory[]> {
5+
const data = await readFile('eventfiles/directory.json', 'utf8');
6+
return JSON.parse(data) as EventDirectory[];
77
}
88

99
export async function count(): Promise<number> {
10-
const allEvents = await getAll();
10+
const allEvents = (await find('', [], []));
1111
return allEvents.length;
1212
}
1313

14-
export async function exists(name: string): Promise<boolean> {
15-
const allEvents = await getAll();
16-
return allEvents.includes(name);
14+
export async function exists(id: string): Promise<EventDirectoryEvent | undefined> {
15+
const allDirectories = await getAll();
16+
17+
function traverse(directory: EventDirectory): EventDirectoryEvent | undefined {
18+
const event = directory.Events?.find(event => event.Id === id);
19+
if (event !== undefined) {
20+
return event;
21+
}
22+
23+
for (const subDirectory of directory.SubDirectories ?? []) {
24+
const eventInSubdirectory = traverse(subDirectory);
25+
if (eventInSubdirectory !== undefined) {
26+
return eventInSubdirectory;
27+
}
28+
}
29+
30+
return undefined;
31+
}
32+
33+
for (const subDirectory of allDirectories) {
34+
const event = traverse(subDirectory);
35+
if (event !== undefined) {
36+
return event;
37+
}
38+
}
39+
40+
return undefined;
1741
}
1842

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);
43+
export async function nonExisting(ids: readonly string[]): Promise<string[]> {
44+
const allEvents = await getAll();
45+
const result = [...ids];
46+
47+
function traverse(directory: EventDirectory) {
48+
for (const event of directory.Events ?? []) {
49+
const eventId = event.Id;
50+
if (result.includes(eventId)) {
51+
result.splice(result.indexOf(eventId), 1);
52+
}
53+
}
54+
55+
for (const subDirectory of directory.SubDirectories ?? []) {
56+
traverse(subDirectory);
2557
}
2658
}
2759

60+
for (const directory of allEvents) {
61+
traverse(directory);
62+
}
63+
2864
return result;
2965
}
3066

3167
export async function find(
68+
pattern: string | RegExp | undefined,
69+
ignoreIds: readonly string[] = [],
70+
startAt: number[] = [],
71+
): Promise<ReadonlyArray<EventDirectoryEvent | [EventDirectory, number[]]>> {
72+
if (pattern !== undefined) {
73+
const accumulator: EventDirectoryEvent[] = [];
74+
await _find(pattern, ignoreIds, await resolvePath(startAt), accumulator);
75+
return [...new Set(accumulator)].sort((a, b) =>
76+
a.Name.localeCompare(b.Name));
77+
}
78+
79+
const dir = await resolvePath(startAt);
80+
81+
return [
82+
...(dir.SubDirectories ?? []).map((dir, i) => [dir, [...startAt, i]] as [EventDirectory, number[]]),
83+
...(dir.Events ?? []).filter(event => !ignoreIds.includes(event.Id)),
84+
];
85+
}
86+
87+
async function _find(
3288
pattern: string | RegExp,
33-
ignore: readonly string[] = [],
34-
): Promise<readonly string[]> {
35-
const allEvents = await getAll();
36-
const regex = new RegExp(pattern, 'i');
37-
const filtered = allEvents.filter(event =>
38-
regex.test(event) && !ignore.includes(event));
39-
return filtered;
89+
ignoreIds: readonly string[],
90+
startAt: EventDirectory,
91+
accumulator: EventDirectoryEvent[],
92+
) {
93+
if (startAt?.Events !== undefined) {
94+
const regex = new RegExp(pattern, 'i');
95+
accumulator.push(...startAt?.Events
96+
.filter(event => regex.test(event.Name) && !ignoreIds.includes(event.Id)) ?? []);
97+
}
98+
99+
await Promise.all(startAt.SubDirectories
100+
?.map(async subDirectory => _find(pattern, ignoreIds, subDirectory, accumulator))
101+
?? []);
102+
}
103+
104+
export async function resolvePath(path: number[]): Promise<EventDirectory> {
105+
let Name = 'HAW Hamburg';
106+
let Events: EventDirectoryEvent[] | undefined;
107+
let SubDirectories: EventDirectory[] | undefined = await getAll();
108+
109+
for (const part of path) {
110+
if (SubDirectories === undefined || SubDirectories.length <= part) {
111+
throw new Error('Ungültiger Pfad');
112+
}
113+
114+
Name = SubDirectories[part]!.Name;
115+
Events = SubDirectories[part]!.Events;
116+
SubDirectories = SubDirectories[part]!.SubDirectories;
117+
}
118+
119+
return {Name, SubDirectories, Events};
40120
}

source/lib/calendar-helper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ export function getUrlFromContext(ctx: MyContext): string {
1515
return getUrl(ctx.from!.id, ctx.userconfig.mine);
1616
}
1717

18+
export function getEventNameFromContext(ctx: MyContext, eventId: string): string {
19+
const name = ctx.userconfig.mine.events[eventId]?.name;
20+
if (name === undefined) {
21+
throw new Error('Konnte Veranstaltungsnamen nicht finden');
22+
}
23+
24+
return name;
25+
}
26+
1827
export function formatDateToHumanReadable(isoDateString: string): string {
1928
const date = new Date(Date.parse(isoDateString + 'Z'));
2029
return date.toLocaleString('de-DE', {

source/lib/change-helper.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import {readFile} from 'node:fs/promises';
22
import {html as format} from 'telegram-format';
33
import {
44
formatDateToHumanReadable,
5+
getEventNameFromContext,
56
parseDateTimeToDate,
67
} from './calendar-helper.ts';
78
import type {
89
Change,
910
EventEntryFileContent,
1011
EventEntryInternal,
12+
MyContext,
1113
} from './types.ts';
1214

1315
export function generateChangeDescription(change: Change): string {
@@ -35,8 +37,8 @@ export function generateChangeDescription(change: Change): string {
3537
return text;
3638
}
3739

38-
export function generateChangeText(change: Change): string {
39-
let text = generateChangeTextHeader(change);
40+
export function generateChangeText(ctx: MyContext, change: Change): string {
41+
let text = generateChangeTextHeader(ctx, change);
4042

4143
if (Object.keys(change).length > 2) {
4244
text += '\nÄnderungen:\n';
@@ -46,11 +48,11 @@ export function generateChangeText(change: Change): string {
4648
return text;
4749
}
4850

49-
export function generateChangeTextHeader(change: Change): string {
51+
export function generateChangeTextHeader(ctx: MyContext, change: Change): string {
5052
let text = '';
5153
text += format.bold('Veranstaltungsänderung');
5254
text += '\n';
53-
text += format.bold(format.escape(change.name));
55+
text += format.bold(format.escape(getEventNameFromContext(ctx, change.eventId)));
5456
if (change.date) {
5557
text += ` ${formatDateToHumanReadable(change.date)}`;
5658
}
@@ -59,14 +61,13 @@ export function generateChangeTextHeader(change: Change): string {
5961
return text;
6062
}
6163

62-
export function generateShortChangeText(change: Change): string {
63-
return `${change.name} ${formatDateToHumanReadable(change.date)}`;
64+
export function generateShortChangeText(ctx: MyContext, change: Change): string {
65+
return `${getEventNameFromContext(ctx, change.eventId)} ${formatDateToHumanReadable(change.date)}`;
6466
}
6567

66-
export async function loadEvents(eventname: string): Promise<EventEntryInternal[]> {
68+
export async function loadEvents(eventId: string): Promise<EventEntryInternal[]> {
6769
try {
68-
const filename = eventname.replaceAll('/', '-');
69-
const content = await readFile(`eventfiles/${filename}.json`, 'utf8');
70+
const content = await readFile(`eventfiles/${eventId}.json`, 'utf8');
7071
const array = JSON.parse(content) as EventEntryFileContent[];
7172
const parsed = array.map((o): EventEntryInternal => ({
7273
...o,

source/lib/inline-menu-filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const DEFAULT_FILTER = '.+';
22

33
export function filterButtonText<T>(getCurrentFilterFunction: (ctx: T) => string | undefined): (ctx: T) => string {
44
return ctx => {
5-
let text = '🔎 Filter';
5+
let text = '🔎 Ab hier filtern';
66
const currentFilter = getCurrentFilterFunction(ctx);
77
if (currentFilter && currentFilter !== '.+') {
88
text += ': ' + currentFilter;

source/lib/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type Session = {
1919
adminuserquicklook?: number; // User ID
2020
adminuserquicklookfilter?: string;
2121
eventfilter?: string;
22+
eventPath?: number[]; // Path to the selected subdirectory
2223
generateChange?: Partial<Change>;
2324
page?: number;
2425
privacySection?: 'telegram' | 'persistent' | 'tmp';
@@ -44,13 +45,14 @@ export type Userconfig = {
4445
export type RemovedEventsDisplayStyle = 'cancelled' | 'removed' | 'emoji';
4546

4647
export type EventDetails = {
48+
name: string;
4749
alertMinutesBefore?: number;
4850
notes?: string;
4951
};
5052

5153
export type Change = {
5254
add?: true;
53-
name: string;
55+
eventId: string;
5456
date: string;
5557
remove?: true;
5658
namesuffix?: string;
@@ -83,7 +85,19 @@ export type MensaSettings = MealWishes & {
8385
showAdditives?: boolean;
8486
};
8587

88+
export type EventDirectory = {
89+
readonly Name: string;
90+
readonly SubDirectories: EventDirectory[] | undefined;
91+
readonly Events: EventDirectoryEvent[] | undefined;
92+
};
93+
94+
export type EventDirectoryEvent = {
95+
readonly Id: string;
96+
readonly Name: string;
97+
};
98+
8699
export type EventEntryFileContent = {
100+
readonly Id: string;
87101
readonly Name: string;
88102
readonly Location: string;
89103
readonly Description: string;
@@ -92,6 +106,7 @@ export type EventEntryFileContent = {
92106
};
93107

94108
export type EventEntryInternal = {
109+
readonly Id: string;
95110
readonly Name: string;
96111
readonly Location: string;
97112
readonly Description: string;

0 commit comments

Comments
 (0)