🎉 Mostly working matrix bot

(timetable group handling missing)
This commit is contained in:
2023-06-23 18:56:59 +02:00
commit 9a4a4a2f08
5 changed files with 844 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
.env
data.json

280
index.js Normal file
View File

@ -0,0 +1,280 @@
import * as sdk from "matrix-js-sdk";
import fetch from "node-fetch";
import "dotenv/config";
import { JsonDB, Config } from "node-json-db";
import { TimetableClient } from "./timetable.js";
const db = new JsonDB(new Config("data", true, true, "/"));
const timetable = new TimetableClient(
process.env.TIMETABLE_ENDPOINT,
process.env.TIMETABLE_TOKEN
);
await db.push("/rooms", {}, false);
// Login
const userId = process.env.MATRIX_USER;
const client = sdk.createClient({
baseUrl: process.env.MATRIX_URL,
accessToken: process.env.MATRIX_TOKEN,
userId: userId,
fetchFn: fetch,
});
client.once("sync", function (state) {
if (state === "PREPARED") {
console.log("sync finished");
}
});
// Auto-join invited rooms
client.on("RoomMember.membership", function (_, member) {
if (member.membership === "invite" && member.userId === userId) {
client
.joinRoom(member.roomId)
.then(function () {
console.log("Auto-joined %s", member.roomId);
})
.catch(() => {
client.leave(member.roomId);
});
}
});
function formatUpdateMessage(update) {
let message = "";
if (update.type == "addition") message += "🆕";
else if (update.type == "deletion") message += "🗑";
else if (update.type == "change") message += "✏";
if (update.type == "addition" || update.type == "deletion") {
message += " ";
if (update.change.type == "cancellation") message += "🚫";
else message += "📑";
}
message += ` <b>${update.lesson}th Lesson</b><br><ul>\n`;
if (update.type == "addition" || update.type == "deletion") {
message += `\n<li><b>Subject:</b> ${
update.change.change.subject || "Unknown"
}`;
if (update.change.change.teacher) {
message += `</li>\n<li><b>Teacher:</b> <del>${
update.change.teacher || "Unknown"
}</del> ${update.change.change.teacher}`;
} else {
message += `</li>\n<li><b>Teacher:</b> ${
update.change.teacher || "Unknown"
}`;
}
message += `</li>\n<li><b>Room:</b> ${
update.change.change.room || "Unknown"
}`;
if (update.change.notes) {
message += `</li>\n<li><b>Notes:</b></li>\n${update.change.notes}`;
}
} else {
for (const change of Object.keys(update.change)) {
const changeValue = update.change[change];
message += `<br>\n${
change.charAt(0).toUpperCase() + change.slice(1)
}: <del>${changeValue.before}</del> ${changeValue.after}`;
}
}
return message;
}
// Check for timetable updates
setTimeout(async () => {
const history = await timetable.getUpdates();
const lastCheck = await db.getObjectDefault("/lastTimetableUpdate", 0);
const updates = history.filter((entry) => entry.updatedAt > lastCheck);
await db.push("/lastTimetableUpdate", new Date().getTime());
const rooms = await db.getData("/rooms");
for (const update of updates) {
for (const roomId of Object.keys(rooms)) {
const room = rooms[roomId];
if (!update.class.includes(room.filter)) continue;
console.log(update);
let message = formatUpdateMessage(update);
client.sendHtmlMessage(roomId, plainText(message), message);
}
}
}, 1000);
// Handle events
client.on("Room.timeline", async function (event, room) {
if (event.getLocalAge() > 5000) return;
if (event.event.sender == userId) return;
try {
if (event.getType() === "m.room.message") {
await handleMessage(event, room.roomId);
} else if (
event.getType() === "m.reaction" ||
event.getType() === "m.room.redaction"
) {
await handleReaction(event, room.roomId);
}
} catch (e) {
client.sendTextMessage(
room.roomId,
"⛔ Your command failed executing! Please try again"
);
}
});
function plainText(message) {
return message.replace("<li>", "- ").replace(/<[^>]+>/g, "");
}
function sendReaction(room, toEvent, text) {
client.sendEvent(room, "m.reaction", {
"m.relates_to": {
rel_type: "m.annotation",
event_id: toEvent,
key: text,
},
});
}
async function handleMessage(event, room) {
const body = event.event.content.body;
const sender = event.event.sender;
const userState = await db.getObjectDefault(`/rooms/${room}/state`);
if (!userState) {
if (body == "help") {
// Send help message
const helpMessage = `<b>Available commands</b>:<ul>
<li> <code>help</code>: List available commands </li>
<li> <code>info</code>: Get information about the current room's configuration </li>
<li> <code>filter [class (eg. 10c)]</code>: Set the class to filter for. Use only filter available in the timetable v2 app.</li>
<li> <code>timetable</code>: Set your timetable</li>
<li> <code>groups</code>: Configure your timetable groups</li>
<li> <code>reset</code>: Reset the configuration for this room</li>
`;
client.sendHtmlMessage(room, plainText(helpMessage), helpMessage);
} else if (body == "info") {
// Send room information
const roomData = await db.getObjectDefault(`/rooms/${room}`);
if (!roomData) {
client.sendTextMessage(
room,
"⛔ This room has no configuration data yet"
);
return;
}
const response = `<b>Current Room Configuration</b>:
<br>Filter: <code>${roomData.filter || "None"}</code>
<br>Timetable ID: <code>${roomData.timetable || "Not set"}</code>
<br>Timetable Groups: <code>${(roomData.groups || []).join(", ")}</code>`;
client.sendHtmlMessage(room, plainText(response), response);
} else if (body.startsWith("filter ")) {
// Set filtering
const filter = body.split(" ")[1].toLowerCase();
const response = `👍 Now filtering for <code>${filter}</code>`;
await db.push(`/rooms/${room}/filter`, filter);
client.sendHtmlMessage(room, plainText(response), response);
} else if (body == "timetable") {
// Timetable
if (!(await db.getObjectDefault(`/rooms/${room}/filter`))) {
client.sendTextMessage(room, "⛔ You need to configure a filter first");
return;
}
// Query timetables
const timetables = await timetable.getTimetables(
await db.getData(`/rooms/${room}/filter`)
);
await db.push(`/rooms/${room}/state`, "timetable");
let response = `🛠 Which timetable do you want to use?<ul>\n`;
// Build response message
for (const timetable of timetables.timetables) {
response += `<li><code>${timetable.id}</code>: ${timetable.title}</li>\n`;
}
client.sendHtmlMessage(room, plainText(response), response);
} else if (body == "groups") {
if (!(await db.getObjectDefault(`/rooms/${room}/timetable`))) {
client.sendTextMessage(room, "⛔ You need to select a timetable first");
return;
}
// Timetable groups
await db.push(`/rooms/${room}/state`, "groups");
await db.push(`/rooms/${room}/stateData/sender`, event.sender.userId);
const groups = await timetable.getGroups(
await db.getData(`/rooms/${room}/filter`),
await db.getData(`/rooms/${room}/timetable`)
);
let response = `🛠 Set your timetable groups by reacting with the correct group. React with ✅ to confirm your selection.<ul>`;
const message = await client.sendHtmlMessage(
room,
plainText(response),
response
);
for (const group of groups) {
sendReaction(room, message.event_id, group);
}
sendReaction(room, message.event_id, "✅");
} else if (body == "reset") {
// Reset room
await db.delete(`/rooms/${room}`);
const response = "⚠ The configuration for this room was reset!";
client.sendHtmlMessage(room, plainText(response), response);
} else {
const response =
"Unknown command! Type <code>help</code> for a list of valid commands";
client.sendHtmlMessage(room, plainText(response), response);
}
} else {
// If user is in setup mode
if (body == "abort") {
await db.delete(`/rooms/${room}/state`);
const response = `❌ Aborted current action`;
client.sendHtmlMessage(room, plainText(response), response);
} else if (userState == "timetable") {
const timetable = parseInt(body);
if (!timetable) {
client.sendTextMessage(room, "Please enter the timetable id!");
return;
}
await db.push(`/rooms/${room}/timetable`, timetable);
await db.delete(`/rooms/${room}/state`);
const response = `✅ Now using timetable <code>${timetable}</code>`;
client.sendHtmlMessage(room, plainText(response), response);
} else {
const response =
"⚠ You are currently in setup mode. Complete the setup or type <code>abort</code> to execute other commands.";
client.sendHtmlMessage(room, plainText(response), response);
}
}
}
async function handleReaction(event, room) {
const state = await db.getObjectDefault(`/rooms/${room}/state`);
if (state == "groups") {
const stateData = await db.getData(`/rooms/${room}/stateData`);
if (event.sender.userId != stateData.sender) return;
if (event.event.content["m.relates_to"].key == "✅") {
await db.push(`/rooms/${room}/groups`, Object.values(stateData.groups));
await db.delete(`/rooms/${room}/stateData`);
await db.delete(`/rooms/${room}/state`);
const response = `✅ Now using timetable groups <code>${Object.values(
stateData.groups
).join(", ")}</code>`;
client.sendHtmlMessage(room, plainText(response), response);
} else if (event.getType() === "m.reaction") {
await db.push(
`/rooms/${room}/stateData/groups/${event.event.event_id}`,
event.event.content["m.relates_to"].key
);
} else {
await db.delete(`/rooms/${room}/stateData/groups/${event.event.redacts}`);
}
}
}
client.startClient(0);

