• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

VolvoxLLC / volvox-bot / 23592885485

26 Mar 2026 11:53AM UTC coverage: 90.643%. Remained the same
23592885485

Pull #392

github

web-flow
Merge 56f35ea76 into ac8a1088d
Pull Request #392: refactor: reduce cognitive complexity - bot batch 3

6553 of 7667 branches covered (85.47%)

Branch coverage included in aggregate %.

78 of 84 new or added lines in 4 files covered. (92.86%)

4 existing lines in 2 files now uncovered.

11156 of 11870 relevant lines covered (93.98%)

224.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.25
/src/utils/discordCache.js
1
/**
2
 * Discord API Cache Layer
3
 * Caches Discord API fetch results (channels, roles, members, guilds)
4
 * to reduce API calls and improve response times.
5
 *
6
 * Uses the centralized cache system (Redis with in-memory fallback).
7
 *
8
 * @see https://github.com/VolvoxLLC/volvox-bot/issues/177
9
 */
10

11
import { debug, warn } from '../logger.js';
12
import { cacheDelPattern, cacheGet, cacheSet, TTL } from './cache.js';
13

14
/**
15
 * Fetch a channel with caching.
16
 * Falls through to Discord API on cache miss, caches the serialized result.
17
 *
18
 * When `expectedGuildId` is provided the function returns `null` if the
19
 * resolved channel belongs to a different guild — preventing cross-guild
20
 * message leaks from misconfigured channel IDs.
21
 *
22
 * @param {import('discord.js').Client} client - Discord client
23
 * @param {string} channelId - Channel ID to fetch
24
 * @param {string} [expectedGuildId] - If set, reject channels not belonging to this guild
25
 * @returns {Promise<import('discord.js').Channel|null>} The channel, or null if not found / wrong guild
26
 */
27
/**
28
 * Check whether a channel belongs to the expected guild; log and return false if not.
29
 */
30
function isChannelGuildValid(channel, channelId, expectedGuildId, source) {
31
  if (!expectedGuildId) return true;
4!
NEW
32
  const guildId = channel.guildId ?? channel.guildId;
×
33
  if (guildId === expectedGuildId) return true;
4!
NEW
34
  warn(`Channel belongs to a different guild${source ? ` (${source})` : ''} — blocked`, {
×
35
    channelId,
36
    channelGuildId: guildId,
37
    expectedGuildId,
38
  });
39
  return false;
4✔
40
}
41

42
/**
43
 * Fetch a channel from the Discord API, cache its metadata, and validate guild ownership.
44
 */
45
async function fetchChannelFromApi(client, channelId, expectedGuildId) {
46
  const channel = await client.channels.fetch(channelId);
3✔
47
  if (!channel) return null;
2!
48

49
  const cacheKey = `discord:channel:${channelId}`;
2✔
50
  await cacheSet(
2✔
51
    cacheKey,
52
    {
53
      id: channel.id,
54
      name: channel.name ?? null,
2!
55
      type: channel.type,
56
      guildId: channel.guildId ?? null,
3!
57
    },
58
    TTL.CHANNEL_DETAIL,
59
  );
60
  debug('Fetched and cached channel', { channelId, name: channel.name });
2✔
61

62
  if (!isChannelGuildValid(channel, channelId, expectedGuildId)) return null;
2!
63
  return channel;
2✔
64
}
65

66
export async function fetchChannelCached(client, channelId, expectedGuildId) {
67
  if (!channelId) return null;
8✔
68

69
  // Try Discord.js internal cache first (always fastest)
70
  const djsCached = client.channels.cache.get(channelId);
7✔
71
  if (djsCached) {
7✔
72
    return isChannelGuildValid(djsCached, channelId, expectedGuildId) ? djsCached : null;
1!
73
  }
74

75
  // Try Redis/memory cache for fast-reject before hitting the API
76
  const cacheKey = `discord:channel:${channelId}`;
4✔
77
  const cached = await cacheGet(cacheKey);
4✔
78
  if (cached) {
4✔
79
    if (expectedGuildId && cached.guildId && cached.guildId !== expectedGuildId) {
2!
UNCOV
80
      warn('Channel belongs to a different guild (cached) — blocked', {
×
81
        channelId,
82
        channelGuildId: cached.guildId,
83
        expectedGuildId,
84
      });
85
      return null;
×
86
    }
87

88
    // Re-check DJS cache in case it was populated during the async gap
89
    const recheckDjs = client.channels.cache.get(channelId);
2✔
90
    if (recheckDjs) {
2✔
91
      return isChannelGuildValid(recheckDjs, channelId, expectedGuildId) ? recheckDjs : null;
1!
92
    }
93

94
    debug('Redis cache hit for channel — fetching real Channel object', { channelId });
1✔
95
  }
96

97
  // Fetch from Discord API
98
  try {
3✔
99
    return await fetchChannelFromApi(client, channelId, expectedGuildId);
3✔
100
  } catch (err) {
101
    warn('Failed to fetch channel', { channelId, error: err.message });
1✔
102
    return null;
1✔
103
  }
104
}
105

