diff --git a/packages/bot/src/bot/modules/command_handler.ts b/packages/bot/src/bot/modules/command_handler.ts index 82a21da..0e30869 100644 --- a/packages/bot/src/bot/modules/command_handler.ts +++ b/packages/bot/src/bot/modules/command_handler.ts @@ -52,12 +52,9 @@ let commands: SimpleCommand[]; if (msg.author!.bot) return; - // If we can't reply to the message, return - const member = await getOwnMemberInServer(msg.channel.server); - if (!member.hasPermission(msg.channel, "SendMessage")) { - console.debug("Cannot reply to message; returning"); - return; - } + // Don't bother with the computed SendMessage pre-check as the bot's permission calculation may not match Stoat's actual enforcement. + // The API enforces permissions anyway. + await getOwnMemberInServer(msg.channel.server); // Send message through anti spam check and custom rules if (!(await antispam(msg))) return; diff --git a/packages/bot/src/bot/modules/event_handler.ts b/packages/bot/src/bot/modules/event_handler.ts index 59bc844..da250e6 100644 --- a/packages/bot/src/bot/modules/event_handler.ts +++ b/packages/bot/src/bot/modules/event_handler.ts @@ -3,10 +3,9 @@ import crypto from "crypto"; import { client, dbs } from "../.."; import Infraction from "automod-lib/dist/types/antispam/Infraction"; import InfractionType from "automod-lib/dist/types/antispam/InfractionType"; -import { storeInfraction } from "../util"; +import { getOwnMemberInServer, storeInfraction } from "../util"; import { fetchUsername } from "./mod_logs"; import { DEFAULT_PREFIX } from "./command_handler"; -import type { SendableEmbed } from "../../stoat/index.js"; import { UserSystemMessage } from "../../stoat/index.js"; const DM_SESSION_LIFETIME = 1000 * 60 * 5; @@ -83,36 +82,42 @@ client.on("messageCreate", async (message) => { }); // Send a message when added to a server -client.on("serverMemberJoin", (member) => { - if (member.id.user != client.user?.id) return; - - if (!member.server) return; - - const embed: SendableEmbed = { - title: "Hi there, thanks for adding me!", - description: `My prefix is "${DEFAULT_PREFIX}", but you can also @mention me instead.\nCheck out ${DEFAULT_PREFIX}help to get started!`, - icon_url: client.user.avatarURL, - colour: "#ff6e6d", - url: `/bot/${client.user.id}`, - }; - - let channels = member.server.channels.filter((c) => c && c.type == "TextChannel" && member.hasPermission(c, "SendMessage") && member.hasPermission(c, "SendEmbeds")); +client.on("serverCreate", async (server) => { + console.log(`Joined new server: ${server.name} (${server.id})`); + + const member = await getOwnMemberInServer(server).catch((e) => { + console.warn("Cannot send hello message: Failed to fetch own member in server:", e); + return undefined; + }); + + const channels = server.channels.filter((c) => c && c.type == "TextChannel"); + + // Filter by permissions when possible, but never reject ALL channels + let candidates = channels; + if (member) { + const permitted = channels.filter((c) => member.hasPermission(c, "SendMessage")); + if (permitted.length > 0) { + candidates = permitted; + } + } // Attempt to find an appropriate channel, otherwise use the first one available let channel = - channels.find((c) => c?.name?.toLowerCase() == "welcome") || - channels.find((c) => c?.name?.toLowerCase() == "general") || - channels.find((c) => c?.name?.toLowerCase() == "bots") || - channels.find((c) => c?.name?.toLowerCase() == "spam") || - channels[0]; - - if (!channel) return console.debug("Cannot send hello message: No suitable channel found"); + candidates.find((c) => c?.name?.toLowerCase() == "welcome") || + candidates.find((c) => c?.name?.toLowerCase() == "general") || + candidates.find((c) => c?.name?.toLowerCase() == "bots") || + candidates.find((c) => c?.name?.toLowerCase() == "spam") || + candidates[0]; + + if (!channel) { + console.warn("Cannot send hello message: No suitable channel found in server", server.id); + return; + } channel .sendMessage({ - content: `👋 "Hi there!")`, - embeds: [embed], + content: `## Hey ${server.name}!\nThanks for trusting AutoMod to protect and manage your community.\nThis bot's prefix is "${DEFAULT_PREFIX}", but you can also @mention it instead.\nCheck out \`${DEFAULT_PREFIX}help\` to get started!\n\nFull setup guide: `, }) - .catch((e) => console.debug("Cannot send hello message: " + e)); + .catch((e) => console.warn("Cannot send hello message:", e)); }); client.on("error", (err) => console.error("Client error:", err)); diff --git a/packages/bot/src/bot/util.ts b/packages/bot/src/bot/util.ts index 1143459..40630ef 100644 --- a/packages/bot/src/bot/util.ts +++ b/packages/bot/src/bot/util.ts @@ -103,7 +103,27 @@ function getPermissionLevelFromMember(serverMember: ServerMember, server: Server } async function getOwnMemberInServer(server: Server): Promise { - return server.member || (await server.fetchMember(client.user!.id)); + const member = server.member || (await server.fetchMember(client.user!.id)); + + // If the bot has no roles, the server's default_permissions determine what the bot can do. + if (!member.roles?.length) { + const raw = client.servers.getUnderlyingObject(server.id); + if (!raw.defaultPermissions && raw.default_permissions == null) { + try { + const data = await client.api.get(`/servers/${server.id}`, { include_channels: true }); + client.servers.updateUnderlyingObject(server.id, data); + if (data.channels) { + for (const channel of data.channels) { + if (typeof channel !== "string") { + client.channels.getOrCreate(channel._id, { ...channel, server: channel.server || channel.serverId || data._id }); + } + } + } + } catch (_) {} + } + } + + return member; } // Utility functions diff --git a/packages/bot/src/stoat/Client.ts b/packages/bot/src/stoat/Client.ts index 5198b33..51bbccf 100644 --- a/packages/bot/src/stoat/Client.ts +++ b/packages/bot/src/stoat/Client.ts @@ -271,8 +271,27 @@ export class Client extends EventEmitter { case "ServerCreate": { const data = event.server || event; if (!this.servers.has(data._id)) { - if (event.channels) for (const ch of event.channels) this.channels.getOrCreate(ch._id, ch); - this.servers.getOrCreate(data._id, data, true); + try { + const serverData = await this.api.get(`/servers/${data._id}`, { include_channels: true }); + if (serverData.channels) { + for (const channel of serverData.channels) { + if (typeof channel !== "string") { + this.channels.getOrCreate(channel._id, { ...channel, server: channel.server || channel.serverId || serverData._id }); + } + } + } + this.servers.getOrCreate(data._id, serverData, true); + } catch (e) { + // Fallback: use the WS event data + if (event.channels) + for (const ch of event.channels) { + if (typeof ch !== "string") { + this.channels.getOrCreate(ch._id, { ...ch, server: ch.server || ch.serverId || data._id }); + } + } + this.servers.getOrCreate(data._id, data, true); + console.error(`[WS] Failed to fetch server ${data._id}:`, (e as any)?.message || e); + } } break; } diff --git a/packages/bot/src/stoat/collections/ServerCollection.ts b/packages/bot/src/stoat/collections/ServerCollection.ts index da9f771..64c3d34 100644 --- a/packages/bot/src/stoat/collections/ServerCollection.ts +++ b/packages/bot/src/stoat/collections/ServerCollection.ts @@ -14,7 +14,7 @@ export class ServerCollection extends BaseCollection { if (data.channels) { for (const channel of data.channels) { if (typeof channel !== "string") { - this.client.channels.getOrCreate(channel._id, channel); + this.client.channels.getOrCreate(channel._id, { ...channel, server: channel.server || channel.serverId || data._id }); } } } diff --git a/packages/bot/src/stoat/structures/Channel.ts b/packages/bot/src/stoat/structures/Channel.ts index aa5e53d..b67091f 100644 --- a/packages/bot/src/stoat/structures/Channel.ts +++ b/packages/bot/src/stoat/structures/Channel.ts @@ -65,7 +65,7 @@ export class Channel { const server = this.server; if (server && this.client.user.id === server.ownerId) return (1n << 64n) - 1n; let perms = server ? server.permission : 0n; - const dp = this.data.defaultPermissions; + const dp = this.data.defaultPermissions ?? this.data.default_permissions; if (dp) { perms |= BigInt(typeof dp === "number" ? 0n : (dp.a ?? 0n)); perms &= ~BigInt(typeof dp === "number" ? BigInt(dp) : (dp.d ?? 0n)); diff --git a/packages/bot/src/stoat/structures/Server.ts b/packages/bot/src/stoat/structures/Server.ts index 2949651..505659c 100644 --- a/packages/bot/src/stoat/structures/Server.ts +++ b/packages/bot/src/stoat/structures/Server.ts @@ -43,7 +43,12 @@ export class Server { return b ? new File(this.client, b) : undefined; } get channelIds() { - return this.data.channelIds ?? this.data.channels ?? []; + const ids = this.data.channelIds ?? this.data.channel_ids; + if (ids && Array.isArray(ids)) return ids; + const channels = this.data.channels; + if (!channels || !Array.isArray(channels) || channels.length === 0) return []; + if (typeof channels[0] === "string") return channels; + return channels.map((c: any) => c._id || c.id || c); } get channels() { return this.channelIds.map((id: string) => this.client.channels.get(id)).filter((x: any) => x); @@ -59,7 +64,7 @@ export class Server { return map; } get defaultPermissions() { - return this.data.defaultPermissions ?? 0n; + return this.data.default_permissions ?? this.data.defaultPermissions ?? 0n; } get discoverable() { return this.data.discoverable; diff --git a/packages/bot/src/stoat/structures/ServerMember.ts b/packages/bot/src/stoat/structures/ServerMember.ts index ec9728a..1a4f7ba 100644 --- a/packages/bot/src/stoat/structures/ServerMember.ts +++ b/packages/bot/src/stoat/structures/ServerMember.ts @@ -88,7 +88,7 @@ export class ServerMember { } } // Apply channel-level overrides - const dp = target.data?.defaultPermissions; + const dp = target.data?.defaultPermissions ?? target.data?.default_permissions; if (dp) { perms |= BigInt(typeof dp === "number" ? 0n : (dp.a ?? 0n)); perms &= ~BigInt(typeof dp === "number" ? BigInt(dp) : (dp.d ?? 0n));