-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbot.js
More file actions
445 lines (388 loc) · 16 KB
/
Copy pathbot.js
File metadata and controls
445 lines (388 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
require('dotenv').config();
const { Client, GatewayIntentBits, ChannelType } = require('discord.js');
const { createClient } = require('@supabase/supabase-js');
const express = require('express');
const cors = require('cors');
// Initialize Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions
]
});
// Initialize Supabase client
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
// Configuration
const GUILD_ID = process.env.DISCORD_GUILD_ID;
const BASHER_PROGRESS_CATEGORY_ID = '1351223065354178722';
const BASHER_PROGRESS_CATEGORY_NAME = 'Basher Progress';
const ORGANISER_ID = 77; // Bot's unique ID in your database
const DAILY_POINTS = 5;
const MIN_WORD_COUNT = 40;
// Bot status tracking
let botStatus = {
online: false,
username: null,
guild: null,
uptime: null,
lastActivity: null,
messagesProcessed: 0,
pointsAwarded: 0,
errors: 0
};
// Express server setup
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// API Endpoints
app.get('/', (req, res) => {
res.json({
message: 'BashEye Discord Bot API',
version: '1.0.0',
endpoints: {
'/status': 'GET - Bot status information',
'/health': 'GET - Health check',
'/stats': 'GET - Bot statistics'
}
});
});
app.get('/status', (req, res) => {
res.json({
status: 'success',
data: {
...botStatus,
uptime: botStatus.online ? Date.now() - botStatus.uptime : null,
uptimeFormatted: botStatus.online ? formatUptime(Date.now() - botStatus.uptime) : null
}
});
});
app.get('/health', (req, res) => {
const isHealthy = botStatus.online && client.isReady();
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'unhealthy',
online: botStatus.online,
ready: client.isReady(),
timestamp: new Date().toISOString()
});
});
app.get('/stats', (req, res) => {
res.json({
status: 'success',
data: {
messagesProcessed: botStatus.messagesProcessed,
pointsAwarded: botStatus.pointsAwarded,
errors: botStatus.errors,
guild: botStatus.guild,
monitoringCategory: BASHER_PROGRESS_CATEGORY_NAME,
dailyPoints: DAILY_POINTS,
minimumWords: MIN_WORD_COUNT
}
});
});
// Helper function to format uptime
function formatUptime(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
// Helper function to get today's date in format for description
function getTodayDateString() {
const today = new Date();
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const day = today.getDate();
const month = months[today.getMonth()];
const year = today.getFullYear().toString().slice(-2);
return `${day}${month}'${year}`;
}
// Helper function to get today's date key for tracking
function getTodayKey() {
return new Date().toDateString();
}
// Helper function to count words in text
function countWords(text) {
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
}
// Helper function to check if message has screenshot/image
function hasAttachment(message) {
return message.attachments.size > 0 &&
message.attachments.some(attachment =>
attachment.contentType && attachment.contentType.startsWith('image/')
);
}
// Function to get member ID from database by Discord username
async function getMemberIdByDiscordUsername(discordUsername) {
try {
const { data, error } = await supabase
.from('members')
.select('id')
.eq('discord_username', discordUsername)
.single();
if (error) {
console.error('Error fetching member:', error);
return null;
}
return data?.id || null;
} catch (error) {
console.error('Error in getMemberIdByDiscordUsername:', error);
return null;
}
}
// Function to award points to member
async function awardPoints(memberId, date) {
try {
const description = `PU-${date}`;
const { data, error } = await supabase
.from('points')
.insert({
member_id: memberId,
organiser_id: ORGANISER_ID,
points: DAILY_POINTS,
description: description
});
if (error) {
console.error('Error inserting points:', error);
return false;
}
console.log(`✅ Awarded ${DAILY_POINTS} points to member ${memberId} for ${description}`);
return true;
} catch (error) {
console.error('Error in awardPoints:', error);
return false;
}
}
// Function to check if member already received points today (database-based)
async function hasReceivedPointsToday(discordUsername) {
try {
const today = new Date().toISOString().split('T')[0]; // Get today's date in YYYY-MM-DD format
const todayDescription = `PU-${getTodayDateString()}`; // PU-20Sep25 format
console.log(`🔍 Checking database for existing points today for ${discordUsername}`);
console.log(` Looking for description: ${todayDescription}`);
// First get the member ID
const memberId = await getMemberIdByDiscordUsername(discordUsername);
if (!memberId) {
console.log(` ❌ Member not found in database`);
return false;
}
// Check if there's already a point record for today
const { data, error } = await supabase
.from('points')
.select('id')
.eq('member_id', memberId)
.eq('organiser_id', ORGANISER_ID)
.eq('description', todayDescription)
.limit(1);
if (error) {
console.error('Error checking daily points:', error);
return false; // If error, allow the attempt (safer than blocking)
}
const hasPoints = data && data.length > 0;
console.log(` 📊 Database check result: ${hasPoints ? 'ALREADY AWARDED' : 'NOT AWARDED YET'}`);
return hasPoints;
} catch (error) {
console.error('Error in hasReceivedPointsToday:', error);
return false; // If error, allow the attempt
}
}
// Function to mark member as having received points today (no longer needed - database handles this)
function markPointsAwarded(userId) {
// This function is now redundant since we're using database tracking
// Keeping it for backwards compatibility but it does nothing
console.log(`📝 Points marked in database for user ${userId}`);
}
// Function to check if channel is in basher-progress category
async function isInBasherProgressCategory(channel) {
try {
console.log(`🔍 Checking channel: ${channel.name} (ID: ${channel.id}, Type: ${channel.type})`);
console.log(`📁 Channel parentId: ${channel.parentId}`);
console.log(`🎯 Looking for category ID: ${BASHER_PROGRESS_CATEGORY_ID}`);
// Direct category check - this is the key insight from your Python code!
if (channel.parentId === BASHER_PROGRESS_CATEGORY_ID) {
console.log(`✅ DIRECT MATCH: Channel is directly in basher-progress category!`);
return true;
}
// For forum threads, check if the parent channel's category matches
if ((channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread) && channel.parent) {
console.log(`📋 Thread detected. Parent: ${channel.parent.name} (ID: ${channel.parent.id})`);
console.log(`📁 Parent's parentId: ${channel.parent.parentId}`);
if (channel.parent.parentId === BASHER_PROGRESS_CATEGORY_ID) {
console.log(`✅ THREAD MATCH: Parent channel is in basher-progress category!`);
return true;
}
}
console.log(`❌ No match found - parentId: ${channel.parentId}, thread parent's parentId: ${channel.parent?.parentId}`);
return false;
} catch (error) {
console.error('Error checking channel category:', error);
return false;
}
}
// Main message handler
async function handleMessage(message) {
// Ignore bot messages first (before any logging)
if (message.author.bot) return;
// Check if message is from the correct guild (before any logging)
if (!message.guild || message.guild.id !== GUILD_ID) return;
// Only log messages from basher-progress category now
const isInCategory = await isInBasherProgressCategory(message.channel);
if (!isInCategory) return;
// Update activity tracking
botStatus.lastActivity = new Date().toISOString();
botStatus.messagesProcessed++;
// Now log only relevant messages
console.log(`\n🔗 MESSAGE IN BASHER-PROGRESS:`);
console.log(` Author: ${message.author.username}`);
console.log(` Channel: ${message.channel.name} (ID: ${message.channel.id})`);
console.log(` Channel Type: ${message.channel.type}`);
console.log(` Channel ParentId: ${message.channel.parentId}`);
console.log(` Content: "${message.content.substring(0, 100)}..."`);
console.log(` Attachments: ${message.attachments.size}`);
console.log(`\n📩 Processing message from ${message.author.username} in #${message.channel.name}`);
console.log(`✅ Message is in basher-progress category!`);
// Check if message has an image attachment
if (!hasAttachment(message)) {
console.log(`❌ Message from ${message.author.username} has no image attachment`);
try {
await message.reply({
content: `📸 **Missing Screenshot!**\n\nHi ${message.author}, your daily progress update needs to include a screenshot/image to earn points.\n\n*Please add an image and try again!*`,
allowedMentions: { users: [message.author.id] }
});
} catch (error) {
console.error('Error sending image requirement notification:', error);
}
return;
}
// Check if message has at least 35 words
const wordCount = countWords(message.content);
if (wordCount < MIN_WORD_COUNT) {
console.log(`❌ Message from ${message.author.username} has only ${wordCount} words (minimum: ${MIN_WORD_COUNT})`);
try {
await message.reply({
content: `📝 **Need More Details!**\n\nHi ${message.author}, your progress update needs at least **${MIN_WORD_COUNT} words** to earn points.\n\n*Current word count: ${wordCount}*\n*Required: ${MIN_WORD_COUNT} words*\n\nPlease add more details about your progress!`,
allowedMentions: { users: [message.author.id] }
});
} catch (error) {
console.error('Error sending word count notification:', error);
}
return;
}
// Check if user already received points today
if (await hasReceivedPointsToday(message.author.username)) {
console.log(`ℹ️ ${message.author.username} already received points today`);
try {
await message.reply({
content: `🕒 **Already Earned Today!**\n\nHi ${message.author}, you've already earned your daily points today!\n\n*Come back tomorrow for your next daily update.*`,
allowedMentions: { users: [message.author.id] }
});
} catch (error) {
console.error('Error sending daily limit notification:', error);
}
return;
}
// Get member ID from database
const memberId = await getMemberIdByDiscordUsername(message.author.username);
if (!memberId) {
console.log(`❌ Member ${message.author.username} not found in database`);
try {
await message.reply({
content: `🔍 **Member Not Found!**\n\nHi ${message.author}, you're not registered in our system yet.\n\n*Please contact an administrator to register your Discord account.*`,
allowedMentions: { users: [message.author.id] }
});
} catch (error) {
console.error('Error sending member not found notification:', error);
}
return;
}
// Award points
const dateString = getTodayDateString();
const success = await awardPoints(memberId, dateString);
if (success) {
// Points are automatically marked as awarded by the database insertion
botStatus.pointsAwarded++;
// React to the message to show it was processed
try {
await message.react('✅');
console.log(`🎉 Successfully processed daily update from ${message.author.username}`);
} catch (error) {
console.error('Error reacting to message:', error);
botStatus.errors++;
}
} else {
botStatus.errors++;
// Notify user of failure via reply
try {
await message.reply({
content: `❌ **Failed to award points!**\n\nSorry ${message.author}, there was an error processing your daily update. Please contact an administrator for assistance.\n\n*Error: Unable to record points in the database*`,
allowedMentions: { users: [message.author.id] }
});
console.log(`⚠️ Notified ${message.author.username} about point awarding failure`);
} catch (replyError) {
console.error('Error sending failure notification:', replyError);
botStatus.errors++;
}
}
}
// Event handlers
client.once('clientReady', () => {
const guild = client.guilds.cache.get(GUILD_ID);
// Update bot status
botStatus.online = true;
botStatus.username = client.user.tag;
botStatus.guild = guild ? guild.name : 'Unknown';
botStatus.uptime = Date.now();
botStatus.lastActivity = new Date().toISOString();
console.log(`🚀 ${client.user.tag} is online and monitoring basher-progress!`);
console.log(`📊 Monitoring guild: ${GUILD_ID}`);
console.log(`📁 Looking for category ID: ${BASHER_PROGRESS_CATEGORY_ID}`);
console.log(`📂 Category name: ${BASHER_PROGRESS_CATEGORY_NAME}`);
console.log(`💰 Daily points: ${DAILY_POINTS}`);
console.log(`📝 Minimum words: ${MIN_WORD_COUNT}`);
console.log(`🌐 API server available at http://localhost:${PORT}`);
});
client.on('messageCreate', handleMessage);
// Also handle thread messages specifically
client.on('threadCreate', async (thread) => {
console.log(`🧵 New thread created: ${thread.name} in ${thread.parent?.name || 'unknown'}`);
if (thread.parent && thread.parent.parent) {
console.log(` Parent category: ${thread.parent.parent.name} (ID: ${thread.parent.parent.id})`);
}
});
client.on('error', error => {
console.error('Discord client error:', error);
botStatus.errors++;
botStatus.lastActivity = new Date().toISOString();
});
// Start Express server
app.listen(PORT, () => {
console.log(`🌐 API server running on http://localhost:${PORT}`);
});
// Start the bot
client.login(process.env.DISCORD_BOT_TOKEN).catch(error => {
console.error('Failed to login:', error);
botStatus.errors++;
process.exit(1);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('🛑 Shutting down bot...');
botStatus.online = false;
client.destroy();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('🛑 Shutting down bot...');
botStatus.online = false;
client.destroy();
process.exit(0);
});