upd1
This commit is contained in:
parent
6357080619
commit
880ae5a359
8 changed files with 232 additions and 138 deletions
|
@ -1,9 +1,10 @@
|
|||
PREFIX=!
|
||||
BASE_URL=https://example.com
|
||||
OWNER_ID=@root:example.com
|
||||
USER_ID=@bot:example.com
|
||||
ACCESS_TOKEN=1234567890
|
||||
DEVICE_ID=Bot-Device
|
||||
|
||||
PREFIX=!
|
||||
LOG_CHANNEL=!1234567890:example.com
|
||||
AP_FETCH_PORT=3000
|
||||
AP_FETCH_DOMAIN=example.com
|
||||
OWNER_ID=@nyx:nyx.ftp.sh
|
||||
|
|
|
@ -1,15 +1,46 @@
|
|||
import { exec as ossl_exec } from "openssl-wrapper";
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
|
||||
import env from 'dotenv';
|
||||
env.config();
|
||||
|
||||
const password = "penis";
|
||||
|
||||
function openssl(func, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ossl_exec(func, args, function (err, buffer) {
|
||||
if (err)
|
||||
reject(err);
|
||||
|
||||
resolve(buffer);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const domain = process.env.AP_FETCH_DOMAIN;
|
||||
const pubkey = fs.readFileSync('data/publickey.crt', 'utf8');
|
||||
|
||||
if (!fs.existsSync("data/server-crt.pem")) {
|
||||
console.log("Generating certificate...");
|
||||
await openssl('req', {
|
||||
new: true,
|
||||
newkey: 'rsa:4096',
|
||||
days: 365,
|
||||
nodes: true,
|
||||
x509: true,
|
||||
subj: '/C=',
|
||||
keyout: "data/server-key.pem",
|
||||
out: "data/server-crt.pem"
|
||||
});
|
||||
|
||||
console.log("Certificate generated!");
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
const pubkey = fs.readFileSync('data/server-crt.pem', 'utf8');
|
||||
const notice = fs.readFileSync('auth-fetch-notice.txt', 'utf8');
|
||||
const userId = process.env.USER_ID;
|
||||
const username = userId.slice(1, userId.indexOf(":"));
|
||||
|
||||
const actor = {
|
||||
'@context': [
|
||||
|
@ -19,7 +50,7 @@ const actor = {
|
|||
|
||||
'id': 'https://' + domain + '/actor',
|
||||
'type': 'Person',
|
||||
'preferredUsername': 'possumbot',
|
||||
'preferredUsername': username,
|
||||
'inbox': 'https://' + domain + '/inbox',
|
||||
|
||||
'publicKey': {
|
||||
|
@ -30,7 +61,7 @@ const actor = {
|
|||
}
|
||||
|
||||
const webfinger = {
|
||||
'subject': 'acct:possumbot@' + domain + '',
|
||||
'subject': 'acct:' + userId,
|
||||
|
||||
'links': [
|
||||
{
|
||||
|
@ -41,24 +72,24 @@ const webfinger = {
|
|||
]
|
||||
}
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.write(notice);
|
||||
res.end();
|
||||
app.get('/', (_, res) => {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.write(notice);
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.get('/actor', (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/activity+json' });
|
||||
res.write(JSON.stringify(actor));
|
||||
res.end();
|
||||
app.get('/actor', (_, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/activity+json' });
|
||||
res.write(JSON.stringify(actor));
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.get('/.well-known/webfinger', (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.write(JSON.stringify(webfinger));
|
||||
res.end();
|
||||
app.get('/.well-known/webfinger', (_, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.write(JSON.stringify(webfinger));
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.listen(process.env.AP_FETCH_PORT);
|
||||
|
||||
export default true;
|
||||
export default function (port) {
|
||||
app.listen(port);
|
||||
};
|
||||
|
|
45
index.js
45
index.js
|
@ -4,7 +4,6 @@ import * as sdk from "matrix-js-sdk";
|
|||
import sdkExt from "./lib/ext.js";
|
||||
import { resolve } from "node:path";
|
||||
import fs from "node:fs";
|
||||
import util from "util";
|
||||
import { LocalStorage } from 'node-localstorage';
|
||||
|
||||
import fetch from "node-fetch";
|
||||
|
@ -31,6 +30,8 @@ const client = sdk.createClient({
|
|||
|
||||
sdkExt(client);
|
||||
|
||||
apfetch(process.env.AP_FETCH_PORT);
|
||||
|
||||
var prefixes = [process.env.PREFIX];
|
||||
client.initialized = false;
|
||||
|
||||
|
@ -39,7 +40,7 @@ client.commands = [];
|
|||
|
||||
for (const file of fs.readdirSync(resolve("modules"))) {
|
||||
try {
|
||||
if(file.startsWith(".")) continue;
|
||||
if (file.startsWith(".")) continue;
|
||||
var module = (await import(resolve("modules", file))).default;
|
||||
client.modules.push(module);
|
||||
client.log("[load:modules]", `loaded ${module.name}`);
|
||||
|
@ -50,7 +51,7 @@ for (const file of fs.readdirSync(resolve("modules"))) {
|
|||
|
||||
for (const file of fs.readdirSync(resolve("commands"))) {
|
||||
try {
|
||||
if(file.startsWith(".")) continue;
|
||||
if (file.startsWith(".")) continue;
|
||||
var command = (await import(resolve("commands", file))).default;
|
||||
command.usage = process.env.PREFIX + command.command + (command.usage ? " " + command.usage : '');
|
||||
client.commands.push(command);
|
||||
|
@ -62,14 +63,14 @@ for (const file of fs.readdirSync(resolve("commands"))) {
|
|||
|
||||
function doModule(client, event) {
|
||||
client.modules.forEach(m => {
|
||||
if(!m) return;
|
||||
if (!m) return;
|
||||
|
||||
var config = client.config.get(event.sender.roomId);
|
||||
if(config.get(m.name) === false) return;
|
||||
if (config.get(m.name) === false) return;
|
||||
|
||||
try {
|
||||
m.hooks?.message(client, event);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
client.log("[hook]", err);
|
||||
}
|
||||
});
|
||||
|
@ -77,32 +78,32 @@ function doModule(client, event) {
|
|||
|
||||
function doCommand(client, event, cmd, args) {
|
||||
var command;
|
||||
command = client.commands.filter(c=>c.command==cmd)[0];
|
||||
if(!command)
|
||||
command = client.commands.filter(c => c.command == cmd)[0];
|
||||
if (!command)
|
||||
return false;
|
||||
|
||||
if((command.owner && event.sender.userId != process.env.OWNER_ID) || (command.minPwr && event.sender.powerLevel < command.minPwr && event.sender.userId != process.env.OWNER_ID)) {
|
||||
if ((command.owner && event.sender.userId != process.env.OWNER_ID) || (command.minPwr && event.sender.powerLevel < command.minPwr && event.sender.userId != process.env.OWNER_ID)) {
|
||||
var addl = command.minPwr ? `this command requires a power level of ${command.minPwr}\nyour power level is ${event.sender.powerLevel}` : "this command is owner-only.";
|
||||
client.reply(event, addl, '<img src="mxc://possum.city/b4Vo1BTcq49B7TbFWCqq76HQWQEdNIqq" alt="nuh uh" /><br>' + addl.replaceAll("\n", "<br>"));
|
||||
client.reply(event, addl, '<img src="mxc://nyx.ftp.sh/2kAXPyosdaz1M6Fv8t2V9SyxcJZEscdR" alt="nuh uh" /><br>' + addl.replaceAll("\n", "<br>"));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
command.execute(client, event, args);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
client.log("[cmd]", err);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
client.once("sync", async function (state, prevState, data) {
|
||||
if(state != "PREPARED") return;
|
||||
client.setGlobalErrorOnUnknownDevices(false);
|
||||
client.name = client._store.users[client.credentials.userId].displayName;
|
||||
client.log("[sdk:sync]", "connected:", client.name);
|
||||
prefixes.push(client.name + ": ");
|
||||
prefixes.push(client.name + " ");
|
||||
client.initialized = true;
|
||||
if (state != "PREPARED") return;
|
||||
client.setGlobalErrorOnUnknownDevices(false);
|
||||
client.name = client._store.users[client.credentials.userId].displayName;
|
||||
client.log("[sdk:sync]", "connected:", client.name);
|
||||
prefixes.push(client.name + ": ");
|
||||
prefixes.push(client.name + " ");
|
||||
client.initialized = true;
|
||||
});
|
||||
|
||||
client.on(sdk.RoomEvent.Timeline, async function (event, room, toStartOfTimeline) {
|
||||
|
@ -114,22 +115,22 @@ client.on(sdk.RoomEvent.Timeline, async function (event, room, toStartOfTimeline
|
|||
|
||||
var content = event.getContent();
|
||||
|
||||
if(content["m.relates_to"] !== undefined)
|
||||
if (content["m.relates_to"] !== undefined)
|
||||
content.body = content.body.substring(content.body.indexOf("\n\n") + 2);
|
||||
|
||||
event.sender = client.getRoom(event.sender.roomId).getMember(event.getSender());
|
||||
var content = content.body;
|
||||
var isCommand = false;
|
||||
|
||||
for(const prefix of prefixes) {
|
||||
if(content.startsWith(prefix)) {
|
||||
for (const prefix of prefixes) {
|
||||
if (content.startsWith(prefix)) {
|
||||
isCommand = true;
|
||||
content = content.substring(prefix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isCommand) {
|
||||
if (!isCommand) {
|
||||
doModule(client, event);
|
||||
} else {
|
||||
var args = content.split(/\s/g);
|
||||
|
|
174
lib/fedimbed.js
174
lib/fedimbed.js
|
@ -1,5 +1,4 @@
|
|||
import httpSignature from "@peertube/http-signature";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
const FRIENDLY_USERAGENT = "PossumBot/1.0 (+https://bot.possum.city/)";
|
||||
|
@ -23,13 +22,13 @@ domainCache.set("cohost.org", "cohost"); // no nodeinfo
|
|||
|
||||
async function resolvePlatform(url) {
|
||||
const urlObj = new URL(url);
|
||||
if(domainCache.has(urlObj.hostname)) return domainCache.get(urlObj.hostname);
|
||||
if (domainCache.has(urlObj.hostname)) return domainCache.get(urlObj.hostname);
|
||||
|
||||
const res = await fetch(urlObj.origin + "/.well-known/nodeinfo", {
|
||||
headers: {"User-Agent": FRIENDLY_USERAGENT},
|
||||
headers: { "User-Agent": FRIENDLY_USERAGENT },
|
||||
}).then((res) => res.text());
|
||||
|
||||
if(!res.startsWith("{")) {
|
||||
if (!res.startsWith("{")) {
|
||||
console.error("[fedimbed]", `No nodeinfo for "${urlObj.hostname}"???`);
|
||||
domainCache.set(urlObj.hostname, null);
|
||||
return null;
|
||||
|
@ -37,17 +36,17 @@ async function resolvePlatform(url) {
|
|||
|
||||
const probe = JSON.parse(res);
|
||||
|
||||
if(!probe?.links) {
|
||||
if (!probe?.links) {
|
||||
console.error("[fedimbed]", `No nodeinfo for "${urlObj.hostname}"???`);
|
||||
domainCache.set(urlObj.hostname, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeinfo = await fetch(probe.links[probe.links.length - 1].href, {
|
||||
headers: {"User-Agent": FRIENDLY_USERAGENT},
|
||||
headers: { "User-Agent": FRIENDLY_USERAGENT },
|
||||
}).then((res) => res.json());
|
||||
|
||||
if(!nodeinfo?.software?.name) {
|
||||
if (!nodeinfo?.software?.name) {
|
||||
console.error("[fedimbed]", `Got nodeinfo for "${urlObj.hostname}", but missing software name.`);
|
||||
domainCache.set(urlObj.hostname, null);
|
||||
return null;
|
||||
|
@ -58,7 +57,7 @@ async function resolvePlatform(url) {
|
|||
}
|
||||
|
||||
const keyId = "https://" + process.env.AP_FETCH_DOMAIN + "/actor#main-key";
|
||||
const privKey = fs.readFileSync("data/private.pem");
|
||||
const privKey = fs.readFileSync("data/server-crt.pem");
|
||||
async function signedFetch(url, options) {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
|
@ -69,20 +68,17 @@ async function signedFetch(url, options) {
|
|||
|
||||
const headerNames = ["(request-target)", "host", "date"];
|
||||
|
||||
httpSignature.sign(
|
||||
{
|
||||
getHeader: (name) => headers[name.toLowerCase()],
|
||||
setHeader: (name, value) => (headers[name] = value),
|
||||
method: options.method ?? "GET",
|
||||
path: urlObj.pathname,
|
||||
},
|
||||
{
|
||||
keyId,
|
||||
key: privKey,
|
||||
headers: headerNames,
|
||||
authorizationHeaderName: "signature",
|
||||
}
|
||||
);
|
||||
httpSignature.sign({
|
||||
getHeader: (name) => headers[name.toLowerCase()],
|
||||
setHeader: (name, value) => (headers[name] = value),
|
||||
method: options.method ?? "GET",
|
||||
path: urlObj.pathname,
|
||||
}, {
|
||||
keyId,
|
||||
key: privKey,
|
||||
headers: headerNames,
|
||||
authorizationHeaderName: "signature",
|
||||
});
|
||||
|
||||
options.headers = Object.assign(headers, options.headers ?? {});
|
||||
|
||||
|
@ -95,16 +91,16 @@ async function processUrl(url) {
|
|||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
console.error("[fedimbed]", err);
|
||||
invalidUrl = true;
|
||||
}
|
||||
|
||||
if(invalidUrl) return {};
|
||||
if (invalidUrl) return {};
|
||||
|
||||
// some lemmy instances have old reddit frontend subdomains
|
||||
// but these frontends are just frontends and dont actually expose the API
|
||||
if(urlObj.hostname.startsWith("old.")) {
|
||||
if (urlObj.hostname.startsWith("old.")) {
|
||||
urlObj.hostname = urlObj.hostname.replace("old.", "");
|
||||
url = urlObj.href;
|
||||
}
|
||||
|
@ -138,7 +134,7 @@ async function processUrl(url) {
|
|||
} catch (err) {
|
||||
console.error("[fedimbed]", `Failed to signed fetch "${url}", retrying unsigned: ${err}`);
|
||||
}
|
||||
if(!rawPostData) {
|
||||
if (!rawPostData) {
|
||||
try {
|
||||
rawPostData = await fetch(url, {
|
||||
headers: {
|
||||
|
@ -152,7 +148,7 @@ async function processUrl(url) {
|
|||
}
|
||||
|
||||
let postData;
|
||||
if(rawPostData?.startsWith("{")) {
|
||||
if (rawPostData?.startsWith("{")) {
|
||||
try {
|
||||
postData = JSON.parse(rawPostData);
|
||||
} catch (err) {
|
||||
|
@ -162,18 +158,18 @@ async function processUrl(url) {
|
|||
console.warn("[fedimbed]", `Got non-JSON for "${url}": ${rawPostData}`);
|
||||
}
|
||||
|
||||
if(postData?.error) {
|
||||
if (postData?.error) {
|
||||
console.error("[fedimbed]", `Received error for "${url}": ${postData.error}`);
|
||||
console.error("[fedimbed]", postData);
|
||||
return postData;
|
||||
}
|
||||
|
||||
if(!postData) {
|
||||
if (!postData) {
|
||||
// We failed to get post.
|
||||
// Assume it was due to AFM or forced HTTP signatures and use MastoAPI
|
||||
|
||||
// Follow redirect from /object since we need the ID from /notice
|
||||
if(PATH_REGEX.pleroma.test(urlObj.pathname)) {
|
||||
if (PATH_REGEX.pleroma.test(urlObj.pathname)) {
|
||||
url = await signedFetch(url, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
|
@ -181,7 +177,7 @@ async function processUrl(url) {
|
|||
},
|
||||
redirect: "manual",
|
||||
}).then((res) => res.headers.get("location"));
|
||||
if(url.startsWith("/")) {
|
||||
if (url.startsWith("/")) {
|
||||
url = urlObj.origin + url;
|
||||
}
|
||||
urlObj = new URL(url);
|
||||
|
@ -190,32 +186,32 @@ async function processUrl(url) {
|
|||
let redirUrl;
|
||||
const options = {};
|
||||
const headers = {};
|
||||
if(PATH_REGEX.pleroma2.test(urlObj.pathname)) {
|
||||
if (PATH_REGEX.pleroma2.test(urlObj.pathname)) {
|
||||
redirUrl = url.replace("notice", "api/v1/statuses");
|
||||
} else if(PATH_REGEX.mastodon.test(urlObj.pathname)) {
|
||||
} else if (PATH_REGEX.mastodon.test(urlObj.pathname)) {
|
||||
const postId = urlObj.pathname.match(PATH_REGEX.mastodon)?.[2];
|
||||
redirUrl = urlObj.origin + "/api/v1/statuses/" + postId;
|
||||
} else if(PATH_REGEX.mastodon2.test(urlObj.pathname)) {
|
||||
} else if (PATH_REGEX.mastodon2.test(urlObj.pathname)) {
|
||||
redirUrl = url.replace(/^\/(.+?)\/statuses/, "/api/v1/statuses");
|
||||
} else if(PATH_REGEX.misskey.test(urlObj.pathname)) {
|
||||
} else if (PATH_REGEX.misskey.test(urlObj.pathname)) {
|
||||
let noteId = url.split("/notes/")[1];
|
||||
if(noteId.indexOf("/") > -1) {
|
||||
if (noteId.indexOf("/") > -1) {
|
||||
noteId = noteId.split("/")[0];
|
||||
} else if(noteId.indexOf("?") > -1) {
|
||||
} else if (noteId.indexOf("?") > -1) {
|
||||
noteId = noteId.split("?")[0];
|
||||
} else if(noteId.indexOf("#") > -1) {
|
||||
} else if (noteId.indexOf("#") > -1) {
|
||||
noteId = noteId.split("#")[0];
|
||||
}
|
||||
console.log("[fedimbed]", "Misskey post ID: " + noteId);
|
||||
redirUrl = urlObj.origin + "/api/notes/show/";
|
||||
options.method = "POST";
|
||||
options.body = JSON.stringify({noteId});
|
||||
options.body = JSON.stringify({ noteId });
|
||||
headers["Content-Type"] = "application/json";
|
||||
} else {
|
||||
console.error("[fedimbed]", `Missing MastoAPI replacement for "${platform}"`);
|
||||
}
|
||||
|
||||
if(redirUrl) {
|
||||
if (redirUrl) {
|
||||
console.log(
|
||||
"[fedimbed]",
|
||||
`Redirecting "${url}" to "${redirUrl}": ${JSON.stringify(options)}, ${JSON.stringify(headers)}`
|
||||
|
@ -233,7 +229,7 @@ async function processUrl(url) {
|
|||
} catch (err) {
|
||||
console.error("[fedimbed]", `Failed to signed fetch "${url}" via MastoAPI, retrying unsigned: ${err}`);
|
||||
}
|
||||
if(!rawPostData2) {
|
||||
if (!rawPostData2) {
|
||||
try {
|
||||
rawPostData2 = await signedFetch(
|
||||
redirUrl,
|
||||
|
@ -249,15 +245,15 @@ async function processUrl(url) {
|
|||
}
|
||||
|
||||
let postData2;
|
||||
if(rawPostData2?.startsWith("{")) {
|
||||
if (rawPostData2?.startsWith("{")) {
|
||||
postData2 = JSON.parse(rawPostData2);
|
||||
} else {
|
||||
console.warn("[fedimbed]", `Got non-JSON for "${url}" via MastoAPI: ${rawPostData2}`);
|
||||
}
|
||||
|
||||
if(!postData2) {
|
||||
if (!postData2) {
|
||||
console.warn("[fedimbed]", `Bailing trying to re-embed "${url}": Failed to get post from normal and MastoAPI.`);
|
||||
} else if(postData2.error) {
|
||||
} else if (postData2.error) {
|
||||
console.error(
|
||||
"[fedimbed]",
|
||||
`Bailing trying to re-embed "${url}", MastoAPI gave us error: ${JSON.stringify(postData2.error)}`
|
||||
|
@ -281,18 +277,18 @@ async function processUrl(url) {
|
|||
avatar: postData2.account?.avatar ?? postData2.user?.avatarUrl,
|
||||
};
|
||||
timestamp = postData2.created_at ?? postData2.createdAt;
|
||||
emotes = postData2.emojis.filter((x) => !x.name.endsWith("@.")).map((x) => ({name: `:${x.name}:`, url: x.url}));
|
||||
emotes = postData2.emojis.filter((x) => !x.name.endsWith("@.")).map((x) => ({ name: `:${x.name}:`, url: x.url }));
|
||||
sensitive = postData2.sensitive;
|
||||
|
||||
const attachments = postData2.media_attachments ?? postData2.files;
|
||||
if(attachments) {
|
||||
for(const attachment of attachments) {
|
||||
if (attachments) {
|
||||
for (const attachment of attachments) {
|
||||
const contentType = await fetch(attachment.url, {
|
||||
method: "HEAD",
|
||||
}).then((res) => res.headers.get("Content-Type"));
|
||||
|
||||
if(contentType) {
|
||||
if(contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) {
|
||||
if (contentType) {
|
||||
if (contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) {
|
||||
files.push({
|
||||
url: attachment.url,
|
||||
desc: attachment.description ?? attachment.comment,
|
||||
|
@ -306,13 +302,13 @@ async function processUrl(url) {
|
|||
attachment.pleroma?.mime_type ?? type.indexOf("/") > -1
|
||||
? type
|
||||
: type +
|
||||
"/" +
|
||||
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image"
|
||||
? "png"
|
||||
: type == "video"
|
||||
"/" +
|
||||
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image"
|
||||
? "png"
|
||||
: type == "video"
|
||||
? "mp4"
|
||||
: "mpeg");
|
||||
if(type.startsWith("image") || type.startsWith("video") || type.startsWith("audio")) {
|
||||
if (type.startsWith("image") || type.startsWith("video") || type.startsWith("audio")) {
|
||||
files.push({
|
||||
url: attachment.url,
|
||||
desc: attachment.description ?? attachment.comment,
|
||||
|
@ -323,11 +319,11 @@ async function processUrl(url) {
|
|||
}
|
||||
}
|
||||
|
||||
if(postData2.sensitive && attachments.length > 0) {
|
||||
if (postData2.sensitive && attachments.length > 0) {
|
||||
spoiler = true;
|
||||
}
|
||||
|
||||
if(postData2.poll) {
|
||||
if (postData2.poll) {
|
||||
poll = {
|
||||
end: new Date(postData2.poll.expires_at),
|
||||
total: postData2.poll.votes_count,
|
||||
|
@ -340,9 +336,9 @@ async function processUrl(url) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if(postData.id) {
|
||||
if (postData.id) {
|
||||
const realUrlObj = new URL(postData.id);
|
||||
if(realUrlObj.origin != urlObj.origin) {
|
||||
if (realUrlObj.origin != urlObj.origin) {
|
||||
platform = await resolvePlatform(postData.id);
|
||||
platformName = platform.replace("gotosocial", "GoToSocial").replace(/^(.)/, (_, c) => c.toUpperCase());
|
||||
url = postData.id;
|
||||
|
@ -354,32 +350,32 @@ async function processUrl(url) {
|
|||
timestamp = postData.published;
|
||||
sensitive = postData.sensitive;
|
||||
|
||||
if(postData.tag) {
|
||||
if (postData.tag) {
|
||||
let tag = postData.tag;
|
||||
// gts moment
|
||||
if(!Array.isArray(tag)) tag = [tag];
|
||||
emotes = tag.filter((x) => !!x.icon).map((x) => ({name: x.name, url: x.icon.url}));
|
||||
if (!Array.isArray(tag)) tag = [tag];
|
||||
emotes = tag.filter((x) => !!x.icon).map((x) => ({ name: x.name, url: x.icon.url }));
|
||||
}
|
||||
|
||||
// NB: gts doesnt send singular attachments as array
|
||||
const attachments = Array.isArray(postData.attachment) ? postData.attachment : [postData.attachment];
|
||||
for(const attachment of attachments) {
|
||||
if(!attachment) continue;
|
||||
if(attachment.mediaType) {
|
||||
if(attachment.mediaType.startsWith("video/") || attachment.mediaType.startsWith("image/") || attachment.mediaType.startsWith("audio/")) {
|
||||
for (const attachment of attachments) {
|
||||
if (!attachment) continue;
|
||||
if (attachment.mediaType) {
|
||||
if (attachment.mediaType.startsWith("video/") || attachment.mediaType.startsWith("image/") || attachment.mediaType.startsWith("audio/")) {
|
||||
files.push({
|
||||
url: attachment.url,
|
||||
desc: attachment.name ?? attachment.description ?? attachment.comment,
|
||||
type: attachment.mediaType,
|
||||
});
|
||||
}
|
||||
} else if(attachment.url) {
|
||||
} else if (attachment.url) {
|
||||
const contentType = await fetch(attachment.url, {
|
||||
method: "HEAD",
|
||||
}).then((res) => res.headers.get("Content-Type"));
|
||||
|
||||
if(contentType) {
|
||||
if(contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) {
|
||||
if (contentType) {
|
||||
if (contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) {
|
||||
files.push({
|
||||
url: attachment.url,
|
||||
desc: attachment.name ?? attachment.description ?? attachment.comment,
|
||||
|
@ -393,9 +389,9 @@ async function processUrl(url) {
|
|||
type.indexOf("/") > -1
|
||||
? type
|
||||
: type +
|
||||
"/" +
|
||||
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image" ? "png" : type == "video" ? "mp4" : "mpeg");
|
||||
if(type.startsWith("image") || type.startsWith("video") || type.startsWith("audio")) {
|
||||
"/" +
|
||||
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image" ? "png" : type == "video" ? "mp4" : "mpeg");
|
||||
if (type.startsWith("image") || type.startsWith("video") || type.startsWith("audio")) {
|
||||
files.push({
|
||||
url: attachment.url,
|
||||
desc: attachment.name ?? attachment.description ?? attachment.comment,
|
||||
|
@ -408,11 +404,11 @@ async function processUrl(url) {
|
|||
}
|
||||
}
|
||||
|
||||
if(postData.sensitive && attachments.length > 0) {
|
||||
if (postData.sensitive && attachments.length > 0) {
|
||||
spoiler = true;
|
||||
}
|
||||
|
||||
if(postData.image?.url) {
|
||||
if (postData.image?.url) {
|
||||
const imageUrl = new URL(postData.image.url);
|
||||
const contentType = await fetch(postData.image.url, {
|
||||
method: "HEAD",
|
||||
|
@ -424,7 +420,7 @@ async function processUrl(url) {
|
|||
});
|
||||
}
|
||||
|
||||
if(postData.name) title = postData.name;
|
||||
if (postData.name) title = postData.name;
|
||||
|
||||
// Author data is not sent with the post with AS2
|
||||
const authorData = await signedFetch(postData.actor ?? postData.attributedTo, {
|
||||
|
@ -436,10 +432,10 @@ async function processUrl(url) {
|
|||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
// only posts can be activity+json right now, reduce log spam
|
||||
if(platform !== "cohost") console.error("[fedimbed]", `Failed to get author for "${url}": ${err}`);
|
||||
if (platform !== "cohost") console.error("[fedimbed]", `Failed to get author for "${url}": ${err}`);
|
||||
});
|
||||
|
||||
if(authorData) {
|
||||
if (authorData) {
|
||||
const authorUrlObj = new URL(authorData.url ?? authorData.id);
|
||||
author = {
|
||||
name: authorData.name,
|
||||
|
@ -459,7 +455,7 @@ async function processUrl(url) {
|
|||
};
|
||||
}
|
||||
|
||||
if(postData.endTime && postData.oneOf && postData.votersCount) {
|
||||
if (postData.endTime && postData.oneOf && postData.votersCount) {
|
||||
poll = {
|
||||
end: new Date(postData.endTime),
|
||||
total: postData.votersCount,
|
||||
|
@ -472,7 +468,7 @@ async function processUrl(url) {
|
|||
}
|
||||
|
||||
// We could just continue without author but it'd look ugly and be confusing.
|
||||
if(!author) {
|
||||
if (!author) {
|
||||
console.warn("[fedimbed]", `Bailing trying to re-embed "${url}": Failed to get author.`);
|
||||
return {};
|
||||
}
|
||||
|
@ -489,7 +485,7 @@ async function processUrl(url) {
|
|||
//cw = htmlToMarkdown(cw);
|
||||
|
||||
let desc = cw;
|
||||
if((cw != "" || sensitive) && files.length) {
|
||||
if ((cw != "" || sensitive) && files.length) {
|
||||
desc += "<span data-mx-spoiler>" + content + "</span>";
|
||||
} else {
|
||||
desc = content;
|
||||
|
@ -504,9 +500,9 @@ async function processUrl(url) {
|
|||
title: title ?? user,
|
||||
author: title
|
||||
? {
|
||||
name: user,
|
||||
url: author.url,
|
||||
}
|
||||
name: user,
|
||||
url: author.url,
|
||||
}
|
||||
: null,
|
||||
footer: {
|
||||
text: platformName,
|
||||
|
@ -517,7 +513,7 @@ async function processUrl(url) {
|
|||
fields: [],
|
||||
};
|
||||
|
||||
if(poll) {
|
||||
if (poll) {
|
||||
baseEmbed.fields.push({
|
||||
name: "Poll",
|
||||
value:
|
||||
|
@ -541,8 +537,8 @@ async function processUrl(url) {
|
|||
cw != "" && (files.length > 0)
|
||||
? `:warning: ${cw} <span data-mx-spoiler>${url}</span>`
|
||||
: spoiler
|
||||
? `<span data-mx-spoiler>${url}</span>`
|
||||
: "",
|
||||
? `<span data-mx-spoiler>${url}</span>`
|
||||
: "",
|
||||
embeds,
|
||||
files,
|
||||
emotes
|
||||
|
@ -550,9 +546,9 @@ async function processUrl(url) {
|
|||
}
|
||||
|
||||
async function fedimbed(msg) {
|
||||
if(URLS_REGEX.test(msg)) {
|
||||
if (URLS_REGEX.test(msg)) {
|
||||
const urls = msg.match(URLS_REGEX);
|
||||
for(let url of urls) {
|
||||
for (let url of urls) {
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
|
@ -560,9 +556,9 @@ async function fedimbed(msg) {
|
|||
console.error("[fedimbed]", `Invalid URL "${url}"`);
|
||||
// noop
|
||||
}
|
||||
for(const service of Object.keys(PATH_REGEX)) {
|
||||
for (const service of Object.keys(PATH_REGEX)) {
|
||||
const regex = PATH_REGEX[service];
|
||||
if(urlObj && regex.test(urlObj.pathname)) {
|
||||
if (urlObj && regex.test(urlObj.pathname)) {
|
||||
console.log("[fedimbed]", `Hit "${service}" for "${url}", processing now.`);
|
||||
try {
|
||||
const response = await processUrl(url);
|
||||
|
|
39
login.js
39
login.js
|
@ -1,5 +1,38 @@
|
|||
import { LocalStorage } from 'node-localstorage';
|
||||
import readline from "node:readline/promises";
|
||||
import { stdin, stdout } from 'node:process';
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
import readline from "node:readline";
|
||||
import Olm from "@matrix-org/olm";
|
||||
global.Olm = Olm;
|
||||
|
||||
const { stdin: input, stdout: output } = require('node:process');
|
||||
const rl = readline.createInterface({ input, output });
|
||||
const rl = readline.createInterface({ input: stdin, output: stdout });
|
||||
|
||||
const host = await rl.question("Server: ");
|
||||
const user = await rl.question("Username: ");
|
||||
const pass = await rl.question("Password: ");
|
||||
const idnt = await rl.question("Device ID: ");
|
||||
|
||||
const userId = `@${user}:${host}`;
|
||||
|
||||
const localStorage = new LocalStorage("./data/localstorage");
|
||||
const cryptoStore = new sdk.LocalStorageCryptoStore(localStorage);
|
||||
const store = new sdk.MemoryStore({ localStorage });
|
||||
|
||||
const client = sdk.createClient({
|
||||
baseUrl: "https://" + host,
|
||||
deviceId: idnt,
|
||||
cryptoStore,
|
||||
store
|
||||
});
|
||||
|
||||
await client.loginWithPassword(userId, pass);
|
||||
await client.initCrypto();
|
||||
await client.startClient();
|
||||
|
||||
console.log(`BASE_URL=${client.getHomeserverUrl()}
|
||||
USER_ID=${client.getUserId()}
|
||||
ACCESS_TOKEN=${client.getAccessToken()}
|
||||
DEVICE_ID=${client.getDeviceId()}`);
|
||||
|
||||
client.stopClient();
|
||||
process.exit(0);
|
|
@ -6,6 +6,9 @@
|
|||
"main": "index.js",
|
||||
"author": "Ashley Graves",
|
||||
"license": "GPL-v3",
|
||||
"scripts": {
|
||||
"login": "node login.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@matrix-org/olm": "^3.2.15",
|
||||
"@node-rs/xxhash": "^1.7.3",
|
||||
|
@ -17,11 +20,12 @@
|
|||
"jsdom": "^25.0.0",
|
||||
"matrix-js-sdk": "^32.4.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-localstorage": "^3.0.5"
|
||||
"node-localstorage": "^3.0.5",
|
||||
"openssl-wrapper": "^0.3.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"jsdom>tough-cookie": "^5.0.0-rc.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
privkey.pem
Normal file
28
privkey.pem
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCscpi8oYpieOQx
|
||||
rysZVAtDBQHoKuQdtrz4zlLnZoAsWKkCgN5Df/+nxIrvp+yURnrrDPuNqTvxoo/n
|
||||
mP88cuz8xAH8Gvnc6aZNrLENQwo02LscfxlAh+sXThaYN1qzf+jkJpPcy4sCXYam
|
||||
7E/O7RYwRBPPw3JgMZgdjgX1IRTHmHUaAlTAcB78FFZIn0p2UGCFHSOrIRSOL8ga
|
||||
e1s7alOfSqTF92N78lnbprAqxBbqiYTEY4CyTdqKZrbHjSvJcNjxdcifvmC/FIqC
|
||||
PBmx49FMeUTKyY6Px5/eYcNY/MWjUCIR+65tGLftDJ1hlcZO4w3/qdKDmV/ErTVJ
|
||||
jY1VQV1ZAgMBAAECggEABK08RIFXsMFOjvpif4FZUiv57UNHLyHG5jMnlGxMiudX
|
||||
Tgtopl6mSZ/O6h5HuuMJGllzyQJkmceq12u9dZ+Nmx1FtddAHM70BPnrTfdveMIX
|
||||
8Pc8HBU+OZeTN/WTBCMykbK/d9Fyp6cZq7/k5Nu71PtDqPLH+0vdBZArITFl0DPg
|
||||
LRhG9oQmzlo9kY19upCKtn7mj/0OCv/p20RU7fjRZJcBTaZ0G6nDJbropZrp0RqK
|
||||
AlTl+/u52cu2DpAt4EHMy3BJoNw2lk1b+ierEi9XolutO2IFoR9jvzrv3xW0wVom
|
||||
Zm+Q8F4W5yNr1/lHqSVaBswn3HII9qgTTV+O3LgAAQKBgQDgrK7oyd4E71Uzywls
|
||||
UhxQPNvGnDK7O13vVkrnyz93aUPXkkZE1EPviaRmXFKNVl1M5tjpIuyOSjl2o2M+
|
||||
mpxAue526+l9zt28DGa4CyJ5y/frezI95lOaJx03QLfrD6OPXZSpqNvk+ozyPl6s
|
||||
ZnB9u0avHDW4oWXgTd4+DPy1WQKBgQDEfcbVkkHTyJxiqhV9uax8WhNPxmRbnsft
|
||||
0VFDSVBmqrwDpjAAlSrXegr44VPabbY8ioq9ExIaGVmihr8VpGkWmSAJIpHw+sjR
|
||||
ml+r5irl8t+C7cEJJra60uuQ/SevgG9YbgWMXKvuzzYRlhcU8w5vBXccyaMEkMnV
|
||||
o4HCqczoAQKBgFG4vg5Sjv1AiL9EFPNfkojk+hPt8M4FtA9AVhKl7TnkAhdBT2nt
|
||||
w6A3cqMA5c4fIeS8+x0h5OWEvg4XNBwrZLZuavy6pr1qJ2ElKZ+7/RhMRqtSrl2x
|
||||
j6s8mvXkBoHruFSHd3GWyBUrxWS/pvQSdsxk+DrtieTUYBgMetAbLThRAoGADiIl
|
||||
7TLJ/VvOs+IcDaEPYRpxjSluCpEPPHHz8G0TlW7ueyy3AzO3kyw6IdDDYVDG7O/i
|
||||
LttyT+JG6kPa9smOrYtyHHYaHUVMsJb0Dr9NkqC3pwlG+0uHlUSaoSE0e5E3cRro
|
||||
10HLNDA/aWBsZJtfDGlOOgne6fMoMW/DY6cnEAECgYAQV8szcOywT7xxu8nJPJyV
|
||||
nhcbqvJrPHdHZW01EPTtYRICOd5WFyw/bvULpWNKJm+aV5H+Q5JLFiJyxhJ6+juY
|
||||
9LuJ1LHhnvEUmnn5ZOmDcjI0RvPZ4hFjIHslAKrizZ8fKdQLBW34c7GGMEHiN4S9
|
||||
2QyE9tbfBp0u0pDBuR56wQ==
|
||||
-----END PRIVATE KEY-----
|
|
@ -1,4 +1,4 @@
|
|||
# PossumBot
|
||||
modular matrix bot :3
|
||||
|
||||
see: [wiki](https://git.lgbt/root/possumbot/wiki)
|
||||
see: [wiki](https://git.lgbt/sisterkissers/matrix-bot/wiki)
|
Loading…
Reference in a new issue