Compare commits
10 Commits
e8552966ec
...
main
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/
|
node_modules/
|
||||||
.env
|
.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 * as sdk from "matrix-js-sdk";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
import process from "process";
|
||||||
|
import fs from "fs";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { JsonDB, Config } from "node-json-db";
|
import { JsonDB, Config } from "node-json-db";
|
||||||
import { TimetableClient } from "./timetable.js";
|
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(
|
const timetable = new TimetableClient(
|
||||||
process.env.TIMETABLE_ENDPOINT,
|
process.env.TIMETABLE_ENDPOINT,
|
||||||
process.env.TIMETABLE_TOKEN
|
process.env.TIMETABLE_TOKEN
|
||||||
@ -21,6 +25,24 @@ const client = sdk.createClient({
|
|||||||
fetchFn: fetch,
|
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) {
|
client.once("sync", function (state) {
|
||||||
if (state === "PREPARED") {
|
if (state === "PREPARED") {
|
||||||
console.log("sync finished");
|
console.log("sync finished");
|
||||||
@ -76,9 +98,9 @@ function formatUpdateMessage(update) {
|
|||||||
} else {
|
} else {
|
||||||
for (const change of Object.keys(update.change)) {
|
for (const change of Object.keys(update.change)) {
|
||||||
const changeValue = update.change[change];
|
const changeValue = update.change[change];
|
||||||
message += `<br>\n${
|
message += `<br><li>\n${
|
||||||
change.charAt(0).toUpperCase() + change.slice(1)
|
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
|
// Check for timetable updates
|
||||||
setTimeout(async () => {
|
setInterval(async () => {
|
||||||
const history = await timetable.getUpdates();
|
const history = await timetable.getUpdates();
|
||||||
const lastCheck = await db.getObjectDefault("/lastTimetableUpdate", 0);
|
const lastCheck = await db.getObjectDefault("/lastTimetableUpdate", 0);
|
||||||
const updates = history.filter((entry) => entry.updatedAt > lastCheck);
|
const updates = history.filter((entry) => entry.updatedAt > lastCheck);
|
||||||
@ -104,14 +126,17 @@ setTimeout(async () => {
|
|||||||
client.sendHtmlMessage(roomId, plainText(message), message);
|
client.sendHtmlMessage(roomId, plainText(message), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, process.env.TIMETABLE_INTERVAL || 1000 * 60);
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
client.on("Room.timeline", async function (event, room) {
|
client.on("Room.timeline", async function (event, room) {
|
||||||
if (event.getLocalAge() > 5000) return;
|
if (event.getLocalAge() > 5000) return;
|
||||||
if (event.event.sender == userId) 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 (senderPowerLevel <= 40) {
|
||||||
if (
|
if (
|
||||||
event.getType() == "m.room.message" &&
|
event.getType() == "m.room.message" &&
|
||||||
@ -125,6 +150,36 @@ client.on("Room.timeline", async function (event, room) {
|
|||||||
return;
|
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 {
|
try {
|
||||||
if (event.getType() === "m.room.message") {
|
if (event.getType() === "m.room.message") {
|
||||||
await handleMessage(event, room.roomId);
|
await handleMessage(event, room.roomId);
|
||||||
@ -172,6 +227,7 @@ async function handleMessage(event, room) {
|
|||||||
<li> <code>timetable</code>: Set your timetable</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>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>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);
|
client.sendHtmlMessage(room, plainText(helpMessage), helpMessage);
|
||||||
} else if (body == "info") {
|
} else if (body == "info") {
|
||||||
@ -241,6 +297,11 @@ async function handleMessage(event, room) {
|
|||||||
await db.delete(`/rooms/${room}`);
|
await db.delete(`/rooms/${room}`);
|
||||||
const response = "⚠ The configuration for this room was reset!";
|
const response = "⚠ The configuration for this room was reset!";
|
||||||
client.sendHtmlMessage(room, plainText(response), response);
|
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 {
|
} else {
|
||||||
const response =
|
const response =
|
||||||
"Unknown command! Type <code>help</code> for a list of valid commands";
|
"Unknown command! Type <code>help</code> for a list of valid commands";
|
||||||
@ -296,3 +357,8 @@ async function handleReaction(event, room) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client.startClient(0);
|
client.startClient(0);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.info("Interrupted");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user