Compare commits
10 Commits
e8552966ec
...
1f2c27fa7e
Author | SHA1 | Date | |
---|---|---|---|
1f2c27fa7e | |||
cc944871a4 | |||
68861e55db | |||
3d631e04b6 | |||
fede589b0c | |||
a0887106d6 | |||
b0e1fa39fa | |||
2a2852f5dd | |||
6bd5c35eb2 | |||
f843ee1339 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
data/
|
25
.env.sample
Normal file
25
.env.sample
Normal 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
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
data.json
|
||||
data/
|
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
27
README.md
Normal 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
|
||||
|
||||

|
||||
|
||||
## 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
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
BIN
images/profile-picture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
images/screenshot.jpg
Normal file
BIN
images/screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
78
index.js
78
index.js
@ -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);
|
||||
});
|
||||
|
Reference in New Issue
Block a user