forked from keinBot/cuffeo
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1641c0ae | ||
|
|
763082a59b | ||
|
|
ae2a88dbbc | ||
|
|
a0bfa2db1a | ||
|
|
14e5be0b2b | ||
|
|
4e342540bf | ||
|
|
bbc847a31b | ||
|
|
97820479b2 | ||
| 303baa25ad |
@@ -1,7 +1,7 @@
|
|||||||
import Discord from "discord.js";
|
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
|
||||||
export class discord extends EventEmitter {
|
export default class discord extends EventEmitter {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super();
|
super();
|
||||||
this.options = options || {};
|
this.options = options || {};
|
||||||
@@ -9,7 +9,17 @@ export class discord extends EventEmitter {
|
|||||||
this.set = this.options.set || "all";
|
this.set = this.options.set || "all";
|
||||||
this.network = "discord";
|
this.network = "discord";
|
||||||
|
|
||||||
this.bot = new Discord.Client();
|
this.bot = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
GatewayIntentBits.DirectMessages,
|
||||||
|
GatewayIntentBits.GuildMessageReactions
|
||||||
|
],
|
||||||
|
partials: [Partials.Channel]
|
||||||
|
});
|
||||||
|
|
||||||
this.bot.login(this.token);
|
this.bot.login(this.token);
|
||||||
|
|
||||||
this.server = {
|
this.server = {
|
||||||
@@ -19,7 +29,7 @@ export class discord extends EventEmitter {
|
|||||||
me: {}
|
me: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.bot.on("ready", () => {
|
this.bot.on("clientReady", () => {
|
||||||
this.server.me = {
|
this.server.me = {
|
||||||
nickname: this.bot.user.username,
|
nickname: this.bot.user.username,
|
||||||
username: this.bot.user.username,
|
username: this.bot.user.username,
|
||||||
@@ -27,18 +37,29 @@ export class discord extends EventEmitter {
|
|||||||
prefix: `${this.bot.user.username}!${this.bot.user.id.toString()}`,
|
prefix: `${this.bot.user.username}!${this.bot.user.id.toString()}`,
|
||||||
id: this.bot.user.id.toString()
|
id: this.bot.user.id.toString()
|
||||||
};
|
};
|
||||||
|
this.emit("data", ["ready", "discord"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bot.on("message", msg => {
|
this.bot.on("messageCreate", msg => {
|
||||||
if(msg.author.id !== this.server.me.id)
|
// Ignore messages from self
|
||||||
this.emit("data", ["message", this.reply(msg)]);
|
if (msg.author.id === this.bot.user.id) return;
|
||||||
|
|
||||||
|
console.log(`[DISCORD DEBUG] Received message: '${msg.content}' from ${msg.author.username} in ${msg.channel.name || 'DM'}`);
|
||||||
|
|
||||||
|
const replyObj = this.reply(msg);
|
||||||
|
console.log(`[DISCORD DEBUG] Emitting data event for message:`, replyObj.message);
|
||||||
|
this.emit("data", ["message", replyObj]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optional: Log errors to avoid crashing
|
||||||
|
this.bot.on("error", console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
reply(tmp) {
|
reply(tmp) {
|
||||||
return {
|
return {
|
||||||
type: "discord",
|
type: "discord",
|
||||||
network: "Discord",
|
network: "Discord",
|
||||||
channel: tmp.channel.name,
|
channel: tmp.channel.name || 'DM',
|
||||||
channelid: tmp.channel.id,
|
channelid: tmp.channel.id,
|
||||||
user: {
|
user: {
|
||||||
prefix: `${tmp.author.username}!${tmp.author.id}`,
|
prefix: `${tmp.author.username}!${tmp.author.id}`,
|
||||||
@@ -47,27 +68,49 @@ export class discord extends EventEmitter {
|
|||||||
account: tmp.author.id.toString()
|
account: tmp.author.id.toString()
|
||||||
},
|
},
|
||||||
message: tmp.content,
|
message: tmp.content,
|
||||||
|
media: tmp.attachments.find(a =>
|
||||||
|
a.contentType?.startsWith('video/mp4') ||
|
||||||
|
a.contentType?.startsWith('video/webm')
|
||||||
|
)?.url,
|
||||||
time: ~~(Date.now() / 1000),
|
time: ~~(Date.now() / 1000),
|
||||||
|
raw: tmp,
|
||||||
self: this.server,
|
self: this.server,
|
||||||
reply: msg => this.send(tmp, this.format(msg)),
|
reply: msg => { return this.send(tmp, this.format(msg)); },
|
||||||
replyAction: msg => this.send(tmp, this.format(`*${msg}*`), "normal"),
|
replyAction: msg => this.send(tmp, this.format(`*${msg}*`), "normal"),
|
||||||
replyNotice: msg => this.send(tmp, this.format(msg))
|
replyNotice: msg => this.send(tmp, this.format(msg))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
send(r, msg, mode = "blah") {
|
|
||||||
|
async send(r, msg, mode = "blah") {
|
||||||
|
try {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "normal":
|
case "normal":
|
||||||
r.channel.send(msg);
|
return await r.channel.send(msg);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
r.reply(msg);
|
return await r.reply(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to send message:", e);
|
||||||
}
|
}
|
||||||
sendmsg(mode, recipient, msg) {
|
|
||||||
this.bot.channels.get(recipient).send(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendmsg(mode, recipient, msg) {
|
||||||
|
try {
|
||||||
|
const channel = this.bot.channels.cache.get(recipient) || await this.bot.channels.fetch(recipient).catch(() => null);
|
||||||
|
if (channel) {
|
||||||
|
await channel.send(msg);
|
||||||
|
} else {
|
||||||
|
console.error(`Channel ${recipient} not found or not cached`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to sendmsg:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
format(msg) {
|
format(msg) {
|
||||||
|
if (typeof msg === 'object') return msg;
|
||||||
return msg.toString()
|
return msg.toString()
|
||||||
.replace(/\[b\](.*?)\[\/b\]/g, "**$1**") // bold
|
.replace(/\[b\](.*?)\[\/b\]/g, "**$1**") // bold
|
||||||
.replace(/\[i\](.*?)\[\/i\]/g, "*$1*") // italic
|
.replace(/\[i\](.*?)\[\/i\]/g, "*$1*") // italic
|
||||||
|
|||||||
329
src/clients/matrix.mjs
Normal file
329
src/clients/matrix.mjs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import { createClient } from "matrix-js-sdk";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
export default class matrix extends EventEmitter {
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
this.options = options || {};
|
||||||
|
this.baseUrl = options.baseUrl || "https://matrix.org";
|
||||||
|
this.token = options.token || null;
|
||||||
|
this.userId = options.userId || null;
|
||||||
|
if (!this.userId && this.token) {
|
||||||
|
// Try to derive userId if not provided but we have a token?
|
||||||
|
// Actually usually you need userId to init client or it can be inferred?
|
||||||
|
// sdk.createClient({ baseUrl, accessToken, userId })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set = this.options.set || "all";
|
||||||
|
this.network = "Matrix";
|
||||||
|
|
||||||
|
// Custom logger to suppress FetchHttpApi debug spam
|
||||||
|
const silentLogger = {
|
||||||
|
debug: () => {}, // Suppress debug
|
||||||
|
info: (msg, ...args) => console.info(`[Matrix] ${msg}`, ...args),
|
||||||
|
warn: (msg, ...args) => console.warn(`[Matrix] ${msg}`, ...args),
|
||||||
|
error: (msg, ...args) => console.error(`[Matrix] ${msg}`, ...args),
|
||||||
|
trace: () => {},
|
||||||
|
getChild: () => silentLogger
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = createClient({
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
accessToken: this.token,
|
||||||
|
userId: this.userId,
|
||||||
|
logger: silentLogger
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server = {
|
||||||
|
set: this.set,
|
||||||
|
channel: new Map(),
|
||||||
|
user: new Map(),
|
||||||
|
me: {},
|
||||||
|
download: (mxcUrl) => this.download(mxcUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (async () => {
|
||||||
|
await this.start();
|
||||||
|
return this;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await this.client.startClient({ initialSyncLimit: 10 });
|
||||||
|
|
||||||
|
this.client.once('sync', async (state, prevState, res) => {
|
||||||
|
if (state === 'PREPARED') {
|
||||||
|
const me = await this.client.getProfileInfo(this.client.getUserId());
|
||||||
|
this.server.me = {
|
||||||
|
nickname: me.displayname || this.client.getUserId(),
|
||||||
|
username: this.client.getUserId(),
|
||||||
|
account: this.client.getUserId(),
|
||||||
|
prefix: `${this.client.getUserId()}!matrix`, // simplified
|
||||||
|
id: this.client.getUserId()
|
||||||
|
};
|
||||||
|
this.emit("data", ["ready", "matrix"]);
|
||||||
|
|
||||||
|
if (this.options.channels) {
|
||||||
|
for (const channel of this.options.channels) {
|
||||||
|
try {
|
||||||
|
await this.client.joinRoom(channel);
|
||||||
|
console.log(`[Matrix] Joined room: ${channel}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Matrix] Failed to join room ${channel}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on("Room.timeline", (event, room, toStartOfTimeline) => {
|
||||||
|
if (toStartOfTimeline) return;
|
||||||
|
|
||||||
|
if (event.getType() !== "m.room.message") return;
|
||||||
|
|
||||||
|
// Ignore own messages
|
||||||
|
if (event.getSender() === this.client.getUserId()) return;
|
||||||
|
|
||||||
|
const content = event.getContent();
|
||||||
|
if (!content || !content.body) return;
|
||||||
|
|
||||||
|
if (['m.text', 'm.notice', 'm.emote'].indexOf(content.msgtype) === -1) return;
|
||||||
|
|
||||||
|
const age = Date.now() - event.getTs();
|
||||||
|
if (age > 60000) return; // Ignore old events (1m)
|
||||||
|
|
||||||
|
// Check if DM (<= 2 members)
|
||||||
|
const memberCount = room.getJoinedMemberCount();
|
||||||
|
// if (memberCount <= 2) {
|
||||||
|
// We can log it if needed, but return early to avoid trigger processing
|
||||||
|
// console.log(`[Matrix] Ignoring DM from ${event.getSender()}`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const msgData = await this.reply(event, room);
|
||||||
|
this.emit("data", ["message", msgData]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Matrix] Error processing message:`, err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling?
|
||||||
|
// this.client.on("error", ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reply(event, room) {
|
||||||
|
const senderId = event.getSender();
|
||||||
|
const senderMember = room.getMember(senderId);
|
||||||
|
const senderName = senderMember ? senderMember.name : senderId; // fallback
|
||||||
|
|
||||||
|
const content = event.getContent();
|
||||||
|
|
||||||
|
let message = content.body;
|
||||||
|
if (content.msgtype === 'm.emote') {
|
||||||
|
message = `* ${senderName} ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle replies
|
||||||
|
let replyTo = null;
|
||||||
|
const relation = content['m.relates_to'];
|
||||||
|
|
||||||
|
if (relation && relation['m.in_reply_to']) {
|
||||||
|
const relatedEventId = relation['m.in_reply_to'].event_id;
|
||||||
|
try {
|
||||||
|
let relatedEvent = room.findEventById(relatedEventId);
|
||||||
|
|
||||||
|
if (!relatedEvent) {
|
||||||
|
try {
|
||||||
|
const fetched = await this.client.fetchRoomEvent(room.roomId, relatedEventId);
|
||||||
|
if (fetched.getContent) {
|
||||||
|
relatedEvent = fetched;
|
||||||
|
} else {
|
||||||
|
// Create a mock object if it's raw JSON
|
||||||
|
relatedEvent = {
|
||||||
|
getContent: () => fetched.content,
|
||||||
|
getSender: () => fetched.sender,
|
||||||
|
getType: () => fetched.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (fetchErr) {
|
||||||
|
console.error(`[Matrix] Failed to fetch remote event ${relatedEventId}:`, fetchErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedEvent) {
|
||||||
|
const relatedContent = relatedEvent.getContent();
|
||||||
|
const isMedia = ['m.image', 'm.video', 'm.audio', 'm.file'].includes(relatedContent.msgtype);
|
||||||
|
let mediaUrl = null;
|
||||||
|
let mxcUrl = null;
|
||||||
|
if (isMedia && relatedContent.url) {
|
||||||
|
mxcUrl = relatedContent.url;
|
||||||
|
mediaUrl = this.client.mxcUrlToHttp(relatedContent.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
replyTo = {
|
||||||
|
sender: relatedEvent.getSender(),
|
||||||
|
message: relatedContent.body,
|
||||||
|
url: mediaUrl,
|
||||||
|
mxcUrl: mxcUrl,
|
||||||
|
type: relatedContent.msgtype,
|
||||||
|
filename: relatedContent.filename || relatedContent.body, // fallback to body if filename missing
|
||||||
|
mimetype: relatedContent.info?.mimetype || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[Matrix] Failed to resolve reply event ${relatedEventId}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "matrix",
|
||||||
|
network: "Matrix",
|
||||||
|
channel: room.name || "Room",
|
||||||
|
channelid: room.roomId,
|
||||||
|
user: {
|
||||||
|
prefix: `${senderId}!${senderId}`,
|
||||||
|
nick: senderName,
|
||||||
|
username: senderId, // Matrix users are defined by ID usually
|
||||||
|
account: senderId
|
||||||
|
},
|
||||||
|
message: message,
|
||||||
|
replyTo: replyTo,
|
||||||
|
time: ~~(event.getTs() / 1000),
|
||||||
|
raw: event,
|
||||||
|
self: this.server,
|
||||||
|
reply: (msg) => this.send(room.roomId, this.format(msg)),
|
||||||
|
replyAction: (msg) => this.send(room.roomId, this.format(msg), "emote"),
|
||||||
|
replyNotice: (msg) => this.send(room.roomId, this.format(msg), "notice")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(mxcUrl) {
|
||||||
|
if (!mxcUrl) throw new Error("No MXC URL provided");
|
||||||
|
|
||||||
|
const accessToken = this.client.getAccessToken();
|
||||||
|
if (!accessToken) console.warn("[Matrix] No access token available for download!");
|
||||||
|
|
||||||
|
const tryDownload = async (url) => {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errText = await res.text();
|
||||||
|
throw new Error(`HTTP Error ${res.status}: ${res.statusText} - ${errText}`);
|
||||||
|
}
|
||||||
|
return await res.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse MXC
|
||||||
|
const cleanMxc = mxcUrl.replace('mxc://', '');
|
||||||
|
const [serverName, mediaId] = cleanMxc.split('/');
|
||||||
|
|
||||||
|
if (!serverName || !mediaId) {
|
||||||
|
throw new Error(`Invalid MXC URL format: ${mxcUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 1: Authenticated Client API (MSC3916 / Matrix 1.11+)
|
||||||
|
const baseUrl = this.client.baseUrl;
|
||||||
|
const authUrl = `${baseUrl}/_matrix/client/v1/media/download/${serverName}/${mediaId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ab = await tryDownload(authUrl);
|
||||||
|
return Buffer.from(ab);
|
||||||
|
} catch (e1) {
|
||||||
|
// console.warn(`[Matrix] Authenticated Client API failed: ${e1.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Legacy Media API (v3) with Auth
|
||||||
|
const v3Url = `${baseUrl}/_matrix/media/v3/download/${serverName}/${mediaId}`;
|
||||||
|
try {
|
||||||
|
const ab = await tryDownload(v3Url);
|
||||||
|
return Buffer.from(ab);
|
||||||
|
} catch (e2) {
|
||||||
|
// console.warn(`[Matrix] Legacy v3 API failed: ${e2.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("All download strategies failed.");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Matrix] Download failed:`, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format(msg) {
|
||||||
|
if (typeof msg === 'object') return msg; // Should probably be stringified or handled?
|
||||||
|
// simple bbcode to markdown/html or just strip?
|
||||||
|
// Matrix supports HTML.
|
||||||
|
// Reuse logic from others if needed, but for now simple replacement
|
||||||
|
return msg.toString()
|
||||||
|
.replace(/\[b\](.*?)\[\/b\]/g, "**$1**")
|
||||||
|
.replace(/\[i\](.*?)\[\/i\]/g, "*$1*");
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(roomId, msg, type = "text") {
|
||||||
|
let content = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'emote') content.msgtype = 'm.emote';
|
||||||
|
if (type === 'notice') content.msgtype = 'm.notice';
|
||||||
|
|
||||||
|
if (typeof msg === 'object') {
|
||||||
|
content.body = msg.body || JSON.stringify(msg);
|
||||||
|
if (msg.formatted_body) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = msg.formatted_body;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// String handling
|
||||||
|
content.body = msg.toString();
|
||||||
|
// Simple auto-formatting check
|
||||||
|
if (content.body.includes('**') || content.body.includes('*')) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = content.body
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>")
|
||||||
|
.replace(/\*(.*?)\*/g, "<i>$1</i>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure roomId is valid
|
||||||
|
if (!roomId) throw new Error("No roomId provided for send");
|
||||||
|
|
||||||
|
await this.client.sendEvent(roomId, "m.room.message", content, "");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Matrix] Failed to send message:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendmsg(mode, recipient, msg) {
|
||||||
|
if (recipient.startsWith('@')) {
|
||||||
|
console.log(`[Matrix] Sending DM to ${recipient}`);
|
||||||
|
try {
|
||||||
|
// Always create a fresh DM room
|
||||||
|
const createOpts = {
|
||||||
|
visibility: "private",
|
||||||
|
invite: [recipient],
|
||||||
|
is_direct: true
|
||||||
|
};
|
||||||
|
const result = await this.client.createRoom(createOpts);
|
||||||
|
if (result && result.room_id) {
|
||||||
|
await this.send(result.room_id, msg);
|
||||||
|
console.log(`[Matrix] Sent DM to ${recipient} in new room ${result.room_id}`);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(`[Matrix] Failed to send DM to ${recipient}:`, e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipient should be a roomId for channel/room messages
|
||||||
|
await this.send(recipient, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user