365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
import * as sdk from "matrix-js-sdk";
|
|
import fetch from "node-fetch";
|
|
import process from "process";
|
|
import fs from "fs";
|
|
import "dotenv/config";
|
|
import { JsonDB, Config } from "node-json-db";
|
|
import { TimetableClient } from "./timetable.js";
|
|
|
|
const authPassword = process.env.AUTH_PASSWORD;
|
|
|
|
const db = new JsonDB(new Config("data/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,
|
|
});
|
|
|
|
const displayName = process.env.MATRIX_DISPLAYNAME || "Timetable V2";
|
|
(async () => {
|
|
const profileInfo = await client.getProfileInfo(userId);
|
|
if (profileInfo.displayname != displayName) {
|
|
await client.setProfileInfo("displayname", {
|
|
displayname: displayName,
|
|
});
|
|
const file = fs.readFileSync("./images/profile-picture.png");
|
|
const content = await client.uploadContent(file, {
|
|
name: "profile-picture.png",
|
|
type: "image/png",
|
|
});
|
|
await client.setProfileInfo("avatar_url", {
|
|
avatar_url: content.content_uri,
|
|
});
|
|
}
|
|
})();
|
|
|
|
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><li>\n${
|
|
change.charAt(0).toUpperCase() + change.slice(1)
|
|
}: <del>${changeValue.before}</del> ${changeValue.after}</li>`;
|
|
}
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
// Check for timetable updates
|
|
setInterval(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);
|
|
}
|
|
}
|
|
}, process.env.TIMETABLE_INTERVAL || 1000 * 60);
|
|
|
|
// Handle events
|
|
client.on("Room.timeline", async function (event, room) {
|
|
if (event.getLocalAge() > 5000) return;
|
|
if (event.event.sender == userId) return;
|
|
|
|
const senderPowerLevel = client
|
|
.getRoom(room.roomId)
|
|
.getMember(event.event.sender).powerLevel;
|
|
|
|
if (senderPowerLevel <= 40) {
|
|
if (
|
|
event.getType() == "m.room.message" &&
|
|
event.event.content.body == "help"
|
|
) {
|
|
client.sendTextMessage(
|
|
room.roomId,
|
|
"⛔ You need at least powerlevel 40 moderate this bot"
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!(await db.getObjectDefault(`/authStatus/${room.roomId}`, false)) &&
|
|
authPassword
|
|
) {
|
|
if (event.getType() !== "m.room.message") return;
|
|
|
|
if (!event.event.content.body.startsWith("login")) {
|
|
const response =
|
|
"🔐 Not authenticated! Use <code>login [password]</code> to login";
|
|
client.sendHtmlMessage(room.roomId, plainText(response), response);
|
|
return;
|
|
}
|
|
|
|
if (event.event.content.body.split("login ")[1] == authPassword) {
|
|
await db.push(`/authStatus/${room.roomId}`, true);
|
|
client.sendTextMessage(room.roomId, "🔑 This room is now authenticated");
|
|
client
|
|
.redactEvent(room.roomId, event.event.event_id, undefined, {
|
|
reason: "Redacted login password",
|
|
})
|
|
.catch((e) => {
|
|
console.warn("Could not redact password in " + room.roomId);
|
|
});
|
|
return;
|
|
} else {
|
|
client.sendTextMessage(room.roomId, "❌ Invalid password");
|
|
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 <i>[Not implemented yet]</i></li>
|
|
<li> <code>reset</code>: Reset the configuration for this room</li>
|
|
<li> <code>logout</code>: Logout and reset the configuration for 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.`;
|
|
response +=
|
|
"<br>\n<b>Warning:</b> Timetable group support is WIP. You can configure your timetable groups, but they will not be taken into account while processing updates just yet. ";
|
|
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 if (body == "logout") {
|
|
await db.delete(`/rooms/${room}`);
|
|
await db.delete(`/authStatus/${room}`);
|
|
const response = "🔐 This room was logged out!";
|
|
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.getObjectDefault(`/rooms/${room}/stateData`);
|
|
if (event.sender.userId != stateData.sender) return;
|
|
if (event.event.content["m.relates_to"].key == "✅") {
|
|
if (!stateData.groups) stateData.groups = {};
|
|
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);
|
|
|
|
process.on("SIGINT", () => {
|
|
console.info("Interrupted");
|
|
process.exit(0);
|
|
});
|