mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
use webhooks
This commit is contained in:
@@ -3,7 +3,18 @@
|
|||||||
"guildId": "700146429359816735",
|
"guildId": "700146429359816735",
|
||||||
"channelId": "1441685291944513566",
|
"channelId": "1441685291944513566",
|
||||||
"mode": "global",
|
"mode": "global",
|
||||||
|
"webhookId": "1460150998717956309",
|
||||||
|
"webhookToken": "Lq0prLr8rCx73ooYLv5X402s9cD_Kd6HsOJiKs1obvrNwq4KX--9bSNX01zxraXTiZ8S",
|
||||||
"createdAt": 1768169994193,
|
"createdAt": 1768169994193,
|
||||||
"updatedAt": 1768169994193
|
"updatedAt": 1768197526600
|
||||||
|
},
|
||||||
|
"800083965666132008": {
|
||||||
|
"guildId": "800083965666132008",
|
||||||
|
"channelId": "870272768334233601",
|
||||||
|
"mode": "global",
|
||||||
|
"webhookId": "1460154683552301066",
|
||||||
|
"webhookToken": "-4mVd50-lnnGXiZp2G8Sg0Y0wgnXKj5QLtRA6igX_PLWfGLsGtuMMmWHl-eVhwt38MFb",
|
||||||
|
"createdAt": 1768198405065,
|
||||||
|
"updatedAt": 1768198405067
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const {
|
|||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
AttachmentBuilder,
|
AttachmentBuilder,
|
||||||
PermissionsBitField,
|
PermissionsBitField,
|
||||||
|
WebhookClient,
|
||||||
} = require('discord.js');
|
} = require('discord.js');
|
||||||
const logger = require('../globals/logger').child('discordBot');
|
const logger = require('../globals/logger').child('discordBot');
|
||||||
const io = require('../globals/io');
|
const io = require('../globals/io');
|
||||||
@@ -425,6 +426,43 @@ function canManageBridge(message) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canManageWebhooksInChannel(channel) {
|
||||||
|
if (!channel?.guild) return false;
|
||||||
|
const botMember = channel.guild.members?.me;
|
||||||
|
const perms = channel.permissionsFor(botMember);
|
||||||
|
if (!perms) return false;
|
||||||
|
return perms.has(PermissionsBitField.Flags.ManageWebhooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureBridgeWebhook(channel, guildId) {
|
||||||
|
if (!channel?.id || !guildId) return null;
|
||||||
|
if (!canManageWebhooksInChannel(channel)) {
|
||||||
|
throw new Error('Missing Manage Webhooks permission in this channel.');
|
||||||
|
}
|
||||||
|
const existing = getGuildConfig(guildId);
|
||||||
|
if (
|
||||||
|
existing?.channelId &&
|
||||||
|
String(existing.channelId) === String(channel.id) &&
|
||||||
|
existing?.webhookId &&
|
||||||
|
existing?.webhookToken
|
||||||
|
) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const webhook = await channel.createWebhook({
|
||||||
|
name: 'Rover Chat Bridge',
|
||||||
|
reason: 'Rover chat bridge webhook',
|
||||||
|
});
|
||||||
|
if (!webhook?.id || !webhook?.token) {
|
||||||
|
throw new Error('Failed to create webhook.');
|
||||||
|
}
|
||||||
|
return setGuildConfig(guildId, {
|
||||||
|
channelId: channel.id,
|
||||||
|
mode: existing?.mode || 'global',
|
||||||
|
webhookId: webhook.id,
|
||||||
|
webhookToken: webhook.token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatBridgeStatus(entry) {
|
function formatBridgeStatus(entry) {
|
||||||
if (!entry) return 'Chat bridge is not configured for this server.';
|
if (!entry) return 'Chat bridge is not configured for this server.';
|
||||||
return `Chat bridge is **${entry.mode}** in <#${entry.channelId}>.`;
|
return `Chat bridge is **${entry.mode}** in <#${entry.channelId}>.`;
|
||||||
@@ -482,15 +520,22 @@ async function handleBridgeCommand(message, tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'here') {
|
if (action === 'here') {
|
||||||
const channelId = message.channelId;
|
try {
|
||||||
const entry = setGuildConfig(guildId, {
|
const entry = await ensureBridgeWebhook(message.channel, guildId);
|
||||||
channelId,
|
if (mode) {
|
||||||
mode: normalizeMode(mode, getGuildConfig(guildId)?.mode || 'global'),
|
setGuildConfig(guildId, { channelId: entry.channelId, mode, webhookId: entry.webhookId, webhookToken: entry.webhookToken });
|
||||||
});
|
}
|
||||||
|
const updated = getGuildConfig(guildId);
|
||||||
await message.reply({
|
await message.reply({
|
||||||
content: `Chat bridge set to **${entry.mode}** in <#${entry.channelId}>.`,
|
content: `Chat bridge set to **${updated.mode}** in <#${updated.channelId}>.`,
|
||||||
allowedMentions: { parse: [], repliedUser: false },
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await message.reply({
|
||||||
|
content: `Failed to set chat bridge: ${err.message}`,
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +556,12 @@ async function handleBridgeCommand(message, tokens) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const entry = setGuildConfig(guildId, { channelId: current.channelId, mode: nextMode });
|
const entry = setGuildConfig(guildId, {
|
||||||
|
channelId: current.channelId,
|
||||||
|
mode: nextMode,
|
||||||
|
webhookId: current.webhookId,
|
||||||
|
webhookToken: current.webhookToken,
|
||||||
|
});
|
||||||
await message.reply({
|
await message.reply({
|
||||||
content: `Chat bridge mode updated to **${entry.mode}** in <#${entry.channelId}>.`,
|
content: `Chat bridge mode updated to **${entry.mode}** in <#${entry.channelId}>.`,
|
||||||
allowedMentions: { parse: [], repliedUser: false },
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
@@ -584,21 +634,27 @@ async function handleCommand(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChatLine(payload) {
|
function formatWebhookUsername(payload) {
|
||||||
const name = payload.nickname || payload.socketId?.slice(0, 6) || 'unknown';
|
const name = payload.nickname || payload.socketId?.slice(0, 6) || 'unknown';
|
||||||
const roleTag =
|
if (payload.fromDiscord) {
|
||||||
payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin'
|
const origin = payload.discordGuildName ? ` (From: ${payload.discordGuildName})` : '';
|
||||||
? '`Admin`'
|
const isAdmin =
|
||||||
: null;
|
payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin';
|
||||||
const roverTag = payload.roverId ? `\`Rover: ${payload.roverId}\`` : '`Rover: none`';
|
const adminTag = isAdmin ? ' [Rover Admin]' : '';
|
||||||
|
return `${name}${origin}${adminTag}`;
|
||||||
|
}
|
||||||
const record = payload.roverId ? rovers.get(payload.roverId) : null;
|
const record = payload.roverId ? rovers.get(payload.roverId) : null;
|
||||||
const percent = record?.batteryState?.percentDisplay;
|
const percent = record?.batteryState?.percentDisplay;
|
||||||
const voltageMv = record?.lastSensor?.decoded?.voltageMv ?? record?.lastSensor?.sensors?.voltageMv;
|
const voltageMv = record?.lastSensor?.decoded?.voltageMv ?? record?.lastSensor?.sensors?.voltageMv;
|
||||||
const batteryTag = percent != null ? `\`Battery: ${percent}%\`` : null;
|
const batteryText = percent != null ? `battery ${percent}%` : null;
|
||||||
const voltageTag = voltageMv != null ? `\`Voltage: ${formatVoltage(voltageMv)}\`` : null;
|
const voltageText = voltageMv != null ? `voltage ${formatVoltage(voltageMv)}` : null;
|
||||||
const tags = [roverTag, batteryTag, voltageTag, roleTag].filter(Boolean).join(' ');
|
const roverText = payload.roverId ? `rover ${payload.roverId}` : 'rover none';
|
||||||
const header = tags ? `**${name}** ${tags}` : `**${name}**`;
|
const roleText =
|
||||||
return `${header}\n${payload.text}`;
|
payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin'
|
||||||
|
? 'admin'
|
||||||
|
: null;
|
||||||
|
const suffix = [roverText, batteryText, voltageText, roleText].filter(Boolean).join(' · ');
|
||||||
|
return suffix ? `${name} · ${suffix}` : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBridgeInbound(message) {
|
async function handleBridgeInbound(message) {
|
||||||
@@ -967,17 +1023,30 @@ function handleChatBridgeOutbound(event) {
|
|||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
const guildConfigs = listGuildConfigs();
|
const guildConfigs = listGuildConfigs();
|
||||||
if (!guildConfigs.length) return;
|
if (!guildConfigs.length) return;
|
||||||
const line = formatChatLine(payload);
|
const text = payload.text?.length > 1900 ? `${payload.text.slice(0, 1897)}...` : payload.text;
|
||||||
const text = line.length > 1900 ? `${line.slice(0, 1897)}...` : line;
|
const username = formatWebhookUsername(payload);
|
||||||
|
const avatarURL = payload.fromDiscord
|
||||||
|
? payload.discordUserAvatarUrl || null
|
||||||
|
: client.user?.displayAvatarURL?.({ extension: 'png', size: 128 }) || null;
|
||||||
guildConfigs.forEach((entry) => {
|
guildConfigs.forEach((entry) => {
|
||||||
if (!entry?.channelId) return;
|
if (!entry?.channelId || !entry?.webhookId || !entry?.webhookToken) return;
|
||||||
if (payload.fromDiscord) {
|
if (payload.fromDiscord) {
|
||||||
if (payload.discordGuildId && String(payload.discordGuildId) === String(entry.guildId)) {
|
if (payload.discordGuildId && String(payload.discordGuildId) === String(entry.guildId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entry.mode === 'private') return;
|
if (entry.mode === 'private') return;
|
||||||
}
|
}
|
||||||
sendToChannel(entry.channelId, text, {}, { parse: [] });
|
const webhook = new WebhookClient({ id: entry.webhookId, token: entry.webhookToken });
|
||||||
|
webhook
|
||||||
|
.send({
|
||||||
|
content: text,
|
||||||
|
username,
|
||||||
|
avatarURL,
|
||||||
|
allowedMentions: { parse: [] },
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn('Failed to send webhook message', { guildId: entry.guildId, error: err.message });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function getGuildConfig(guildId) {
|
|||||||
return store[String(guildId)] || null;
|
return store[String(guildId)] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGuildConfig(guildId, { channelId, mode }) {
|
function setGuildConfig(guildId, { channelId, mode, webhookId, webhookToken }) {
|
||||||
if (!guildId) {
|
if (!guildId) {
|
||||||
throw new Error('guildId required');
|
throw new Error('guildId required');
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,8 @@ function setGuildConfig(guildId, { channelId, mode }) {
|
|||||||
guildId: key,
|
guildId: key,
|
||||||
channelId: channelId != null ? String(channelId) : prev?.channelId || null,
|
channelId: channelId != null ? String(channelId) : prev?.channelId || null,
|
||||||
mode: normalizeMode(mode, prev?.mode || 'global'),
|
mode: normalizeMode(mode, prev?.mode || 'global'),
|
||||||
|
webhookId: webhookId != null ? String(webhookId) : prev?.webhookId || null,
|
||||||
|
webhookToken: webhookToken != null ? String(webhookToken) : prev?.webhookToken || null,
|
||||||
createdAt: prev?.createdAt || now,
|
createdAt: prev?.createdAt || now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ function handleSensorFrame(roverId, frame) {
|
|||||||
if (bumps?.bumpLeft || bumps?.bumpRight) {
|
if (bumps?.bumpLeft || bumps?.bumpRight) {
|
||||||
record.lastBumpAt = Date.now();
|
record.lastBumpAt = Date.now();
|
||||||
}
|
}
|
||||||
io.to(record.room).emit('sensorFrame', {
|
io.to(record.room).volatile.emit('sensorFrame', {
|
||||||
roverId,
|
roverId,
|
||||||
frame,
|
frame,
|
||||||
sensors: decoded,
|
sensors: decoded,
|
||||||
|
|||||||
Reference in New Issue
Block a user