Compare commits

...

10 Commits

Author SHA1 Message Date
1f2c27fa7e 🚑 Fix crash when sending non-message events 2023-06-24 17:15:29 +02:00
cc944871a4 🔧 Make update interval configurable 2023-06-24 15:15:33 +02:00
68861e55db 🔒️ Add authentication 2023-06-24 14:52:24 +02:00
3d631e04b6 🐛 Fix powerlevel detection 2023-06-24 14:33:49 +02:00
fede589b0c Automatically set the bot's profile info 2023-06-23 23:38:51 +02:00
a0887106d6 📝 Add .env.sample 2023-06-23 20:55:02 +02:00
b0e1fa39fa 📝 Add README 2023-06-23 20:54:21 +02:00
2a2852f5dd 🐳 Add docker support 2023-06-23 20:43:21 +02:00
6bd5c35eb2 🐛 Fix list for changed substitutions 2023-06-23 20:17:16 +02:00
f843ee1339 Check for updates every minute 2023-06-23 20:16:48 +02:00
9 changed files with 139 additions and 7 deletions

3
.dockerignore Normal file
View File

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

25
.env.sample Normal file
View File

@ -0,0 +1,25 @@
# Environment variables for Timetable V2 Notify
# Use this file as a template and change the values accordingly
# Set Timetable V2 API endpoint and token here
# (Make sure the /api and https:// is included in the endpoint)
TIMETABLE_ENDPOINT=
TIMETABLE_TOKEN=
# How often should the script query for timetable updates
# Default: 60000 (every minute)
TIMETABLE_INTERVAL=
# Set the Matrix url, bot user name and auth token here
MATRIX_URL=
MATRIX_USER=
# You can get the access token by for example logging into
# the bot's matrix account in element and copying the
# Access Token under Settings > Help & About > Advanced
MATRIX_TOKEN=
# The bot set this as its display name (Default: Timetable V2)
MATRIX_DISPLAYNAME=
# If set, this password must be used with the "login" command
# to be able to use this bot
AUTH_PASSWORD=

2
.gitignore vendored
View File

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

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:lts-alpine
WORKDIR /app
COPY package*.json /app/
RUN npm ci
COPY ./ /app/
VOLUME [ "/app/data" ]
CMD ["node", "index.js"]

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Timetable V2 Notify
A [Matrix](https://matrix.org/) bot for [Timetable V2](https://gitlab.minie4.de/minie4/timetable-v2) to notify users when the substitution plan changes.
<div align="center">
<img src="images/logo.svg" width="100" height="100">
</div>
## Screnshot
![Screenshot](images/screenshot.jpg)
## Setup using docker
Build the docker image
```sh
docker build -t timetable-v2-notify .
```
Copy the `.env.sample` to `.env` and change the values as required.
Run the docker image
```sh
docker run -d --name timetable-v2-notify -v timetable-v2-notify-data:/app/data --restart unless-stopped timetable-v2-notify
```

1
images/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
images/profile-picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
images/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,10 +1,14 @@
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 db = new JsonDB(new Config("data", true, true, "/"));
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
@ -21,6 +25,24 @@ const client = sdk.createClient({
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");
@ -76,9 +98,9 @@ function formatUpdateMessage(update) {
} else {
for (const change of Object.keys(update.change)) {
const changeValue = update.change[change];
message += `<br>\n${
message += `<br><li>\n${
change.charAt(0).toUpperCase() + change.slice(1)
}: <del>${changeValue.before}</del> ${changeValue.after}`;
}: <del>${changeValue.before}</del> ${changeValue.after}</li>`;
}
}
@ -86,7 +108,7 @@ function formatUpdateMessage(update) {
}
// Check for timetable updates
setTimeout(async () => {
setInterval(async () => {
const history = await timetable.getUpdates();
const lastCheck = await db.getObjectDefault("/lastTimetableUpdate", 0);
const updates = history.filter((entry) => entry.updatedAt > lastCheck);
@ -104,14 +126,17 @@ setTimeout(async () => {
client.sendHtmlMessage(roomId, plainText(message), message);
}
}
}, 1000);
}, 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 = room.oldState.members[event.event.sender].powerLevel;
const senderPowerLevel = client
.getRoom(room.roomId)
.getMember(event.event.sender).powerLevel;
if (senderPowerLevel <= 40) {
if (
event.getType() == "m.room.message" &&
@ -125,6 +150,36 @@ client.on("Room.timeline", async function (event, room) {
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);
@ -172,6 +227,7 @@ async function handleMessage(event, room) {
<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") {
@ -241,6 +297,11 @@ async function handleMessage(event, 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";
@ -296,3 +357,8 @@ async function handleReaction(event, room) {
}
client.startClient(0);
process.on("SIGINT", () => {
console.info("Interrupted");
process.exit(0);
});