106
/**
107
 * Fetch guild channels list with caching.
108
 * Returns serialized channel data suitable for API responses.
109
 *
110
 * @param {import('discord.js').Guild} guild - Discord guild
111
 * @returns {Promise<Array<{id: string, name: string, type: number, position: number, parentId: string|null}>>}
112
 */
113
export async function fetchGuildChannelsCached(guild) {
114
  const cacheKey = `discord:guild:${guild.id}:channels`;
7✔
115
  const cached = await cacheGet(cacheKey);
7✔
116
  if (cached) return cached;
7✔
117

118
  try {
6✔
119
    const channels = await guild.channels.fetch();
6✔
120
    const serialized = Array.from(channels.values())
5✔
121
      .filter((ch) => ch !== null)
9✔
122
      .map((ch) => ({
8✔
123
        id: ch.id,
124
        name: ch.name,
125
        type: ch.type,
126
        position: ch.position ?? 0,
8!
127
        parentId: ch.parentId ?? null,
16✔
128
      }))
129
      .sort((a, b) => a.position - b.position);
3✔
130

131
    await cacheSet(cacheKey, serialized, TTL.CHANNELS);
5✔
132
    debug('Fetched and cached guild channels', { guildId: guild.id, count: serialized.length });
5✔
133
    return serialized;
5✔
134
  } catch (err) {
135
    warn('Failed to fetch guild channels', { guildId: guild.id, error: err.message });
1✔
136
    return [];
1✔
137
  }
138
}
139

140
/**
141
 * Fetch guild roles list with caching.
142
 * Returns serialized role data suitable for API responses.
143
 *
144
 * @param {import('discord.js').Guild} guild - Discord guild
145
 * @returns {Promise<Array<{id: string, name: string, color: number, position: number, permissions: string}>>}
146
 */
147
export async function fetchGuildRolesCached(guild) {
148
  const cacheKey = `discord:guild:${guild.id}:roles`;
2✔
149
  const cached = await cacheGet(cacheKey);
2✔
150
  if (cached) return cached;
2!
151

152
  try {
2✔
153
    const roles = await guild.roles.fetch();
2✔
154
    const serialized = Array.from(roles.values()).map((role) => ({
2✔
155
      id: role.id,
156
      name: role.name,
157
      color: role.color,
158
      position: role.position,
159
      permissions: role.permissions.bitfield.toString(),
160
    }));
161

162
    await cacheSet(cacheKey, serialized, TTL.ROLES);
1✔
163
    debug('Fetched and cached guild roles', { guildId: guild.id, count: serialized.length });
1✔
164
    return serialized;
1✔
165
  } catch (err) {
166
    warn('Failed to fetch guild roles', { guildId: guild.id, error: err.message });
1✔
167
    return [];
1✔
168
  }
169
}
170

171
/**
172
 * Fetch a guild member with caching.
173
 *
174
 * @param {import('discord.js').Guild} guild - Discord guild
175
 * @param {string} userId - User ID
176
 * @returns {Promise<import('discord.js').GuildMember|null>}
177
 */
178
export async function fetchMemberCached(guild, userId) {
179
  if (!userId) return null;
6✔
180

181
  // Try Discord.js internal cache first
182
  const djsCached = guild.members.cache.get(userId);
5✔
183
  if (djsCached) return djsCached;
5✔
184

185
  const cacheKey = `discord:guild:${guild.id}:member:${userId}`;
4✔
186
  const cached = await cacheGet(cacheKey);
4✔
187

188
  // If we have cached metadata, try DJS cache again (may have been populated)
189
  if (cached) {
4✔
190
    const recheckDjs = guild.members.cache.get(userId);
1✔
191
    if (recheckDjs) return recheckDjs;
1!
192
  }
193

194
  try {
3✔
195
    const member = await guild.members.fetch(userId);
3✔
196
    if (member) {
1!
197
      await cacheSet(
1✔
198
        cacheKey,
199
        {
200
          id: member.id,
201
          displayName: member.displayName,
202
          joinedAt: member.joinedAt?.toISOString() ?? null,
1!
203
        },
204
        TTL.MEMBERS,
205
      );
206
    }
207
    return member;
1✔
208
  } catch (err) {
209
    // Don't warn for unknown member — it's expected
210
    if (err.code !== 10007) {
2✔
211
      warn('Failed to fetch guild member', { guildId: guild.id, userId, error: err.message });
1✔
212
    }
213
    return null;
2✔
214
  }
215
}
216

217
/**
218
 * Invalidate all cached data for a guild.
219
 * Call this when guild config or structure changes significantly.
220
 *
221
 * @param {string} guildId - Guild ID to invalidate
222
 * @returns {Promise<void>}
223
 */
224
export async function invalidateGuildCache(guildId) {
225
  await cacheDelPattern(`discord:guild:${guildId}:*`);
1✔
226
  debug('Invalidated guild cache', { guildId });
1✔
227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc