-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
415 lines (307 loc) · 12.6 KB
/
main.py
File metadata and controls
415 lines (307 loc) · 12.6 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
import discord
from discord.ext import commands, tasks
import logging
from dotenv import load_dotenv
import os
import re
from datetime import datetime, time as dt_time, timezone, timedelta
import asyncio
from functools import partial
from services.exam_tracker import ExamTracker
from services.reminder import Reminder
import analytics as anal
load_dotenv()
CAMPUSES = ["pilani", "goa", "hyderabad"]
exam_tracker = ExamTracker()
reminder_service = Reminder()
token = os.getenv("DISCORD_TOKEN")
handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
bot = commands.Bot(command_prefix="!!", intents=intents)
bot.remove_command("help")
DISCLAIMER_MSG = "all scores pre-**2022** have been standardized to **390**, so a score in **2021** which may have been **300** becomes **260** in current standards and settings of exam."
IST = timezone(timedelta(hours=5, minutes=30))
async def display(ctx, cache_key, title, generator_func, filename, disclaimer=True):
"""
generates a plot/table based on user-req throught generator_func availed in analytics.py, if cache-hit is found it immediately sends the embed of that to user.
disclaimer message is optional.
"""
cached_url = anal.get_cached_url(cache_key)
if cached_url:
print(f"cache hit: {cache_key}")
embed = discord.Embed(title=title)
embed.set_image(url=cached_url)
await ctx.send(embed=embed)
if disclaimer:
await ctx.send(DISCLAIMER_MSG)
return True
image_buffer = await generator_func
if image_buffer is None:
return None
try:
image_buffer.seek(0)
file = discord.File(fp=image_buffer, filename=filename)
sent_message = await ctx.send(file=file)
if disclaimer:
await ctx.send(DISCLAIMER_MSG)
if sent_message.attachments:
anal.save_url_to_cache(cache_key, sent_message.attachments[0].url)
print(f"cached: {cache_key}")
except Exception as e:
await ctx.send(f"error uploading: {e}")
return None
finally:
image_buffer.close()
return True
def parse_campus(args):
clean_args = args.lower()
for c in CAMPUSES:
if c in clean_args:
return c, clean_args.replace(c, "", 1)
return None, clean_args
async def send_dm(user_id, message):
try:
user = await bot.fetch_user(user_id)
await user.send(message)
print(f"sent DM fallback to {user.name}")
except Exception as e:
print(f"failed to DM user {user_id}: {e}")
@tasks.loop(time=dt_time(hour=9, minute=0, tzinfo=IST))
async def send_exam_reminders():
"""
send daily reminders in the channel where interaction with bot was made.
"""
reminder_service.reset_daily_tracking()
reminders = reminder_service.users_to_remind()
for reminder in reminders:
channel = bot.get_channel(reminder["channel_id"])
msg = f"<@{reminder['user_id']}> {reminder['message']}"
if channel:
try:
await channel.send(msg)
print(
f"sent reminder to user {reminder['user_id']} in channel {channel.name}"
)
except discord.Forbidden:
print(f"no permission in channel {reminder['channel_id']}")
await send_dm(reminder["user_id"], reminder["message"])
except Exception as e:
print(f"failed to send reminder: {e}")
else:
print(f"channel {reminder['channel_id']} not found")
await send_dm(reminder["user_id"], reminder["message"])
@send_exam_reminders.before_loop
async def before_reminder():
await bot.wait_until_ready()
@bot.event
async def on_ready():
print("ready when you're")
if not send_exam_reminders.is_running():
send_exam_reminders.start()
@bot.command(name="plot")
async def plot(ctx, *, args: str = None):
if not args:
return await ctx.send("Usage: `!!plot <campus>`")
campus, _ = parse_campus(args)
if not campus:
return await ctx.send("invalid campus. Please use Pilani, Goa, or Hyderabad.")
loop = asyncio.get_event_loop()
generator = loop.run_in_executor(None, partial(anal.plot_marks_by_campus, campus))
result = await display(
ctx,
cache_key=f"plot_{campus}",
title=f"{campus.title()} Cutoff Trends",
generator_func=generator,
filename=f"{campus}_plot.png",
)
if result is None:
await ctx.send(f"No data found for campus: {campus}")
@bot.command(name="plot-branch")
async def plot_branch(ctx, *, args: str = None):
if not args:
return await ctx.send("Usage: `!!plot-branch <branch> <campus>`")
campus, clean_args = parse_campus(args)
if not campus:
return await ctx.send("please specify a campus (Pilani, Goa, or Hyderabad).")
branch = clean_args.replace(",", "").strip()
if not branch:
return await ctx.send("please specify a branch name.")
await ctx.send(f"generating plot for **{branch}** in **{campus.title()}**...")
loop = asyncio.get_event_loop()
generator = loop.run_in_executor(
None, partial(anal.plot_marks_by_branch, campus, branch)
)
result = await display(
ctx,
cache_key=f"plot_branch_{campus}_{branch}",
title=f"{branch} - {campus.title()}",
generator_func=generator,
filename=f"{campus}_{branch}.png",
)
if result is None:
await ctx.send(f"no data found for **{branch}** in **{campus}**.")
@bot.command(name="select")
async def select(ctx, *, args: str = None):
if not args:
return await ctx.send("usage: `!!select 2024` or `!!select 2024 Pilani`")
year_match = re.search(r"\b(20\d{2})\b", args)
if not year_match:
return await ctx.send(
"could not find a valid year (e.g., 2024) in your command."
)
year = int(year_match.group(1))
remaining_args = args.replace(year_match.group(1), "")
campus, _ = parse_campus(remaining_args)
campus_title = campus.title() if campus else None
filter_msg = f" - {campus_title}" if campus_title else " - All Campuses"
await ctx.send(
f"fetching **{year}** cutoffs{filter_msg.replace(' - ', ' for ')}..."
)
loop = asyncio.get_event_loop()
generator = loop.run_in_executor(
None, partial(anal.select, limit=25, year=year, campus_filter=campus_title)
)
result = await display(
ctx,
cache_key=f"select_{year}_{campus_title or 'all'}",
title=f"{year} Cutoffs{filter_msg}",
generator_func=generator,
filename=f"cutoff_{year}.png",
)
if result is None:
await ctx.send(f"No data found for {year}.")
@bot.command(name="predict")
async def predict(ctx, *, args: str = None):
situation = "most-likely"
campus = None
if args:
raw_args = args.lower().replace("most likely", "most-likely")
for s in ["worst", "best", "most-likely"]:
if s in raw_args:
situation = s
raw_args = raw_args.replace(s, "")
break
campus = raw_args.replace(",", "").strip() or None
filter_msg = f" - {campus.title()}" if campus else " - All Campuses"
await ctx.send(
f"fetching **{situation}** case predictions{filter_msg.replace(' - ', ' for ')}..."
)
loop = asyncio.get_event_loop()
generator = loop.run_in_executor(
None,
partial(
anal.get_predictions, limit=25, campus_filter=campus, situation=situation
),
)
result = await display(
ctx,
cache_key=f"predict_{situation}_{campus or 'all'}",
title=f"{situation.title()} Case Predictions{filter_msg}",
generator_func=generator,
filename=f"pred_2026_{situation}.png",
)
if result is None:
await ctx.send("no prediction data found.")
@bot.command()
async def sy(ctx):
await ctx.send(
"please check: https://admissions.bits-pilani.ac.in/FD/downloads/BITSAT_Syllabus.pdf?06012025"
)
@bot.command()
async def ypt(ctx):
await ctx.send(
"Group Name: BITSATards, Password: 123\nthe link is as follows: https://link.yeolpumta.com/P3R5cGU9Z3JvdXBJbnZpdGUmaWQ9MzU5OTUzNg=="
)
@bot.command()
async def da(ctx):
await ctx.send(
"Session 1 Starts From: Wednesday, 15 April 2026\nSession 2 Starts From: Sunday, 24 May 2026"
)
@bot.command()
async def resources(ctx):
await ctx.send(
"please follow: https://www.reddit.com/r/Bitsatards/wiki/resources/ \nadditionally please refer to: https://discord.com/channels/1221093390167576646/1224005178106187877"
)
@bot.command()
async def src(ctx):
await ctx.send(
"please consider starring this project if you liked it: https://github.com/PranavU-Coder/bitsatards_bot"
)
@bot.command()
async def web(ctx):
await ctx.send("the website: https://bitsat-predictor.com/")
@bot.command()
async def time(ctx, flag: str = None, *, date_str: str = None):
"""
this is for users to effectively track how much time they have till their D-Day.
flags:
!!time (default, no flags) does the job of returning user how much time is left till their exam.
!!time -s DD-MM-YYYY to set the exam date to track.
!!time -r will reset exam-date that has been set by user.
"""
try:
if flag is None:
return await ctx.send(exam_tracker.get_countdown(ctx.author.id))
if flag in ["-s", "-set", "--set"]:
if not date_str:
return await ctx.send(
"please provide a date\nusage: `!!exam -s DD-MM-YYYY`\nExample: `!!time -s 15-04-2026`"
)
try:
exam_date = datetime.strptime(date_str, "%d-%m-%Y").date()
message = exam_tracker.set_exam_date(
user_id=ctx.author.id,
username=str(ctx.author),
channel_id=ctx.channel.id,
exam_date=exam_date,
)
await ctx.send(message)
except ValueError:
await ctx.send(
"invalid date format!\nplease use: `!!exam -s DD-MM-YYYY`\nExample: `!!time -s 15-04-2026`"
)
elif flag in ["-r", "-reset", "--reset"]:
await ctx.send(exam_tracker.reset(ctx.author.id))
except Exception as e:
await ctx.send(f"error: {str(e)}")
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, commands.MissingRequiredArgument):
await ctx.send("missing a required argument! check command usage.")
elif isinstance(error, commands.BadArgument):
await ctx.send(
"invalid argument provided, did you type text instead of a year?"
)
else:
print(f"error: {error}")
await ctx.send("an unexpected error occurred.")
@bot.command()
async def help(ctx):
helper = """
This bot helps you get a rough idea of BITSAT exam cutoffs for the upcoming year, please note that predictions are estimates and may not reflect actual values.
**Commands:**
- `!!plot [campus-name]` - plot marks trend for all branches in a campus
Example: `!!plot Pilani`
- `!!plot-branch [branch-name] [campus-name]` - plot marks trend for a specific branch
Example: `!!plot-branch Computer Science Pilani` or `!!plot-branch cse Pilani`
Note: you can use shortcuts like `cse`, `ece`, `mech`, etc. Comma-separators are no longer needed.
- `!!select [year] [campus-name]` - shows cutoff values for a particular year in tabulated format for all or a specified campus
Example: `!!select 2025`, `!!select 2025 pilani`
- `!!predict [campus-name] [situation]` - shows predictions for 2026 BITSAT exam with different scenarios (worst/best/most-likely)
Example: `!!predict`, `!!predict Pilani`, `!!predict Pilani worst`
- `!!da` - get exam-shift dates for both sessions
- `!!time` - track your time till your bitsat-shift, use flags such as -s & -r for your needs
Example: `!!time -s 15-04-2026` or `!!time -r`
- `!!ypt` - get the link, group-name & password for the official bitsatards-YPT group
- `!!sy` - get syllabus for current BITSAT examination
- `!!resources` - get study resources for the BITSAT examination
- `!!src` - get the source code of this bot
- `!!web` - gets the website link for bitsat-predictor
- `!!help` - shows this help message
**Available Campuses:** Pilani, Goa, Hyderabad
**Branch Shortcuts:** cse, ece, eee, mech, civil, chem, eni, manufacturing, pharm, and more
"""
await ctx.send(helper)
bot.run(token, log_handler=handler, log_level=logging.DEBUG)