quotes-bot/index.mjs
2025-01-31 14:03:42 -06:00

455 lines
12 KiB
JavaScript

"use strict";
import config from "./config.mjs";
import {
MatrixClient,
AutojoinRoomsMixin,
RichRepliesPreprocessor,
} from "matrix-bot-sdk";
import FormData from "form-data";
import axios from "axios";
import sizeOf from "image-size";
import escapeHtml from "escape-html";
let user_id;
const client = new MatrixClient(config.homeserver, config.token);
function get_command_argument(event) {
return event["content"]["body"]
.slice(config.prefix.length)
.split(" ")
.slice(1)
.join(" ")
.trim();
}
function dlog(...data) {
if (config.debug) console.info("[DEBUG]", ...data);
}
async function cmd_quote(room_id, event) {
let caption = get_command_argument(event).substring(0, 128);
dlog(`Quote ${caption} command invoked in ${room_id} by`, event);
if (!caption) {
await client.replyText(
room_id,
event,
`Usage: quote <caption> - takes the replied to image and uploads it to ${config.imag}. Currently isn't working for some reason.`,
);
return;
}
let original_message = await client.getEvent(
room_id,
event["content"]["m.relates_to"]["m.in_reply_to"]["event_id"],
);
let image_url = original_message.content.url;
dlog(`Image URL: ${image_url}`);
if (!image_url || !image_url.startsWith("mxc://")) return;
let image = await axios.get(client.mxcToHttp(image_url), {
responseType: "arraybuffer",
});
let data = new FormData();
data.append("desc", caption);
data.append("key", config.imag_key);
data.append("image", image.data, { filename: "quote.jpg" });
axios({
method: "post",
url: config.imag,
headers: {
...data.getHeaders(),
},
data: data,
})
.then((r) => {
dlog(r);
if (r.status >= 400)
throw new Error(
`Failed to POST the image. Response code: ${r.status}`,
);
client.replyText(
room_id,
event,
`An error occured: HTTP status ${r.status}`,
);
axios.get(`${config.imag}/api/latest`).then(async (r) => {
await client.replyText(
room_id,
event,
`Quote #${r.data} posted! Check it out at ${config.imag}#${r.data} or ${config.imag}image/${r.data} :)`,
);
});
})
.catch(async (e) => {
console.error(e);
await client.replyText(room_id, event, "Error!");
});
}
async function cmd_get(room_id, event) {
let q = get_command_argument(event);
let image_url, image_id;
dlog(`Get ${q} command invoked in ${room_id} by`, event);
if (!q) {
await client.replyText(
room_id,
event,
"Usage: get <quote ID> OR (newest:)(n (result number, starting from 1):)<query> - gets a quote by its ID, or searches for it applying score or newest posted filters, also allows you to set which result to get",
);
return;
}
if (q.match(/^\d+$/)) {
image_id = q;
image_url = `${config.imag}image/${image_id}`;
} else {
let newest = q.startsWith("newest:");
if (newest) q = q.slice(7);
let n = 1;
let matches = q.match(/^(\d+):/);
if (matches !== null) {
n = parseInt(matches[1]);
q = q.replace(/^(\d+:)/, "").trim();
}
let results = await axios.get(
`${config.imag}api/search?q=${encodeURIComponent(q)}&s=${newest ? "newest" : "score"}`,
);
if (results.status >= 400 || results.data.length < n) {
client.replyText(room_id, event, "No such quotes found.");
return;
}
image_id = results.data[Math.max(n - 1, 0)].iid;
image_url = `${config.imag}image/${image_id}`;
}
dlog("Image ID:", image_id);
dlog("Image URL:", image_url);
let image, metadata;
try {
image = await axios.get(image_url, {
responseType: "arraybuffer",
});
} catch (e) {
console.error(e);
await client.replyText(room_id, event, "Failed to fetch the quote.");
return;
}
try {
metadata = (await axios.get(`${config.imag}api/image/${image_id}`))
.data;
} catch (e) {
console.error(e);
await client.replyText(
room_id,
event,
"Failed to fetch the quote metadata, so won't sent the associated metadata with it.",
);
}
dlog("Metadata:", metadata);
let buffer = Buffer.from(image.data);
let dimensions = sizeOf(buffer);
let mime = image.headers["content-type"];
let ext = mime === "image/png" ? "png" : "jpg";
dlog("Dimensions:", dimensions);
dlog("MIME:", mime);
dlog("Extension:", ext);
let content = {
body: `quote-${image_id}.${ext}`,
info: {
size: image.data.byteLength,
w: dimensions.width,
h: dimensions.height,
mimetype: mime,
},
msgtype: "m.image",
url: undefined,
"m.relates_to": {
"m.in_reply_to": {
event_id: event["event_id"],
},
},
};
let mxc = await client.uploadContent(buffer, {
filename: `quote.${ext}`,
type: mime,
});
dlog("Image uploaded:", mxc);
content.url = mxc;
await client.sendMessage(room_id, content);
if (metadata)
await client.sendHtmlText(
room_id,
`<a href="${config.imag}#${image_id}">Quote ${metadata.iid}</a>: "${escapeHtml(metadata.desc)}"
<hr/>
<ul>
<li>Score: ${metadata.score} ${metadata.score < 0 ? "\uD83D\uDC4E" : "\uD83D\uDC4D"}</li>
<li>Created: ${new Date(metadata.created * 1000).toUTCString()}</li>
<li>Edited: ${new Date(metadata.edited * 1000).toUTCString()}</li>
</ul>
<blockquote>${escapeHtml(metadata.ocr).replaceAll("\n", "<br/>")}<blockquote>`,
);
}
async function cmd_join(room_id, event, action = "join", what = "Joined") {
let room = get_command_argument(event);
dlog(`Invoked ${action} ${room} command in ${room_id} by`, event);
if (!room) {
await client.replyText(
room_id,
event,
`Usage: ${action} <room ID or alias> - ${action} a room`,
);
return;
}
try {
dlog(`Resolving ${room}`);
room = await client.resolveRoom(room);
} catch (e) {
console.error(e);
await client.replyText(
room_id,
event,
"Failed to resolve the room. Wrong room ID or alias?",
);
return;
}
await client[`${action}Room`](room)
.then(async () => {
await client.replyText(room_id, event, `${what} ${room}`);
})
.catch(async (e) => {
console.error(e);
await client.replyText(
room_id,
event,
`Failed to ${action} the room. Invalid room ID?`,
);
});
}
async function cmd_leave(room_id, event) {
await cmd_join(room_id, event, "leave", "Left");
}
async function cmd_score(room_id, event) {
let n = 10,
ns = get_command_argument(event);
if (ns && ns.match(/^-?\d+$/)) n = parseInt(ns);
if (n === 0) {
client.replyText(
room_id,
event,
"Usage: score <positive or negative number>",
);
return;
}
let all;
try {
all = (await axios.get(`${config.imag}api/all`)).data;
} catch (e) {
console.error(e);
await client.replyText(room_id, event, "Failed to fetch all quotes.");
}
let qs;
if (n > 0) qs = all.slice(0, n);
else if (n < 0) qs = all.slice(n);
if (qs.length == 1) {
event["content"]["body"] = `${config.prefix}get ${qs[0].iid}`;
return await cmd_get(room_id, event);
}
let html = "<ul>";
for (let idx = 0; idx < qs.length; ++idx) {
let q = qs[idx];
html += `<li><a href="${config.imag}#${q.iid}">Quote #${q.iid}</a> - "${escapeHtml(q.desc)}": ${q.score} ${q.score < 0 ? "\uD83D\uDC4E" : "\uD83D\uDC4D"}</li>`;
}
html += "</ul>";
await client.replyHtmlText(room_id, event, html);
}
async function on_room_message(room_id, event) {
// debug stuff
if (config.debug && room_id !== config.room) return;
// non-debug stuff
if (
!event["content"] ||
!event["content"]["body"] ||
event["sender"] === user_id
)
return;
dlog(room_id, event);
if (
event["content"]["m.relates_to"] &&
event["content"]["m.relates_to"]["m.in_reply_to"] &&
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}quote`)
)
await cmd_quote(room_id, event);
else if (
event["content"]["body"].toLowerCase().startsWith(`${config.prefix}get`)
)
await cmd_get(room_id, event);
else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}source`)
)
await client.replyText(
room_id,
event,
"https://git.everypizza.im/n/quotes-bot (contact @n:everypizza.im for details or help on setting up your own!)",
);
else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}join`) &&
event["sender"] === config.admin
)
await cmd_join(room_id, event);
else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}leave`) &&
event["sender"] === config.admin
)
await cmd_leave(room_id, event);
else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}die`) &&
event["sender"] === config.admin
) {
dlog(`Die invoked in ${room_id} by`, event);
await client.replyText(room_id, event, "Goodnight!");
process.exit();
} else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}help`)
)
await client.replyHtmlText(
room_id,
event,
`
Available commands:<br/>
<br/>
- quote <caption> - post a quote to ${config.imag}<br/>
- get <ID or (newest:)(Nth:)query> - get a quote<br/>
- source - get the source code of the bot<br/>
- join <room id> - join a room (admin only)<br/>
- leave <room id> - leave a room (admin only)<br/>
- die - make the bot shut down (admin only)<br/>
- help - print help<br/>
- score <negative or positive number = 10> - get the quotes with lowest or highest scores<br/>
Source code: <a href="https://git.everypizza.im/n/quotes-bot">https://git.everypizza.im/n/quotes-bot</a>
`.trim(),
);
else if (
event["content"]["body"]
.toLowerCase()
.startsWith(`${config.prefix}score`)
)
await cmd_score(room_id, event);
else if (
event["content"]["body"].toLowerCase().startsWith(`${config.prefix}add`)
)
await client.replyText(
room_id,
event,
"you don't have permissions for that 😾",
);
}
async function main() {
if (!config.room && config.debug)
throw new Error(
"Please set config.room to use debug mode. The quotes bot will only respond to messages in that room.",
);
client.addPreprocessor(new RichRepliesPreprocessor(false));
if (config.debug) {
config.prefix = `${Math.random()}:${config.prefix}`;
dlog(`Debug prefix: ${config.prefix}`);
}
if (config.autojoin) {
dlog("Enabling autojoin");
AutojoinRoomsMixin.setupOnClient(client);
}
await client.start().then(async () => {
user_id = await client.getUserId();
console.log(`Bot started! User ID: ${user_id}`);
});
if (config.room) {
let r = await client.resolveRoom(config.room);
dlog(`Joining ${config.room} (${r})`);
config.room = r;
await client.joinRoom(config.room);
} else config.warn("No default room set");
client.on("room.message", async (room_id, event) => {
try {
await on_room_message(room_id, event);
} catch (e) {
console.error(e);
client.replyText(room_id, event, "Error!");
}
});
}
dlog("Hello, Debug!");
main();