469
package-lock.json generated Normal file
View File

@ -0,0 +1,469 @@
{
"name": "timetable-v2-notify",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "timetable-v2-notify",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"dotenv": "^16.3.1",
"matrix-js-sdk": "^26.1.0",
"node-fetch": "^3.3.1",
"node-json-db": "^2.2.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@matrix-org/matrix-sdk-crypto-js": {
"version": "0.1.0-alpha.11",
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.11.tgz",
"integrity": "sha512-HD3rskPkqrUUSaKzGLg97k/bN+OZrkcX7ODB/pNBs/jqq+/A0wDKqsszJotzFwsQcDPpWn78BmMyvBo4tLxKjw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
"node_modules/another-json": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg=="
},
"node_modules/base-x": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
},
"node_modules/bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"dependencies": {
"base-x": "^4.0.0"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"engines": {
"node": ">= 12"
}
},
"node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/loglevel": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
"integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/matrix-events-sdk": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz",
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
},
"node_modules/matrix-js-sdk": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-26.1.0.tgz",
"integrity": "sha512-2isNpMFCR0hV8wNub4GJ/+8lIbz/jag12gf1P8rU++PfLFpeObooIDI3nnffBGLV5biZ3iLkaLOKAl33dSxchA==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.3.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/matrix-widget-api": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz",
"integrity": "sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==",
"dependencies": {
"@types/events": "^3.0.0",
"events": "^3.2.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz",
"integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-json-db": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-2.2.0.tgz",
"integrity": "sha512-GBuXPjItZ+XivMxR7bhSjVeXKiHZKRN/HbvqrwVemV6V5atcnIIZbg2J2vJHE6RYHydtPCJoUy05YaK4wQO0TA==",
"dependencies": {
"rwlock": "^5.0.0"
}
},
"node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/rwlock": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/rwlock/-/rwlock-5.0.0.tgz",
"integrity": "sha512-XgzRqLMfCcm9QfZuPav9cV3Xin5TRcIlp4X/SH3CvB+x5D2AakdlEepfJKDd8ByncvfpcxNWdRZVUl38PS6ZJg=="
},
"node_modules/sdp-transform": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/unhomoglyph": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"engines": {
"node": ">= 8"
}
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"@matrix-org/matrix-sdk-crypto-js": {
"version": "0.1.0-alpha.11",
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.11.tgz",
"integrity": "sha512-HD3rskPkqrUUSaKzGLg97k/bN+OZrkcX7ODB/pNBs/jqq+/A0wDKqsszJotzFwsQcDPpWn78BmMyvBo4tLxKjw=="
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
"another-json": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg=="
},
"base-x": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
},
"bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"requires": {
"base-x": "^4.0.0"
}
},
"content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
},
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"loglevel": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
"integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg=="
},
"matrix-events-sdk": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz",
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
},
"matrix-js-sdk": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-26.1.0.tgz",
"integrity": "sha512-2isNpMFCR0hV8wNub4GJ/+8lIbz/jag12gf1P8rU++PfLFpeObooIDI3nnffBGLV5biZ3iLkaLOKAl33dSxchA==",
"requires": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.3.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
}
},
"matrix-widget-api": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz",
"integrity": "sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==",
"requires": {
"@types/events": "^3.0.0",
"events": "^3.2.0"
}
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz",
"integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==",
"requires": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
},
"node-json-db": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-2.2.0.tgz",
"integrity": "sha512-GBuXPjItZ+XivMxR7bhSjVeXKiHZKRN/HbvqrwVemV6V5atcnIIZbg2J2vJHE6RYHydtPCJoUy05YaK4wQO0TA==",
"requires": {
"rwlock": "^5.0.0"
}
},
"p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"requires": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
},
"rwlock": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/rwlock/-/rwlock-5.0.0.tgz",
"integrity": "sha512-XgzRqLMfCcm9QfZuPav9cV3Xin5TRcIlp4X/SH3CvB+x5D2AakdlEepfJKDd8ByncvfpcxNWdRZVUl38PS6ZJg=="
},
"sdp-transform": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw=="
},
"unhomoglyph": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
}
}
}

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "timetable-v2-notify",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "minie4",
"license": "MIT",
"dependencies": {
"dotenv": "^16.3.1",
"matrix-js-sdk": "^26.1.0",
"node-fetch": "^3.3.1",
"node-json-db": "^2.2.0"
}
}

