Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified apps/bot/bun.lockb
Binary file not shown.
5 changes: 3 additions & 2 deletions apps/bot/src/lib/channelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export class ChannelManagementService {
});

// Restrict @everyone from sending messages and adding reactions after channel inherits category permissions
// ReadMessageHistory is granted to @everyone so the bot (and all members) can read EasyPoll results
try {
await channel.permissionOverwrites.edit(guild.roles.everyone.id, {
SendMessages: false,
Expand All @@ -150,7 +151,7 @@ export class ChannelManagementService {
ReadMessageHistory: true,
});
logger.info(
`Restricted @everyone from sending messages and adding reactions in vote channel: ${nominee.name}`
`Set permissions for vote channel: ${nominee.name}`
);
} catch (error) {
logger.error(
Expand All @@ -159,7 +160,7 @@ export class ChannelManagementService {
channelId: channel.id,
nomineeName: nominee.name,
},
'Failed to restrict @everyone permissions'
'Failed to set vote channel permissions'
);
}

Expand Down
20 changes: 20 additions & 0 deletions apps/bot/src/lib/voteResultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ export class VoteResultService {
*/
private async findPollInChannel(channel: TextChannel): Promise<PollData | null> {
try {
// Check if bot has ReadMessageHistory permission
const botMember = channel.guild.members.me;
if (!botMember) {
logger.error({ channelId: channel.id }, 'Bot is not a member of this guild');
return null;
}

const hasReadPermission = channel.permissionsFor(botMember)?.has('ReadMessageHistory');
if (!hasReadPermission) {
logger.error(
{
channelId: channel.id,
channelName: channel.name,
guildId: channel.guild.id
},
'Bot lacks ReadMessageHistory permission in vote channel. Please grant this permission to read poll results.'
);
return null;
}

// Fetch recent messages to find the poll - force cache bypass
const messages = await channel.messages.fetch({ limit: DISCORD_CONSTANTS.LIMITS.MESSAGE_FETCH_LIMIT, force: true });

Expand Down
53 changes: 32 additions & 21 deletions apps/bot/src/tests/messageAccessSecurity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,33 @@ describe('Message Access Security Tests', () => {
// Reset message access tracking
messageAccessLog = [];

// Pre-create mockGuild so channels can reference it
mockGuild = {
id: 'test-guild-id',
members: {
me: { id: 'bot-id' }, // Bot member for permission checks
cache: new Map(),
fetch: mock(async () => new Map())
},
channels: {
cache: new Map(),
fetch: mock(async (id: string) => {
const channel = mockGuild.channels.cache.get(id);
if (channel) return channel;
throw new Error('Channel not found');
})
}
};

// Create mock channels with tracking
const createTrackedChannel = (id: string, type: string, name: string) => ({
id,
name,
type: 0, // GUILD_TEXT
guild: mockGuild,
permissionsFor: mock(() => ({
has: mock(() => true) // Grant all permissions in tests
})),
messages: {
fetch: mock(async (options?: any) => {
messageAccessLog.push({
Expand All @@ -51,7 +72,7 @@ describe('Message Access Security Tests', () => {
id: 'poll-message-id',
author: { id: '437618149505105920' }, // EasyPoll bot ID
embeds: [{
description: 'Poll Results: ✅ 15 votes, ❌ 3 votes - Poll closed'
description: '**Question**\nShould we invite nominee?\n\n**Final Result**\n✅ **Yes, Accept** — 15 votes (83%)\n❌ **No, Reject** — 3 votes (17%)'
}],
createdTimestamp: Date.now() - 30000, // 30 seconds ago
reactions: {
Expand All @@ -62,6 +83,12 @@ describe('Message Access Security Tests', () => {
}
};

// If fetching by ID (string), return the message directly
if (typeof options === 'string') {
return mockMessage;
}

// Otherwise return a Map collection
const mockCollection = new Map();
mockCollection.set('poll-message-id', mockMessage);
return mockCollection;
Expand All @@ -80,26 +107,10 @@ describe('Message Access Security Tests', () => {
mockDiscussionChannel = createTrackedChannel('discussion-channel-456', 'discussion', 'nominee-discussion-smith');
mockGovernanceChannel = createTrackedChannel('governance-channel-789', 'governance', 'governance');

// Mock guild
mockGuild = {
id: 'test-guild-id',
channels: {
cache: new Map([
['vote-channel-123', mockVoteChannel],
['discussion-channel-456', mockDiscussionChannel],
['governance-channel-789', mockGovernanceChannel]
]),
fetch: mock(async (id: string) => {
const channel = mockGuild.channels.cache.get(id);
if (channel) return channel;
throw new Error('Channel not found');
})
},
members: {
cache: new Map(),
fetch: mock(async () => new Map())
}
};
// Add channels to guild's cache
mockGuild.channels.cache.set('vote-channel-123', mockVoteChannel);
mockGuild.channels.cache.set('discussion-channel-456', mockDiscussionChannel);
mockGuild.channels.cache.set('governance-channel-789', mockGovernanceChannel);

// Mock client
mockClient = {
Expand Down
Loading