mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
new discord bot stuff
This commit is contained in:
@@ -38,6 +38,7 @@ roomCameras:
|
|||||||
discord:
|
discord:
|
||||||
token: "DISCORD_BOT_TOKEN"
|
token: "DISCORD_BOT_TOKEN"
|
||||||
guildId: "123456789012345678" # optional; bot works in any guild it's invited to
|
guildId: "123456789012345678" # optional; bot works in any guild it's invited to
|
||||||
|
siteUrl: "https://rover.example.com"
|
||||||
channels:
|
channels:
|
||||||
announcements: "123456789012345678"
|
announcements: "123456789012345678"
|
||||||
adminAlerts: "123456789012345678"
|
adminAlerts: "123456789012345678"
|
||||||
@@ -46,3 +47,4 @@ discord:
|
|||||||
roles:
|
roles:
|
||||||
announcementPing: "123456789012345678"
|
announcementPing: "123456789012345678"
|
||||||
adminPing: "123456789012345678"
|
adminPing: "123456789012345678"
|
||||||
|
stalker: "123456789012345678"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ if (!enabled) {
|
|||||||
|
|
||||||
const intents = [
|
const intents = [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMembers,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
];
|
];
|
||||||
@@ -788,9 +789,15 @@ async function handleBridgeInbound(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmbed({ title, description, color }) {
|
function buildEmbed({ title, description, color, includeSiteUrl = true }) {
|
||||||
const embed = new EmbedBuilder().setTitle(title || 'Update').setColor(color || 0x2196f3);
|
const embed = new EmbedBuilder().setTitle(title || 'Update').setColor(color || 0x2196f3);
|
||||||
if (description) embed.setDescription(description);
|
const siteUrl =
|
||||||
|
includeSiteUrl && discordConfig.siteUrl ? String(discordConfig.siteUrl) : '';
|
||||||
|
if (description) {
|
||||||
|
embed.setDescription(siteUrl ? `${description}\n\n${siteUrl}` : description);
|
||||||
|
} else if (siteUrl) {
|
||||||
|
embed.setDescription(siteUrl);
|
||||||
|
}
|
||||||
embed.setTimestamp(new Date());
|
embed.setTimestamp(new Date());
|
||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
@@ -900,6 +907,41 @@ function buildBatteryStatusEmbed(color, records = null) {
|
|||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAllUnlockedEmbed(color, records = null) {
|
||||||
|
const embed = buildEmbed({ title: 'All Rovers Unlocked', color: color || 0x4caf50 });
|
||||||
|
const baseRecords = records || Array.from(rovers.values());
|
||||||
|
const snapshots = baseRecords.map(buildRoverStatusSnapshot).filter(Boolean);
|
||||||
|
if (snapshots.length === 0) {
|
||||||
|
embed.setDescription('No rovers online.');
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
snapshots.forEach((snapshot) => {
|
||||||
|
const percent = snapshot?.batteryState?.percentDisplay;
|
||||||
|
const percentLabel = percent != null ? `${percent}%` : 'n/a';
|
||||||
|
embed.addFields({
|
||||||
|
name: snapshot.name,
|
||||||
|
value: `Battery: ${percentLabel}`,
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAllUnlockedCaption(records = null) {
|
||||||
|
return 'All rovers unlocked.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAccessModeEmbed(mode, color) {
|
||||||
|
const total = rovers.size;
|
||||||
|
const unlocked = Array.from(rovers.values()).filter((entry) => !entry.locked).length;
|
||||||
|
const embed = buildEmbed({
|
||||||
|
title: 'Access Mode Updated',
|
||||||
|
description: `Access mode set to **${mode}**\nUnlocked rovers: **${unlocked}/${total}**`,
|
||||||
|
color: color || 0x2196f3,
|
||||||
|
});
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBatteryCaption(type, payload) {
|
function buildBatteryCaption(type, payload) {
|
||||||
const roverId = payload?.roverId || 'unknown';
|
const roverId = payload?.roverId || 'unknown';
|
||||||
const record = rovers.get(roverId) || findRoverRecord(roverId);
|
const record = rovers.get(roverId) || findRoverRecord(roverId);
|
||||||
@@ -936,23 +978,93 @@ function buildBatteryCaption(type, payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function announce({ channelId, content, pingRoleId, color, title, description, embeds }) {
|
async function getRoleMemberIds(channelId, roleId) {
|
||||||
|
if (!channelId || !roleId) return [];
|
||||||
|
const channel = await fetchChannel(channelId);
|
||||||
|
if (!channel?.guild) return [];
|
||||||
|
const guild = channel.guild;
|
||||||
|
let role = guild.roles.cache.get(roleId) || null;
|
||||||
|
if (!role) {
|
||||||
|
try {
|
||||||
|
role = await guild.roles.fetch(roleId);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to fetch Discord role', { roleId, error: err.message });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!role) return [];
|
||||||
|
if (!role.members || role.members.size === 0) {
|
||||||
|
try {
|
||||||
|
await guild.members.fetch();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to fetch guild members', { guildId: guild.id, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(role.members?.keys?.() || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function announce({
|
||||||
|
channelId,
|
||||||
|
content,
|
||||||
|
pingRoleId,
|
||||||
|
pingUserIds,
|
||||||
|
prefixMentions = true,
|
||||||
|
includeSiteUrl = true,
|
||||||
|
color,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
embeds,
|
||||||
|
}) {
|
||||||
if (!channelId) return;
|
if (!channelId) return;
|
||||||
const prefix = pingRoleId ? `<@&${pingRoleId}> ` : '';
|
const mentionChunks = [];
|
||||||
|
if (pingRoleId) mentionChunks.push(`<@&${pingRoleId}>`);
|
||||||
|
if (Array.isArray(pingUserIds)) {
|
||||||
|
pingUserIds.forEach((id) => {
|
||||||
|
if (id) mentionChunks.push(`<@${id}>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const prefix = prefixMentions && mentionChunks.length ? `${mentionChunks.join(' ')} ` : '';
|
||||||
const payloadEmbeds =
|
const payloadEmbeds =
|
||||||
Array.isArray(embeds) && embeds.length > 0
|
Array.isArray(embeds) && embeds.length > 0
|
||||||
? embeds
|
? embeds
|
||||||
: [buildEmbed({ title, description, color })];
|
: [buildEmbed({ title, description, color, includeSiteUrl })];
|
||||||
const allowedMentions = pingRoleId ? { roles: [pingRoleId], parse: [] } : { parse: [] };
|
const allowedMentions = {
|
||||||
|
parse: [],
|
||||||
|
roles: pingRoleId ? [pingRoleId] : [],
|
||||||
|
users: Array.isArray(pingUserIds) ? pingUserIds : [],
|
||||||
|
};
|
||||||
await sendToChannel(
|
await sendToChannel(
|
||||||
channelId,
|
channelId,
|
||||||
`${prefix}${content || ''}`.trim(),
|
`${prefix}${content || ''}`.trim(),
|
||||||
{ embeds: payloadEmbeds },
|
{ embeds: payloadEmbeds },
|
||||||
allowedMentions,
|
allowedMentions,
|
||||||
!pingRoleId, // keep role mention intact when pinging
|
!(pingRoleId || (Array.isArray(pingUserIds) && pingUserIds.length > 0)), // keep mentions intact when pinging
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function announceUserStatus({ channelId, content, color, title, description, embeds }) {
|
||||||
|
const roles = discordConfig.roles || {};
|
||||||
|
const pingRoleId = roles.announcementPing || null;
|
||||||
|
const pingUserIds = await getRoleMemberIds(channelId, roles.stalker || null);
|
||||||
|
const mainLine = pingRoleId ? `<@&${pingRoleId}> ${content}`.trim() : content;
|
||||||
|
const stalkerMentions =
|
||||||
|
Array.isArray(pingUserIds) && pingUserIds.length > 0
|
||||||
|
? pingUserIds.map((id) => `<@${id}>`).join(' ')
|
||||||
|
: '';
|
||||||
|
const message = [mainLine, stalkerMentions].filter(Boolean).join('\n');
|
||||||
|
await announce({
|
||||||
|
channelId,
|
||||||
|
content: message,
|
||||||
|
pingRoleId,
|
||||||
|
pingUserIds,
|
||||||
|
prefixMentions: false,
|
||||||
|
color,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
embeds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleBusEvent(event) {
|
function handleBusEvent(event) {
|
||||||
const { type, payload } = event || {};
|
const { type, payload } = event || {};
|
||||||
const channels = discordConfig.channels || {};
|
const channels = discordConfig.channels || {};
|
||||||
@@ -964,19 +1076,23 @@ function handleBusEvent(event) {
|
|||||||
schedulePresenceRotation();
|
schedulePresenceRotation();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
announce({
|
if (payload?.mode === MODES.OPEN || payload?.mode === MODES.TURNS) {
|
||||||
channelId: channels.announcements,
|
const caption = `Access mode set to ${payload?.mode}.`;
|
||||||
pingRoleId: roles.announcementPing || null,
|
announceUserStatus({
|
||||||
color: 0x2196f3,
|
channelId: channels.announcements,
|
||||||
title: 'Mode Changed',
|
content: caption,
|
||||||
description: `Server mode set to **${payload?.mode}**`,
|
color: 0x2196f3,
|
||||||
});
|
embeds: [buildAccessModeEmbed(payload?.mode, 0x2196f3)],
|
||||||
|
});
|
||||||
|
}
|
||||||
schedulePresenceRotation();
|
schedulePresenceRotation();
|
||||||
break;
|
break;
|
||||||
case 'communityGoal.updated': {
|
case 'communityGoal.updated': {
|
||||||
const goalText = payload?.text ? sanitizeMentions(String(payload.text)) : null;
|
const goalText = payload?.text ? sanitizeMentions(String(payload.text)) : null;
|
||||||
announce({
|
const caption = goalText ? `Community goal: ${goalText}` : 'Community goal cleared.';
|
||||||
|
announceUserStatus({
|
||||||
channelId: channels.announcements,
|
channelId: channels.announcements,
|
||||||
|
content: caption,
|
||||||
color: 0x8bc34a,
|
color: 0x8bc34a,
|
||||||
title: 'Community Goal',
|
title: 'Community Goal',
|
||||||
description: goalText ? goalText : 'Community goal cleared.',
|
description: goalText ? goalText : 'Community goal cleared.',
|
||||||
@@ -990,16 +1106,19 @@ function handleBusEvent(event) {
|
|||||||
color: 0xf0b651,
|
color: 0xf0b651,
|
||||||
title: 'Rover Locked',
|
title: 'Rover Locked',
|
||||||
description: `${payload?.roverId} locked${payload?.reason ? ` (${payload.reason})` : ''}.`,
|
description: `${payload?.roverId} locked${payload?.reason ? ` (${payload.reason})` : ''}.`,
|
||||||
|
includeSiteUrl: false,
|
||||||
});
|
});
|
||||||
schedulePresenceRotation();
|
schedulePresenceRotation();
|
||||||
break;
|
break;
|
||||||
case 'rover.unlocked':
|
case 'rover.unlocked':
|
||||||
announce({
|
schedulePresenceRotation();
|
||||||
|
break;
|
||||||
|
case 'rovers.allUnlocked':
|
||||||
|
announceUserStatus({
|
||||||
channelId: channels.announcements,
|
channelId: channels.announcements,
|
||||||
pingRoleId: roles.announcementPing || null,
|
content: buildAllUnlockedCaption(),
|
||||||
color: 0x4caf50,
|
color: 0x4caf50,
|
||||||
title: 'Rover Unlocked',
|
embeds: [buildAllUnlockedEmbed(0x4caf50)],
|
||||||
description: `${payload?.roverId} unlocked.`,
|
|
||||||
});
|
});
|
||||||
schedulePresenceRotation();
|
schedulePresenceRotation();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ function lockRover(id, locked, options = {}) {
|
|||||||
if (!record) {
|
if (!record) {
|
||||||
throw new Error('Unknown rover');
|
throw new Error('Unknown rover');
|
||||||
}
|
}
|
||||||
|
const wasAllUnlocked = Array.from(rovers.values()).every((entry) => !entry.locked);
|
||||||
const reason = locked ? options.reason || 'manual' : null;
|
const reason = locked ? options.reason || 'manual' : null;
|
||||||
const silent = Boolean(options.silent);
|
const silent = Boolean(options.silent);
|
||||||
if (locked) {
|
if (locked) {
|
||||||
@@ -118,6 +119,14 @@ function lockRover(id, locked, options = {}) {
|
|||||||
type: 'rover.unlocked',
|
type: 'rover.unlocked',
|
||||||
payload: { roverId: id },
|
payload: { roverId: id },
|
||||||
});
|
});
|
||||||
|
const isAllUnlocked = Array.from(rovers.values()).every((entry) => !entry.locked);
|
||||||
|
if (!wasAllUnlocked && isAllUnlocked) {
|
||||||
|
publishEvent({
|
||||||
|
source: 'roverManager',
|
||||||
|
type: 'rovers.allUnlocked',
|
||||||
|
payload: { roverId: id },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
broadcastRoster();
|
broadcastRoster();
|
||||||
managerEvents.emit('lock', { roverId: id, locked: record.locked, reason: record.lockReason });
|
managerEvents.emit('lock', { roverId: id, locked: record.locked, reason: record.lockReason });
|
||||||
|
|||||||
Reference in New Issue
Block a user