74
timetable.js Normal file
View File

@ -0,0 +1,74 @@
import fetch from "node-fetch";
export class TimetableClient {
constructor(apiEndpoint, token) {
this.apiEndpoint = apiEndpoint;
this.token = token;
}
login(password) {
fetch(
this.apiEndpoint + `/token?password=${encodeURIComponent(password)}`,
{ method: "post" }
)
.then((e) => {
if (e.status == 401) {
console.warn("Invalid Timetable V2 password!");
return;
} else if (e.status != 200) {
console.log("Failed requesting Timetable V2 API-Token!");
return;
}
return e.json();
})
.then((token) => (this.token = token.token));
}
getTimetables(forClass) {
return new Promise((resolve, reject) => {
fetch(
this.apiEndpoint +
`/timetable?token=${encodeURIComponent(
this.token
)}&class=${encodeURIComponent(forClass)}`
)
.then((e) => {
if (e.status != 200) reject();
else return e.json();
})
.then((timetables) => resolve(timetables));
});
}
async getGroups(timetableClass, timetableId) {
const timetables = await fetch(
this.apiEndpoint +
`/timetable?token=${encodeURIComponent(
this.token
)}&class=${encodeURIComponent(timetableClass)}`
);
const timetable = (await timetables.json()).timetables.find(
(e) => e.id == timetableId
);
const foundTimetableGroups = [];
if (!timetable.data) return [];
// Make a list of all possible timetable groups
// found in the current timetable
for (const day of timetable.data) {
for (const lesson of day) {
if (Array.isArray(lesson) && lesson.length > 1) {
for (const group of lesson) {
if (group.group && !foundTimetableGroups.includes(group.group)) {
foundTimetableGroups.push(group.group);
}
}
}
}
}
return foundTimetableGroups;
}
async getUpdates() {
const history = await fetch(
this.apiEndpoint + "/history" + `?token=${encodeURIComponent(this.token)}`
);
return await history.json();
}
}