-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
510 lines (433 loc) · 20.2 KB
/
main.py
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
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
import discord, requests, asyncio
from env import API_KEY, PROXIES, TOKEN, MONGO_SRV_URL, GUILD_IDS
from pymongo import MongoClient
from datetime import date
from loguru import logger
from discord.embeds import Embed
from discord.errors import HTTPException
from discord.ext import commands
from discord import Intents, Embed
from discord_slash import SlashCommand, SlashContext
from discord_slash.utils.manage_commands import create_option
from helpers.constants import BH_HEROES, TH_HEROES
from helpers.graph import plot_trophy_graph, set_graph_embed
from helpers.stats import get_player_stats, get_stats_embed
from helpers.verify import set_verify_embed, write_to_db
from helpers.clan import fetch_clan_contents, send_clan_contents, set_default_clan
from helpers.zap import set_zap_embed
from helpers.zapquake import set_zapquake_embed
mongo_client = MongoClient(MONGO_SRV_URL)
db = mongo_client.coc
col = db.users
bot = commands.Bot(command_prefix='/',intents=Intents.default(), help_command=None)
slash = SlashCommand(bot, sync_commands=True)
@bot.event
async def on_ready():
logger.info(f'We have logged in as {bot.user}')
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.playing, name="/help"))
@slash.slash(name="coc", description='Greets you.')
async def _coc(ctx: SlashContext):
logger.info('coc')
embed = Embed(
title='ClashStats',
description=f"Hello {ctx.author.name}!\nDid you know I'm on **{len(bot.guilds)}** Discord servers?\nAlso, there are **{col.count_documents({})}** verified users using me. I'm more popular than you!",
)
await ctx.send(embed=embed)
@slash.slash(name='help', description='Displays the command list.')
async def _help(ctx: SlashContext):
logger.info('help')
help_contents = []
help0 = Embed(
title="Commands!",
description="Every command starts with the prefix \"coc\" followed by a space and the keyword.",
color=0x000000
)
help0.add_field(name="`verify`", value="Verifies and authenticates you by matching up your Discord ID to your CoC player tag. Many of the following commands require verification before use.\nFollow instructions in DMs.\nEx: /verify", inline=False)
help0.add_field(name="`clan`", value="Retrieve's clan stats. Currently, the most comprehensive ClashStats has available.\nOnly available once you have verified using /verify.\nEx: /clan [clan_tag]\nUNLESS you set up your default clan already:\nEx: /clan", inline=False)
help0.add_field(name="`stats`", value="Retrieve's player's stats.\nEx: /stats [player_tag]\nUNLESS you have already verified using /verify, in which case:\n/stats.", inline=False)
help0.add_field(name="`graph`", value="Plots your trophy count daily.\nHas to be run manually each day, and can only retrieve new trophy counts each day.\nOnly available once you have verified using /verify.\nEx: /graph", inline=False)
help1 = Embed(
title="Commands!",
description="Every command starts with the prefix \"coc\" followed by a space and the keyword.",
color=0x000000
)
help1.add_field(name="`hero`", value="Lists your hero levels.\nTells how far you are from maxed hero levels at your Town Hall level.\nOnly available once you have verified using /verify.\nEx: /hero", inline=False)
help1.add_field(name="`zap`", value="Tells the player how many lightning spells are required to destroy an air defense.\nThis command requires two parameters: the air defense level and your lightning spell level.\nEx: /zap [air defense level] [lightning spell level]", inline=False)
help1.add_field(name="`zapquake`", value="Tells the player how many lightning and earthquake spells are required to destroy an air defense.\nThis command requires three parameters: the air defense level and a lightning spell level, and an earthquake spell level.\nEx: /zapquake [air defense level] [lightning spell level] [earthquake spell level]", inline=False)
help1.add_field(name="`destroy`", value="Destroy your data from ClashStats. We will no longer hold your data in our database. This decision is permanent and cannot be undone!", inline=False)
help1.add_field(name="`invite`", value="Fetches the permanent ClashStats Discord invite link.", inline=False)
help1.add_field(name="`help`", value="Sends this command list.", inline=False)
help2 = Embed(
title="Contribute & Support",
color=0x000000
)
help2.add_field(name="Questions", value="Contact me in Discord @ danmaruchi#8034, or join the support server [here](https://discord.gg/6MXVXxK7pb).", inline=False)
help2.add_field(name="Leave a review & vote", value="If you found ClashStats helpful in any way, leave a review or simply vote for ClashStats on [Top.gg](https://top.gg/bot/870085172136149002)! It takes less than a minute, and helps push out ClashStats to more players.", inline=False)
help2.add_field(name="Donate", value="And if you want to support me directly, consider [buying me a coffee](https://www.buymeacoffee.com/danmaruchi). I'm a broke college student, so anything helps.", inline=False)
help_contents.append(help0)
help_contents.append(help1)
help_contents.append(help2)
pages = len(help_contents)
cur_page = 1
for idx, content in enumerate(help_contents):
content.set_footer(text=f'Want to invite me? Run /invite to get my invite link!\npage {idx+1}/{pages}')
message = await ctx.send(embed=help_contents[cur_page-1])
await message.add_reaction("◀️")
await message.add_reaction("▶️")
def check(reaction, user):
return user == ctx.author and str(reaction.emoji) in ['◀️', '▶️']
while True:
try:
reaction, user = await bot.wait_for("reaction_add", timeout=120, check=check)
if str(reaction.emoji) == "▶️" and cur_page != pages:
cur_page += 1
await message.edit(embed=help_contents[cur_page-1])
await message.remove_reaction(reaction, user)
elif str(reaction.emoji) == "◀️" and cur_page > 1:
cur_page -= 1
await message.edit(embed=help_contents[cur_page-1])
await message.remove_reaction(reaction, user)
else:
await message.remove_reaction(reaction, user)
except asyncio.TimeoutError:
await message.clear_reactions()
break
@slash.slash(name='invite', description='Get the ClashStats invite link.')
async def _invite(ctx: SlashContext):
logger.info('invite')
invite_embed = Embed(
title="Invite me to your server!",
description="[ClashStats Invite Link](https://discord.com/api/oauth2/authorize?client_id=870085172136149002&permissions=414464658496&scope=bot%20applications.commands)",
color=0x000000
)
await ctx.send(embed=invite_embed)
@slash.slash(
name='stats',
description='Get player stats.',
options=[
create_option(
name='tag',
description='Your player tag',
option_type=3,
required=False
)
]
)
async def _stats(ctx: SlashContext, tag: str=None):
logger.info('stats')
author_id = str(ctx.author.id)
selector = {'_id': author_id}
if (user := col.find_one(selector)) is not None:
user_tag = user['player_tag']
if user and tag is None:
await ctx.send(embed=get_stats_embed(user_tag))
return
if tag is None:
await ctx.send("Please enter a tag or run `/verify` to use `/stats` without entering a player tag.")
return
tag = tag.replace('#', "%23")
await ctx.send(embed=get_stats_embed(tag))
@slash.slash(name='verify', description='Link your Discord profile to your Clash of Clans account.')
async def _verify(ctx: SlashContext):
logger.info('verify')
await ctx.defer()
author_id = str(ctx.author.id)
selector = {'_id': author_id}
if col.find_one(selector) is not None:
embed_var = Embed(
title="Already verified!",
description=f"**{ctx.author.name}**, you're good to go! No further verification needed.",
color=0x32C12C
)
await ctx.send(embed=embed_var)
return
def check(msg) -> bool:
return msg.author == ctx.author and str(msg.channel.type) == "private"
await ctx.author.send("Enter your in-game player tag followed by a space and then your player token!\nFor example: `#IN-GAME-PLAYERTAG apitoken`\nYour API token can be found in-game. Gear Icon -> More Settings -> Tap 'Show' to see API token")
user_data = await bot.wait_for("message", check=check)
try:
user_tag = user_data.content.split(' ')[0].replace('#', '%23')
user_token = user_data.content.split(' ')[1]
except:
error_embed = Embed(title='Input Error', description='Please try verifying again in the server you ran this command in!', color=0xFF0000)
await ctx.author.send(embed=error_embed)
await ctx.send(embed=error_embed)
url = f'https://api.clashofclans.com/v1/players/{user_tag}/verifytoken'
token_data = {
'token': f'{user_token}'
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'authorization': f'Bearer {API_KEY}',
}
try:
response = requests.post(
url,
json=token_data,
headers=headers,
proxies=PROXIES
)
except HTTPException as e:
print('error: ', e)
error_embed = Embed(title='Request Error', description='There was an error with the server. Please try again.', color=0xFF0000)
await ctx.author.send(embed=error_embed)
await ctx.send(embed=error_embed)
res = response.json()
if 'status' in res:
if res['status'] == 'ok':
write_to_db(author_id, user_tag) # async 1
embed_var = set_verify_embed(ctx.author.name) # async 2
await ctx.author.send(embed=embed_var) # send verified in dm, async 4
await ctx.send(embed=embed_var) # async 3
elif res['status'] == 'invalid':
error_embed = Embed(title='Input Error', description='Please try verifying again in the server you ran this command in!', color=0xFF0000)
await ctx.author.send(embed=error_embed)
await ctx.send(embed=error_embed)
elif 'status' not in res:
error_embed = Embed(title='API Error', description='There was an error with the Clash of Clans API. Please try again later, or report this issue in the support server.', color=0xFF0000)
await ctx.author.send(embed=error_embed)
await ctx.send(embed=error_embed)
@slash.slash(name='graph', description='Graphs your daily trophy count.')
async def _graph(ctx: SlashContext):
logger.info('graph')
author_id = str(ctx.author.id)
selector = {'_id': author_id}
if (user_data := col.find_one(selector)) is None:
remind_embed = Embed(
title="Verification Required",
description="Please verify by running `/verify` to run `/graph`"
)
await ctx.send(embed=remind_embed)
return
today = str(date.today().strftime('%b %d %y'))
if today in user_data['trophy_data']: # graphs "today" graph and does not make a new graph. Just uses the info alr in database for "today"
plot_trophy_graph(user_data['trophy_data'])
file = discord.File("./graph.png", filename="graph.png")
await ctx.send(file=file, embed=set_graph_embed(ctx.author.name))
return
tag = str(user_data['player_tag'])
player_stats = get_player_stats(tag)
current_trophy_count = str(player_stats['trophies'])
col.update_one(
{
'_id': author_id
},
{
'$set': {
f"trophy_data.{today}": current_trophy_count
}
}
)
user_data = col.find_one({'_id': author_id})
plot_trophy_graph(user_data['trophy_data'])
file = discord.File("./graph.png", filename="graph.png")
await ctx.send(file=file, embed=set_graph_embed(ctx.author.name))
@slash.slash(name='destroy', description='Destroy your data from ClashStats.')
async def _destroy(ctx: SlashContext):
await ctx.defer()
author_id = str(ctx.author.id)
selector = {'_id': author_id}
if (user := col.find_one(selector)) is None:
embed_var = Embed(
title="You don't have any data with us!",
description=f"**{ctx.author.name}**, you're good to go!",
color=0x32C12C
)
await ctx.send(embed=embed_var)
return
def check(msg) -> bool:
return msg.author == ctx.author and str(msg.channel.type) == "private"
await ctx.author.send("Please type your in-game user tag to confirm this decision! Ex: #ABCDEFGH")
user_res = await bot.wait_for("message", check=check)
if 'player_tag' not in user:
embed_var = Embed(
title="There was an error.",
description=f"**{ctx.author.name}**, please join the support server. We weren't able to delete your data.",
color=0xFF0000
)
await ctx.author.send(embed=embed_var)
await ctx.send(embed=embed_var)
return
user_content = str(user_res.content).replace('#', '%23')
if user_content == user['player_tag']:
deletion_selector = {'_id': user['_id']}
try:
col.delete_one(deletion_selector)
embed_var = Embed(
title="Successfully deleted!",
description=f"**{ctx.author.name}**, you're good to go!",
color=0x32C12C
)
await ctx.author.send(embed=embed_var)
await ctx.send(embed=embed_var)
return
except:
embed_var = Embed(
title="There was an error.",
description=f"**{ctx.author.name}**, please join the support server. We weren't able to delete your data.",
color=0xFF0000
)
await ctx.author.send(embed=embed_var)
await ctx.send(embed=embed_var)
return
embed_var = Embed(
title="There was an error.",
description=f"**{ctx.author.name}**, you did not type in the correct player tag. Please try again.",
color=0xFF0000
)
await ctx.author.send(embed=embed_var)
await ctx.send(embed=embed_var)
return
@slash.slash(name='hero', description='Get your hero stats.')
async def _hero(ctx):
logger.info('hero')
author_id = str(ctx.author.id)
selector = {'_id': author_id}
if (user_data := col.find_one(selector)) is None:
await ctx.send("Please verify by using `/verify` to use `/hero`")
return
player_stats = get_player_stats(user_data['player_tag'])
if 'townHallLevel' not in player_stats:
await ctx.send('There was an error with the Clash of Clans API. Please try again later.')
return
if 'heroes' not in player_stats:
await ctx.send('There was an error with the Clash of Clans API. Please try again later.')
return
hall_level = player_stats['townHallLevel']
heroes = player_stats['heroes']
if not len(heroes):
await ctx.send("You don't have any heroes :(")
return
embed_var = discord.Embed(
title=f"{player_stats['name']}'s Heroes",
color=0x000000
)
for hero in heroes:
HERO_DICT = TH_HEROES
if hero['name'] == 'Barbarian King':
hero_name = f"{hero['name']} 👑"
elif hero['name'] == 'Archer Queen':
hero_name = f"{hero['name']} 🏹"
elif hero['name'] == 'Grand Warden':
hero_name = f"{hero['name']} 🪄"
elif hero['name'] == 'Royal Champion':
hero_name = f"{hero['name']} 🔱"
elif hero['name'] == 'Battle Machine':
hero_name = f"{hero['name']} 🔨"
HERO_DICT = BH_HEROES
hall_level = player_stats['builderHallLevel']
else:
hero_name = hero['name']
max_hero_level = HERO_DICT[hall_level][hero['name']]['max_level']
maxed_percentage = round((hero["level"] / max_hero_level) * 100, 2)
if (level_to_max := max_hero_level - hero['level']):
time_to_max = HERO_DICT[hall_level][hero['name']]['max_time'][-level_to_max:]
time_to_max = sum(time_to_max) / 24
embed_value = f"Level {hero['level']} ({maxed_percentage}%)\nYour {hero['name']} is {level_to_max} level(s) away from maxed.\nOnly {time_to_max} days left to go."
else:
embed_value = f"Level {hero['level']} ({maxed_percentage}%)\nYour {hero['name']} is maxed.\nThat's some dedication!"
embed_var.add_field(
name=hero_name,
value=embed_value,
inline=False
)
await ctx.send(embed=embed_var)
@slash.slash(
name='clan',
description='Get clan stats.',
options=[
create_option(
name='clan_tag',
description='Clan tag',
option_type=3,
required=False,
)
]
)
async def _clan(ctx: SlashContext, clan_tag: str=''):
logger.info('clan')
author_id = str(ctx.author.id)
selector = {'_id': author_id}
projection = {'clan_tag': 1}
if (user_data := col.find_one(selector, projection)) is None:
await ctx.send("Please verify by using `/verify` to use `/clan`")
return
if clan_tag == '' and 'clan_tag' in user_data:
clan_tag = user_data['clan_tag']
contents = await fetch_clan_contents(clan_tag)
elif len(clan_tag):
try:
contents = await set_default_clan(bot, ctx, clan_tag, author_id)
except Exception as e:
await ctx.send(e)
else:
error_embed = Embed(title='Input Error', description='Please put a clan tag after `/clan`!', color=0xFF0000)
await ctx.send(embed=error_embed)
try:
await send_clan_contents(bot, ctx, contents)
except Exception as e:
logger.error(f'ERROR: {e}')
@slash.slash(
name='zap',
description='Check how many lightning spells it takes to destroy an air defense.',
options=[
create_option(
name='airdef',
description='Air Defense Level',
option_type=4,
required=True
),
create_option(
name='zap',
description='Lightning Spell Level',
option_type=4,
required=True
)
]
)
async def _zap(ctx: SlashContext, airdef, zap):
logger.info('zap')
airdef = int(airdef)
zap = int(zap)
if (airdef < 1 or airdef > 12) or (zap < 1 or zap > 9):
await ctx.send("Please enter valid air defense and/or lightning spell levels. Ex: /zap [air defense level] [lightning spell level]")
return
embed_var = await set_zap_embed(airdef, zap)
await ctx.send(embed=embed_var)
@slash.slash(
name='zapquake',
description='Check how many lightning and earthquake spells it takes to destroy an air defense.',
options=[
create_option(
name='airdef',
description='Air Defense Level',
option_type=4,
required=True
),
create_option(
name='zap',
description='Lightning Spell Level',
option_type=4,
required=True
),
create_option(
name='quake',
description='Earthquake Spell Level',
option_type=4,
required=True
)
]
)
async def _zapquake(ctx: SlashContext, airdef, zap, quake):
logger.info('zapquake')
airdef = int(airdef)
zap = int(zap)
quake = int(quake)
if (airdef < 1 or airdef > 12) or (zap < 1 or zap > 9) or (quake < 1 or quake > 5):
await ctx.send("Please enter valid air defense, lightning spell levels, and/or earthquake spell levels. Ex: /zapquake [air defense level] [lightning spell level] [earthquake spell level]")
return
embed_var = await set_zapquake_embed(airdef, zap, quake)
await ctx.send(embed=embed_var)
if __name__ == '__main__':
bot.run(TOKEN)