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 += ` ${update.lesson}th Lesson
login [password]
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("help
: List available commands info
: Get information about the current room's configuration filter [class (eg. 10c)]
: Set the class to filter for. Use only filter available in the timetable v2 app.timetable
: Set your timetablegroups
: Configure your timetable groups [Not implemented yet]reset
: Reset the configuration for this roomlogout
: Logout and reset the configuration for room${roomData.filter || "None"}
${roomData.timetable || "Not set"}
${(roomData.groups || []).join(", ")}
`;
client.sendHtmlMessage(room, plainText(response), response);
} else if (body.startsWith("filter ")) {
// Set filtering
const filter = body.split(" ")[1].toLowerCase();
const response = `👍 Now filtering for ${filter}
`;
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?${timetable.id}
: ${timetable.title}help
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 ${timetable}
`;
client.sendHtmlMessage(room, plainText(response), response);
} else {
const response =
"⚠ You are currently in setup mode. Complete the setup or type abort
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 ${Object.values(
stateData.groups
).join(", ")}
`;
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);
});