diff --git a/modules/background_tasks.py b/modules/background_tasks.py new file mode 100644 index 0000000..449fad1 --- /dev/null +++ b/modules/background_tasks.py @@ -0,0 +1,200 @@ +import matplotlib.pyplot as plt +import matplotlib.ticker as mticker +import matplotlib.dates as mdates +import asyncio +import discord +import pandas as pd +import random +import numpy as np +import time +import os +import re + +# py -3.6 -m pip install --upgrade requests-html +from collections import Counter + +from .definitions import local_files_path +from .definitions import bot +from .definitions import SENTEX_GUILD_ID +from .definitions import DAYS_BACK, MOST_COMMON_INT, RESAMPLE, DISCORD_BG_COLOR +from .definitions import COMMUNITY_BASED_CHANNELS, HELP_CHANNELS + + +def community_report(guild): + """ + + Args: + guild: + + Returns: + online: int + idle: int + offline: int + """ + online = 0 + idle = 0 + offline = 0 + + for m in guild.members: + if str(m.status) == "online": + online += 1 + elif str(m.status) == "offline": + offline += 1 + else: + idle += 1 + + return online, idle, offline + + +def df_match(c1, c2): + if c1 == c2: + return np.nan + else: + return c1 + + +async def user_metrics_background_task(): + await bot.wait_until_ready() + global sentdex_guild + sentdex_guild = bot.get_guild(SENTEX_GUILD_ID) + + while not bot.is_closed(): + try: + online, idle, offline = community_report(sentdex_guild) + metrics_path = os.path.join(local_files_path, "usermetrics.csv") + with open(metrics_path, "a") as f: + f.write(f"{int(time.time())},{online},{idle},{offline}\n") + + df_msgs = pd.read_csv(os.path.join(local_files_path, "msgs.csv"), + names=['time', 'uid', 'channel'] + ) + df_msgs = df_msgs[(df_msgs['time'] > time.time() - (86400 * DAYS_BACK))] + df_msgs['count'] = 1 + df_msgs['date'] = pd.to_datetime(df_msgs['time'], unit='s') + df_msgs.drop("time", 1, inplace=True) + df_msgs.set_index("date", inplace=True) + + df_no_dup = df_msgs.copy() + df_no_dup['uid2'] = df_no_dup['uid'].shift(-1) + df_no_dup['uid_rm_dups'] = list(map(df_match, df_no_dup['uid'], df_no_dup['uid2'])) + + df_no_dup.dropna(inplace=True) + + message_volume = df_msgs["count"].resample(RESAMPLE).sum() + + user_id_counts_overall = Counter( + df_no_dup[df_no_dup['channel'].isin(COMMUNITY_BASED_CHANNELS)]['uid'].values).most_common( + MOST_COMMON_INT) + # print(user_id_counts_overall) + + uids_in_help = Counter(df_no_dup[df_no_dup['channel'].isin(HELP_CHANNELS)]['uid'].values).most_common( + MOST_COMMON_INT) + # print(uids_in_help) + + df = pd.read_csv(metrics_path, names=['time', 'online', 'idle', 'offline']) + df = df[(df['time'] > time.time() - (86400 * DAYS_BACK))] + df['date'] = pd.to_datetime(df['time'], unit='s') + df['total'] = df['online'] + df['offline'] + df['idle'] + df.drop("time", 1, inplace=True) + df.set_index("date", inplace=True) + + df = df.resample(RESAMPLE).mean() + df = df.join(message_volume) + + df.dropna(inplace=True) + # print(df.head()) + + fig = plt.figure(facecolor=DISCORD_BG_COLOR) + ax1 = plt.subplot2grid((2, 1), (0, 0)) + plt.ylabel("Active Users") + plt.title("Community Report") + ax1.set_facecolor(DISCORD_BG_COLOR) + ax1v = ax1.twinx() + plt.ylabel("Message Volume") + # ax1v.set_facecolor(DISCORD_BG_COLOR) + ax2 = plt.subplot2grid((2, 1), (1, 0)) + plt.ylabel("Total Users") + ax2.set_facecolor(DISCORD_BG_COLOR) + + ax1.plot(df.index, df.online, label="Active Users\n(Not Idle)") + # ax1v.bar(df.index, df["count"], width=0.01) + + ax1v.fill_between(df.index, 0, df["count"], facecolor="w", alpha=0.2, label="Message Volume") + ax1.legend(loc=2) + ax1v.legend(loc=9) + + ax2.plot(df.index, df.total, label="Total Users") + ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d\n%H:%M')) + + # for label in ax2.xaxis.get_ticklabels(): + # label.set_rotation(45) + ax2.xaxis.set_major_locator(mticker.MaxNLocator(nbins=5, prune='lower')) + ax2.legend() + + plt.subplots_adjust(left=0.11, bottom=0.10, right=0.89, top=0.95, wspace=0.2, hspace=0) + ax1.get_xaxis().set_visible(False) + + ax1v.set_ylim(0, 3 * df["count"].values.max()) + + # plt.show() + plt.savefig(os.path.join(local_files_path, "online.png"), facecolor=fig.get_facecolor()) + plt.clf() + + fig = plt.figure(facecolor=DISCORD_BG_COLOR) + ax1 = plt.subplot2grid((2, 1), (0, 0)) + + plt.xlabel("Message Volume") + plt.title(f"General User Activity (past {DAYS_BACK} days)") + ax1.set_facecolor(DISCORD_BG_COLOR) + + users = [] + msgs = [] + for pair in user_id_counts_overall[::-1]: + try: + users.append(sentdex_guild.get_member(pair[0]).name) # get member name from here + if "Dhanos" in sentdex_guild.get_member(pair[0]).name: + msgs.append(pair[1] / 1.0) + else: + msgs.append(pair[1]) + except Exception as e: + print(str(e)) + y_pos = np.arange(len(users)) + ax1.barh(y_pos, msgs, align='center', alpha=0.5) + plt.yticks(y_pos, users) + + ax2 = plt.subplot2grid((2, 1), (1, 0)) + plt.title(f"Help Channel Activity (past {DAYS_BACK} days)") + plt.xlabel("Help Channel\nMsg Volume") + ax2.set_facecolor(DISCORD_BG_COLOR) + + users = [] + msgs = [] + for pair in uids_in_help[::-1]: + try: + users.append(sentdex_guild.get_member(pair[0]).name) # get member name from here + + if "Dhanos" in sentdex_guild.get_member(pair[0]).name: + msgs.append(pair[1] / 1.0) + else: + msgs.append(pair[1]) + # users.append(pair[0]) + except Exception as e: + print(str(e)) + + y_pos = np.arange(len(users)) + ax2.barh(y_pos, msgs, align='center', alpha=0.5) + plt.yticks(y_pos, users) + + plt.subplots_adjust(left=0.30, bottom=0.15, right=0.99, top=0.95, wspace=0.2, hspace=0.55) + plt.savefig(os.path.join(local_files_path, "activity.png"), facecolor=fig.get_facecolor()) + plt.clf() + + await asyncio.sleep(300) + + except Exception as e: + print(str(e)) + await asyncio.sleep(300) + + +def start_all_background_tasks(): + bot.loop.create_task(user_metrics_background_task()) diff --git a/modules/bot.py b/modules/bot.py new file mode 100644 index 0000000..f4e61fe --- /dev/null +++ b/modules/bot.py @@ -0,0 +1,413 @@ +import traceback +import asyncio +import discord +import random +import time +import os +import re + +# py -3.6 -m pip install --upgrade requests-html +from requests_html import HTMLSession +from matplotlib import style +from discord.ext.commands import CommandError +from discord import Embed, Colour + +from .definitions import bot, local_files_path, HELLO_TEXTS +from .definitions import SENTDEX_ID, SENTEX_GUILD_ID +from .definitions import MAIN_CH_ID, IMAGE_CHAN_IDS, VANITY_ROLE_IDS, CHATBOTS + +from .permissions import is_called_in_bot_channel, is_priv, is_not_priv, is_admin +from .decorators import advanced_perm_check_function, advanced_args_function + +from .background_tasks import community_report +from .minigames import mines +from .emojis import EMOJIS, HAPPY_FACES + +style.use("dark_background") + +commands_available = """```py +def commands(): + return { + sentdebot.member_count(): 'get member count', + sentdebot.community_report(): 'get some stats on community', + sentdebot.search("QUERY"): 'search for a string', + sentdebot.commands(): 'get commands', + sentdebot.logout(): 'Sentdex-only command to log Sentdebot off' + sentdebot.user_activity(): 'See some stats on top users', + } +```""" + + +def search_term(message): + try: + match = re.fullmatch('sentdebot.search\((["|\'])([^\)]+)(["|\'])\)', message) + if match.group(1) != match.group(3): + return False + for check in re.finditer(match.group(1), match.group(2)): + if match.group(2)[check.start() - 1] != '\\': + return False + return match.group(2) + except Exception: + return False + + +@bot.event # event decorator/wrapper +async def on_ready(): + print(f"We have logged in as {bot.user}") + await bot.change_presence(status=discord.Status.online, activity=discord.Game('help(sentdebot)')) + + +@bot.event +async def on_message(message): + print(f"{message.channel}: {message.author}: {message.author.name}: {message.content}") + sentdex_guild = bot.get_guild(SENTEX_GUILD_ID) + + # print(author_roles) + # author_role_ids = [r.id for r in author_roles] + + if random.choice(range(500)) == 30: + try: + author_roles = message.author.roles + matches = [r for r in author_roles if r.id in VANITY_ROLE_IDS] + # print(matches) + + if len(matches) == 0: + try: + role_id_choice = random.choice(VANITY_ROLE_IDS) + actual_role_choice = sentdex_guild.get_role(role_id_choice) + # print(type(message.author)) + author_roles.append(actual_role_choice) + await message.author.edit(roles=author_roles) + except Exception as e: + print('EDITING ROLES ISSUE:', str(e)) + except Exception: + pass + with open(os.path.join(local_files_path, 'msgs.csv'), "a") as f: + if message.author.id not in CHATBOTS: + f.write(f"{int(time.time())}, {message.author.id}, {message.channel}\n") + + with open(os.path.join(local_files_path, 'log.csv'), "a") as f: + if message.author.id not in CHATBOTS: + try: + f.write(f"{int(time.time())}, {message.author.id}, {message.channel}, {message.content}\n") + except Exception as e: + f.write(f"{str(e)}\n") + + if "sentdebot.member_count()" == message.content.lower(): + await message.channel.send(f"```py\n{sentdex_guild.member_count}```") + + elif "sentdebot.community_report()" == message.content.lower() and message.channel.id in IMAGE_CHAN_IDS: + online, idle, offline = community_report(sentdex_guild) + + file = discord.File(os.path.join(local_files_path, "online.png"), filename=f"online.png") + await message.channel.send("", file=file) + + await message.channel.send( + f'```py\n{{\n\t"Online": {online},\n\t"Idle/busy/dnd": {idle},\n\t"Offline": {offline}\n}}```') + + elif "sentdebot.p6()" == message.content.lower(): + await message.channel.send(f"```\nThe Neural Networks from Scratch videos will resume one day. https://nnfs.io```") + + elif "sentdebot.user_activity()" == message.content.lower() and message.channel.id in IMAGE_CHAN_IDS: # and len([r for r in author_roles if r.id in admins_mods_ids]) > 0: + file = discord.File(os.path.join(local_files_path, "activity.png"), filename=f"activity.png") + await message.channel.send("", file=file) + + # await message.channel.send(f'```py\n{{\n\t"Online": {online},\n\t"Idle/busy/dnd": {idle},\n\t"Offline": {offline}\n}}```') + + elif "help(sentdebot)" == message.content.lower() or "sentdebot.commands()" == message.content.lower(): + await message.channel.send(commands_available) + + # if it doesnt work later. + # elif "sentdebot.logout()" == message.content.lower() and message.author.id == 324953561416859658: + elif "sentdebot.logout()" == message.content.lower() and message.author.id == SENTDEX_ID: + await bot.close() + + elif "sentdebot.gtfo()" == message.content.lower() and message.author.id == SENTDEX_ID: + await bot.close() + + elif "sentdebot.get_history()" == message.content.lower() and message.author.id == SENTDEX_ID: + + channel = sentdex_guild.get_channel(MAIN_CH_ID) + + async for message in channel.history(limit=999999999999999): + if message.author.id == SENTDEX_ID: + with open(os.path.join(local_files_path, "history_out.csv"), "a") as f: + f.write(f"{message.created_at},1\n") + + elif message.content.lower().startswith("sentdebot.search"): + query = search_term(message.content) + print(f"Query: {query}") + if query: + # query = match.group(1) + print(query) + + qsearch = query.replace(" ", "%20") + full_link = f"https://pythonprogramming.net/search/?q={qsearch}" + session = HTMLSession() + r = session.get(full_link) + + specific_tutorials = [(tut.text, list(tut.links)[0]) for tut in r.html.find("a") if + "collection-item" in tut.html] + + if len(specific_tutorials) > 0: + return_str = "\n---------------------------------------\n".join( + f'{tut[0]}: ' for tut in specific_tutorials[:3]) + return_str = f"```Searching for '{query}'```\n" + return_str + f"\n----\n...More results: <{full_link}>" + + await message.channel.send(return_str) + else: + await message.channel.send(f"""```py +Traceback (most recent call last): + File "", line 1, in +NotFoundError: {query} not found```""") + + elif bot.user in message.mentions and not message.content.startswith("!"): + ch = message.channel + text = random.choice(HELLO_TEXTS) + emote = random.choice(HAPPY_FACES) + await ch.send(text.format(message.author.name, emote=emote)) + else: + await bot.process_commands(message) + + +async def get_last_message(channel_id, user_id, limit=10): + channel = bot.get_channel(channel_id) + messages = await channel.history(limit=limit).flatten() + message = None + for msg in messages: + if msg.author.id == user_id: + message = msg + break + return message + + +@bot.event +async def on_command_error(ctx, command_error): + text = ctx.message.content + invoked = ctx.invoked_with + text_error = str(command_error) + server = "private_message" if not ctx.guild else f"{ctx.guild} ({ctx.guild.id})" + + if text_error.startswith("The check functions for command") or text_error.startswith("No permission"): + # logger.warning(f"No permission: '{text}', server: '{server}'") + emoji = "⛔" + await ctx.channel.send(f"Some permissions do not allow it to run here, '{text_error}' '!{invoked}'", + delete_after=30) + + elif text_error.endswith("is not found"): + # logger.warning(f"Command not found: '{text}', server: '{server}'") + emoji = "❓" + + elif text_error.startswith("Command raised an exception: RestrictedError"): + # logger.warning(f"Restricted usage: '{text}', server: '{server}'") + emoji = "⛔" + await ctx.channel.send(f"{command_error.original} '!{invoked}'", delete_after=30) + + elif text_error.startswith("Command raised an exception: CommandWithoutPermissions"): + # logger.error(f"Command is free to all users: '{text}', server: '{server}'") + emoji = "⛔" + await ctx.channel.send(f"Command is not checking any permissions, sorry. '!{invoked}'", delete_after=30) + + elif text_error.startswith("You are missing"): + # logger.warning(f"Missing permission: '{text_error}', server: '{server}'") + emoji = "❌" + await ctx.channel.send(f"{text_error} '!{invoked}'", delete_after=30) + + elif text_error.startswith("Command raised an exception: Forbidden: 403 Forbidden"): + # logger.warning(f"Bot is missing permissions: '{text_error}', server: '{server}', '!{invoked}'") + emoji = "❌" + await ctx.channel.send(f"Bot is missing permissions: '!{invoked}'", delete_after=30) + + elif text_error.startswith("Command raised an exception: NotImplementedError"): + # logger.warning(f"Not implemented feature: '{text_error}', server: '{server}', '!{invoked}'") + emoji = "❌" + await ctx.channel.send(f"Not implemented feature: '{text}'", delete_after=30) + + elif type(command_error) is CommandError: + # logger.warning(f"Command error: '{text_error}', server: '{server}', '!{invoked}'") + emoji = "❌" + await ctx.channel.send(f"{text_error}: '!{invoked}'") + + elif "required positional argument" in text_error: + emoji = "❌" + await ctx.channel.send(f"Some arguments are missing: '{command_error.original}'", delete_after=30) + + else: + tb_text = traceback.format_exception(type(command_error), command_error, command_error.__traceback__) + tb_text = ''.join([line for line in tb_text if f'bot{os.path.sep}' in line or f'app{os.path.sep}' in line]) + emoji = "❌" + # logger.error( + # f"No reaction for this error type:\n" + # f"Command: '{ctx.message.content}', server: '{server}', \n" + # f"'{command_error}'\n" + # f"Partial traceback:\n{tb_text}") + print( + f"No reaction for this error type:\n" + f"Command: '{ctx.message.content}', server: '{server}', \n" + f"'{command_error}'\n" + f"Partial traceback:\n{tb_text}") + + try: + await ctx.message.add_reaction(emoji=emoji) + except Exception: + pass + + +@bot.command() +@advanced_args_function(bot) +# @log_call_function +@advanced_perm_check_function(is_not_priv) +# @my_help.help_decorator("Poll with maximum 10 answers. Minimum 2 answers, maximum 10. timeout is optional", +# "? , ..., (timeout=)") +async def poll(ctx, *args, text, force=False, dry=False, timeout=3 * 60, **kwargs): + """ + Multi choice poll with maximum 10 answers + Example `!poll Question, answer1, answer2, .... answer10` + Available delimiters: . ; , + You can add more time with `timeout=200`, default 180, maximum is 3600 (1hour) + Use `-d` for dryrun + Args: + ctx: + *args: + dry: + **kwargs: + + Returns: + + """ + + # text = ' '.join(args) + # logger.debug(text) + arr_text = re.split(r"['.?;,]", text) + arr_text = [el.lstrip().rstrip() for el in arr_text if len(el) > 0] + poll_color = Colour.from_rgb(250, 165, 0) + finished_poll_color = Colour.from_rgb(30, 255, 0) + + timeout = float(timeout) + if timeout > 60 * 60 and not force: + timeout = 60 * 60 + + update_interval = 30 if 30 < timeout else timeout + + if len(arr_text) < 3: + await ctx.send(f"Got less than 1 questions and 2 answers, use some delimiter ?;,") + return None + + question, *answers = arr_text + question = question.title() + "?" + + if dry: + text = f"Question: {question}\n" + f"Answers {len(answers)}: {answers}\n" + f"Timeout: {timeout}" + await ctx.send(text) + return None + + elif len(answers) > 10: + confirm_message = await ctx.send( + f"Got more than 10 answers, sorry. If you want poll with 10 answers, press reaction.", + delete_after=60) + await confirm_message.add_reaction(EMOJIS["green_ok"]) + + def wait_for_confirm(reaction, _cur_user): + return str(reaction.emoji) == EMOJIS["green_ok"] \ + and _cur_user.id == ctx.author.id \ + and reaction.message.id == confirm_message.id + + try: + reaction, user = await bot.wait_for("reaction_add", check=wait_for_confirm, timeout=60) + answers = answers[0:10] + await confirm_message.delete() + except asyncio.TimeoutError: + return None + try: + await ctx.message.delete() + except Exception: + pass + + answer_dict = {} + + embed = Embed(title=question, colour=poll_color) + embed.set_author(name=ctx.author.name) + embed.set_thumbnail(url=ctx.author.avatar_url) + embed.set_footer(text=f"Time left: {timeout / 60:4.1f} min") + + for num, ans in enumerate(answers, 1): + emoji = EMOJIS[num] + answer_dict[emoji] = {'ans': ans.title(), 'votes': 0} + embed.add_field(name=emoji, value=ans, inline=False) + + poll = await ctx.send(embed=embed) + + await poll.add_reaction("🛑") + for num in range(1, len(answers) + 1): + await poll.add_reaction(EMOJIS[num]) + await asyncio.sleep(0.01) + + def check_end_reaction(reaction, user): + b1 = reaction.emoji == "🛑" + b2 = reaction.message.id == poll.id + b3 = user == ctx.message.author + return b1 and b2 and b3 + + end_time = time.time() + timeout + while time.time() <= end_time: + end_loop = False + try: + reaction, message = await bot.wait_for('reaction_add', check=check_end_reaction, timeout=update_interval) + end_loop = True + except asyncio.TimeoutError as err: + pass + + poll = await ctx.channel.fetch_message(poll.id) + # all_votes = 0 + all_users = [] + vote_emojis = [EMOJIS[num] for num in range(1, 11)] + for rc in poll.reactions: + if rc.emoji not in vote_emojis: + continue + users = [user async for user in rc.users()] + all_users += users + me = rc.me + count = rc.count - 1 if me else 0 + # all_votes += count + answer_dict[rc.emoji]['votes'] = count + + all_users_count = len(set(all_users)) - 1 + embed = Embed(title=question, colour=poll_color) + embed.set_author(name=ctx.author.name) + embed.set_thumbnail(url=ctx.author.avatar_url) + embed.set_footer(text=f"Time left: {(end_time - time.time()) / 60:4.1f} min") + for number in range(1, 11): + try: + emoji = EMOJIS[number] + ans = answer_dict[emoji]['ans'] + votes = answer_dict[emoji]['votes'] + # fraction = votes / all_votes * 100 if all_votes > 0 else 0 + fraction = votes / all_users_count * 100 if all_users_count > 0 else 0 + embed.add_field(name=f"{emoji} -`{fraction:<3.1f} %`", value=ans, inline=False) + except KeyError: + break + + await poll.edit(embed=embed) + await asyncio.sleep(0.01) + if end_loop: + break + + embed.colour = finished_poll_color + embed.set_footer(text="") + await poll.edit(content='🔒 Poll has ended', embed=embed) + await poll.clear_reaction("🛑") + + +@bot.command(aliases=['nnfs.io', 'nfs', 'nn']) +async def nnfs(ctx, *args, **kwargs): + color = Colour.from_rgb(10, 180, 50) + url = r'https://nnfs.io/' + desc = '"Neural Networks From Scratch" is a book intended to teach you how to build neural networks on your own, without any libraries, so you can better understand deep learning and how all of the elements work. This is so you can go out and do new/novel things with deep learning as well as to become more successful with even more basic models.' + embed = Embed(title=f"Click link", colour=color, url=url, description=None) + + sentdex = bot.get_user(SENTDEX_ID) + embed.set_thumbnail(url=sentdex.avatar_url) + embed.add_field(name="NNFS is out!", value=desc) + # embed.set_author(name="Sentdex") + embed.set_footer(text=f"for {ctx.author.name}", icon_url=ctx.author.avatar_url) + await ctx.send(embed=embed) diff --git a/modules/decorators.py b/modules/decorators.py new file mode 100644 index 0000000..e597771 --- /dev/null +++ b/modules/decorators.py @@ -0,0 +1,237 @@ +import discord +import asyncio +import random +import time +import re + +from .permissions import CommandWithoutPermissions +from .definitions import send_disapprove, send_approve + +from discord.ext.commands import CommandError +from discord import HTTPException, NotFound + + +def approve_fun(coro): + """ + Decorator that adds reaction if success, else x. + Args: + coro: + + Returns: + message object returned by calling given function with given params + """ + + async def decorator(ctx, *args, **kwargs): + try: + result = await coro(ctx, *args, **kwargs) + await send_approve(ctx) + return result + except NotFound: + pass + except Exception as pe: + try: + await send_disapprove(ctx) + except NotFound: + pass + raise pe + + decorator.__name__ = coro.__name__ + decorator.__doc__ = coro.__doc__ + return decorator + + +def _check_advanced_perm(ctx, *args, rule_sets=None, restrictions=None, **kwargs): + if not rule_sets and not restrictions: + raise CommandWithoutPermissions("Not checking any permission") + + if restrictions: + if type(restrictions) is not list and type(restrictions) is not tuple: + restrictions = [restrictions] + + for rule in restrictions: + valid, error = rule(ctx, *args, **kwargs) + if not valid: + raise error + + rule_sets = [rules if (type(rules) is list or type(rules) is set) else [rules] + for rules in rule_sets] + all_errors = [] + + for rules in rule_sets: + valids, errors = zip(*[chk_f(ctx, *args, **kwargs) for chk_f in rules]) + if all(valids): + return True + else: + all_errors += errors + + if len(all_errors) > 0: + raise all_errors[0] + else: + return True + + +def advanced_perm_check_function(*rules_sets, restrictions=None): + """ + Check channels and permissions, use -s -sudo or -a -admin to run it. + Args: + *rules_sets: list of rules, 1d or 2d, + restrictions: Restrictions must be always met + Returns: + message object returned by calling given function with given params + """ + + def decorator(coro): + async def f(ctx, *args, **kwargs): + valid = _check_advanced_perm(ctx, + *args, + **kwargs, + rule_sets=[*rules_sets], restrictions=restrictions) + if valid: + output = await coro(ctx, *args, **kwargs) + return output + else: + # logger.error(f"Permission check failed! Exceptions should be raised earlier!") + raise CommandError("Permission check failed.") + + f.__name__ = coro.__name__ + f.__doc__ = coro.__doc__ + return f + + return decorator + + +def advanced_args_function(bot, bold_name=False): + """ + Decorator that translates args to create flags and converts string into kwargs. + Args: + bot: bot instance + bold_name: + + Returns: + message object returned by calling given function with given params + """ + + def wrapper(coro): + # logger.warning(f"Advanced args are not supporting non kwargs functions") + + async def f(ctx, *args, **kwargs): + # if text: + # logger.error(f"Text is already in advanced args: {text}") + + good_args, kwargs = _get_advanced_args(bot, ctx, *args, bold_name=bold_name, **kwargs) + output = await coro(ctx, *good_args, **kwargs) + return output + + f.__name__ = coro.__name__ + f.__doc__ = coro.__doc__ + return f + + return wrapper + + +def _get_advanced_args(bot, ctx, *args, bold_name=False, **kwargs): + if not kwargs: + kwargs = {} + + good_args = list() + text_args = [] + args, kwargs = _get_advanced_kwargs(bot, ctx, *args, **kwargs, bold_name=bold_name) + args = list(args) + + for arg in args: + if arg: + if ',' in arg: + arg, *rest = arg.split(',') + for _rest in rest: + args.append(_rest) + good_args.append(arg) + text_args.append(arg) + # else: + # logger.debug(f"Unwanted arg: '{arg}'") + + good_args = tuple(_ar for _ar in good_args if _ar) + print(good_args) + return good_args, kwargs + + +def _get_advanced_kwargs(bot, ctx, *args, bold_name=False, **kwargs): + args = list(args) + if not kwargs: + # kwargs = {"force": False, "dry": False, "sudo": False, 'update': False} + kwargs = {} + + good_args = list() + mention_pattern = re.compile(r"<@[!&]\d+>") + text_args = [] + + for arg in args: + # if arg.startswith("-f") or arg == 'force': + # "force, enforce parameters" + # kwargs['force'] = True + # elif arg.startswith("-d") or arg == 'dry': + # "dry run" + # kwargs['dry'] = True + # elif arg.startswith("-u") or arg == 'update' or arg == "upgrade": + # "update" + # kwargs['update'] = True + # elif arg.startswith("-s") or arg.startswith("-a") or arg == 'sudo': + # "sudo or admin" + # kwargs['sudo'] = True + # elif arg.startswith("-"): + # try: + # _ = float(arg) + # good_args.append(arg) + # except ValueError: + # "drop unknown parameters" + # # logger.warning(f"unknown argument: {arg}") + if "=" in arg: + key, val = arg.split("=") + if key == "force" or key == "dry": + continue + if key and val: + kwargs.update({key: val}) + elif mention_pattern.match(arg) or "@everyone" in arg or "@here" in arg: + name = string_mention_converter(bot, ctx.guild, arg, bold_name=bold_name) + text_args.append(name) + + else: + good_args.append(arg) + text_args.append(arg) + + good_args = tuple(good_args) + text = ' '.join(text_args) + kwargs['text'] = text + + return good_args, kwargs + + +def string_mention_converter(bot, guild, text: "input str", bold_name=True) -> "String": + user_pattern = re.compile(r"<@!(\d+)>") + role_pattern = re.compile(r"<@&(\d+)>") + new_text = text + + new_text = new_text.replace("@everyone", "") + new_text = new_text.replace("@here", "") + + user_list = user_pattern.findall(text) + role_list = role_pattern.findall(text) + + for user in user_list: + try: + user_name = bot.get_user(int(user)).name + except AttributeError: + user_name = f"{user}" + if bold_name: + new_text = new_text.replace(f"<@!{user}>", f"@**{user_name}**") + else: + new_text = new_text.replace(f"<@!{user}>", f"@{user_name}") + + for role in role_list: + try: + roleid = int(role) + role_name = guild.get_role(roleid).name + except AttributeError as err: + # logger.error(f"Error in string_mention_converter {err}") + role_name = f"{role}" + new_text = new_text.replace(f"<@&{role}>", f"@*{role_name}*") + return new_text diff --git a/modules/definitions.py b/modules/definitions.py new file mode 100644 index 0000000..150b35d --- /dev/null +++ b/modules/definitions.py @@ -0,0 +1,98 @@ +import discord +import os + +from discord.ext.commands import Bot + +local_files_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'sentdebot')) + +SENTEX_GUILD_ID = 405403391410438165 +SENTDEX_ID = 324953561416859658 +DHAN_ID = 150409781595602946 +MAIN_CH_ID = 408713676095488000 +DOGS_CH_ID = 671016601440747520 +HELP_CH_ID_0 = 412620789133606914 +HELP_CH_ID_1 = 507281643606638622 +HELP_CH_ID_2 = 682674227664388146 +VOICE2TEXT_CH_ID = 484406428233367562 +HELLO_CHARLES_CH_ID = 407958260168130570 + +IMAGE_CHAN_IDS = [MAIN_CH_ID, + HELP_CH_ID_0, + HELP_CH_ID_1, + HELP_CH_ID_2, + 476412789184004096, # ? + 499945870473691146, # ? + VOICE2TEXT_CH_ID, + ] + +CHATBOTS = [405511726050574336, # Charles + 428904098985803776, # Irene + 414630095911780353, # Charlene (Charles' alter ego) + 500507500962119681] # ? + +SENTDEBOT_ID = 499921685051342848 + +admin_id = 405506750654054401 +mod_id = 405520180974714891 + +admins_mods_ids = [admin_id, mod_id] +# condition to restrict a command to admins/mods: len([r for r in author_roles if r.id in admins_mods_ids]) > 0 + +VANITY_ROLE_IDS = [479433667576332289, + 501115401577431050, + 501115079572324352, + 501114732057460748, + 501115820273958912, + 501114928854204431, + 479433212561719296] + +MOST_COMMON_INT = 10 # when doing counters of activity, top n users. +DAYS_BACK = 21 # days of history to work with +RESAMPLE = "60min" + +# names of channels where we want to count activity. +COMMUNITY_BASED_CHANNELS = ["__main__", + "main", + "help", + "help_0", + "help_1", + "help_2", + "help_3", + "voice-channel-text", + "__init__", + "hello_technical_questions", + "help_overflow", + "dogs", + "show_and_tell", + "politics_enter_at_own_risk"] + +HELP_CHANNELS = ["help", + "hello_technical_questions", + "help_0", + "help_1", + "help_2", + "help_3", + "help_overflow"] + +DISCORD_BG_COLOR = '#36393E' + +HELLO_TEXTS = ["Hello there {emote} {0}.", + "How is it going today {0}? {emote}", + "What's up {0}?", "Hey {0}. {emote}", + "Hi {0}, do you feel well today? {emote}", + "Good day {0}. {emote}", + "Oh {0}! Hello. {emote}", + "Hey {emote}. {0}", + "Hey, do you want some coffee {0}? {emote}", + ] + +intents = discord.Intents.all() +bot = Bot(command_prefix="!", ignore_case=True, intents=intents) + + +async def send_approve(ctx): + await ctx.message.add_reaction('✅') + + +async def send_disapprove(ctx): + await ctx.message.add_reaction('❌') diff --git a/modules/emojis.py b/modules/emojis.py new file mode 100644 index 0000000..c57722a --- /dev/null +++ b/modules/emojis.py @@ -0,0 +1,674 @@ +EMOJIS_MY_SYMBOLS = { + 'green_x': "❎", + 'green_ok': "✅", + 'arrow_back_left': "↩️", + "thinking": "🤔", + "red_x": "❌", + "blocked": "⛔", +} + +EMOJIS_ALPHABET = { + 0: '0️⃣', + 1: '1️⃣', + 2: '2️⃣', + 3: '3️⃣', + 4: '4️⃣', + 5: '5️⃣', + 6: '6️⃣', + 7: '7️⃣', + 8: '8️⃣', + 9: '9️⃣', + 10: '🔟', + "z": "🇿", + "y": "🇾", + "x": "🇽", + "w": "🇼", + "v": "🇻", + "u": "🇺", + "t": "🇹", + "s": "🇸", + "r": "🇷", + "q": "🇶", + "p": "🇵", + "o": "🇴", + "n": "🇳", + "m": "🇲", + "l": "🇱", + "k": "🇰", + "j": "🇯", + "i": "🇮", + "h": "🇭", + "g": "🇬", + "f": "🇫", + "e": "🇪", + "d": "🇩", + "c": "🇨", + "b": "🇧", + "a": "🇦", +} + +EMOJIS_SYMBOLS = { + "heart": '❤️', + "orange_heart": "🧡", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "purple_heart": "💜", + "brown_heart": "🤎", + "white_heart": "🤍", + "black_heart": "🖤", + "broken_heart": "💔", + "heart_exclamation": "❣️", + "two_hearts": "💕", + "revolving_hearts": "💞", + "heartbeat": "💓", + "heartpulse": "💗", + "sparkling_heart": "💖", + "cupid": "💘", + "gift_heart": "💝", + "heart_decoration": "💟", + "peace": "☮️", + "cross": "✝️", + "star_and_crescent": "☪️", + "om_symbol": "🕉️", + "wheel_of_dharma": "☸️", + "star_of_david": "✡️", + "six_pointed_star": "🔯", + "menorah": "🕎", + "yin_yang": "☯️", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "id": "🆔", + "atom": "⚛️", + "accept": "🉑", + "radioactive": "☢️", + "biohazard": "☣️", + "mobile_phone_off": "📴", + "vibration_mode": "📳", + "u6709": "🈶", + "u7121": "🈚", + "u7533": "🈸", + "u55b6": "🈺", + "u6708": "🈷️", + "vs": "🆚", + "white_flower": "💮", + "ideograph_advantage": "🉐", + "secret": "㊙️", + "congratulations": "㊗️", + "u5408": "🈴", + "u6e80": "🈵", + "u5272": "🈹", + "u7981": "🈲", + "a": "🅰️", + "b": "🅱️", + "ab": "🆎", + "cl": "🆑", + "o2": "🅾️", + "sos": "🆘", + "x": "❌", + "o": "⭕", + "octagonal_sign": "🛑", + "no_entry": "⛔", + "name_badge": "📛", + "no_entry_sign": "🚫", + "100": "💯", + "anger": "💢", + "hotsprings": "♨️", + "no_pedestrians": "🚷", + "do_not_litter": "🚯", + "no_bicycles": "🚳", + "non_potable_water": "🚱", + "underage": "🔞", + "no_mobile_phones": "📵", + "no_smoking": "🚭", + "exclamation": "❗", + "grey_exclamation": "❕", + "question": "❓", + "grey_question": "❔", + "bangbang": "‼️", + "interrobang": "⁉️", + "low_brightness": "🔅", + "high_brightness": "🔆", + "part_alternation_mark": "〽️", + "warning": "⚠️", + "children_crossing": "🚸", + "trident": "🔱", + "fleur_de_lis": "⚜️", + "beginner": "🔰", + "recycle": "♻️", + "white_check_mark": "✅", + "u6307": "🈯", + "chart": "💹", + "sparkle": "❇️", + "eight_spoked_asterisk": "✳️", + "negative_squared_cross_mark": "❎", + "globe_with_meridians": "🌐", + "diamond_shape_with_a_dot_inside": "💠", + "m": "Ⓜ️", + "cyclone": "🌀", + "zzz": "💤", + "atm": "🏧", + "wc": "🚾", + "wheelchair": "♿", + "parking": "🅿️", + "u7a7a": "🈳", + "sa": "🈂️", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "mens": "🚹", + "womens": "🚺", + "baby_symbol": "🚼", + "restroom": "🚻", + "put_litter_in_its_place": "🚮", + "cinema": "🎦", + "signal_strength": "📶", + "koko": "🈁", + "symbols": "🔣", + "information_source": "ℹ️", + "abc": "🔤", + "abcd": "🔡", + "capital_abcd": "🔠", + "ng": "🆖", + "ok": "🆗", + "up": "🆙", + "cool": "🆒", + "new": "🆕", + "free": "🆓", + "zero": "0️⃣", + "one": "1️⃣️⃣", + "two": "2️⃣", + "three": "3️⃣'", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "1234": "🔢", + "hash": "#️⃣", + "asterisk": "*️⃣", + "eject": "⏏️", + "arrow_forward": "▶️", + "pause_button": "⏸️", + "play_pause": "⏯️", + "stop_button": "⏹️", + "record_button": "⏺️", + "track_next": "⏭️", + "track_previous": "⏮️", + "fast_forward": "⏩", + "rewind": "⏪", + "arrow_double_up": "⏫", + "arrow_double_down": "⏬", + "arrow_backward": "◀️", + "arrow_up_small": "🔼", + "arrow_down_small": "🔽", + "arrow_right": "➡️", + "arrow_left": "⬅️", + "arrow_up": "⬆️", + "arrow_down": "⬇️", + "arrow_upper_right": "↗️", + "arrow_lower_right": "↘️", + "arrow_lower_left": "↙️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "arrow_right_hook": "↪️", + "leftwards_arrow_with_hook": "↩️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrows_counterclockwise": "🔄", + "arrows_clockwise": "🔃", + "musical_note": "🎵", + "notes": "🎶", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_multiplication_x": "✖️", + "infinity": "♾️", + "heavy_dollar_sign": "💲", + "currency_exchange": "💱", + "tm": "™️", + "copyright": "©", + "registered": "®️", + "wavy_dash": "〰️", + "curly_loop": "➰", + "loop": "➿", + "end": "🔚", + "back": "🔙", + "on": "🔛", + "top": "🔝", + "soon": "🔜", + "heavy_check_mark": "✔️", + "ballot_box_with_check": "☑️", + "radio_button": "🔘", + "white_circle": "⚪", + "black_circle": "⚫", + "red_circle": "🔴", + "blue_circle": "🔵", + "brown_circle": "🟤", + "purple_circle": "🟣", + "green_circle": "🟢", + "yellow_circle": "🟡", + "orange_circle": "🟠", + "small_red_triangle": "🔺", + "small_red_triangle_down": "🔻", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "white_square_button": "🔳", + "black_square_button": "🔲", + "black_small_square": "▪️", + "white_small_square": "▫️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_large_square": "⬛", + "white_large_square": "⬜", + "orange_square": "🟧", + "blue_square": "🟦", + "red_square": "🟥", + "brown_square": "🟫", + "purple_square": "🟪", + "green_square": "🟩", + "yellow_square": "🟨", + "speaker": "🔈", + "mute": "🔇", + "sound": "🔉", + "loud_sound": "🔊", + "bell": "🔔", + "no_bell": "🔕", + "mega": "📣", + "loudspeaker": "📢", + "speech_left": "🗨️", + "eye_in_speech_bubble": "👁‍🗨", + "speech_balloon": "💬", + "thought_balloon": "💭", + "anger_right": "🗯️", + "spades": "♠️", + "clubs": "♣️", + "hearts": "♥️", + "diamonds": "♦️", + "black_joker": "🃏", + "flower_playing_cards": "🎴", + "mahjong": "🀄", + "clock1": "🕐", + "clock2": "🕑", + "clock3": "🕒", + "clock4": "🕓", + "clock5": "🕔", + "clock6": "🕕", + "clock7": "🕖", + "clock8": "🕗", + "clock9": "🕘", + "clock10": "🕙", + "clock11": "🕚", + "clock12": "🕛", + "clock130": "🕜", + "clock230": "🕝", + "clock330": "🕞", + "clock430": "🕟", + "clock530": "🕠", + "clock630": "🕡", + "clock730": "🕢", + "clock830": "🕣", + "clock930": "🕤", + "clock1030": "🕥", + "clock1130": "🕦", + "clock1230": "🕧", + "female_sign": "♀️", + "male_sign": "♂️", + "medical_symbol": "⚕️", + "regional_indicator_z": "🇿", + "regional_indicator_y": "🇾", + "regional_indicator_x": "🇽", + "regional_indicator_w": "🇼", + "regional_indicator_v": "🇻", + "regional_indicator_u": "🇺", + "regional_indicator_t": "🇹", + "regional_indicator_s": "🇸", + "regional_indicator_r": "🇷", + "regional_indicator_q": "🇶", + "regional_indicator_p": "🇵", + "regional_indicator_o": "🇴", + "regional_indicator_n": "🇳", + "regional_indicator_m": "🇲", + "regional_indicator_l": "🇱", + "regional_indicator_k": "🇰", + "regional_indicator_j": "🇯", + "regional_indicator_i": "🇮", + "regional_indicator_h": "🇭", + "regional_indicator_g": "🇬", + "regional_indicator_f": "🇫", + "regional_indicator_e": "🇪", + "regional_indicator_d": "🇩", + "regional_indicator_c": "🇨", + "regional_indicator_b": "🇧", + "regional_indicator_a": "🇦", +} + +EMOJI_FOOD = { + "green_apple": "🍏", + "apple": "🍎", + "pear": "🍐", + "tangerine": "🍊", + "lemon": "🍋", + "banana": "🍌", + "watermelon": "🍉", + "grapes": "🍇", + "blueberries": '🫐', + "strawberry": "🍓", + "melon": "🍈", + "cherries": "🍒", + "peach": "🍑", + "mango": "🥭", + "pineapple": "🍍", + "coconut": "🥥", + "kiwi": "🥝", + "tomato": "🍅", + "eggplant": "🍆", + "avocado": "🥑", + "broccoli": "🥦", + "leafy_green": "🥬", + "cucumber": "🥒", + "hot_pepper": "🌶️", + "corn": "🌽", + "carrot": "🥕", + "onion": "🧅", + "garlic": "🧄", + "potato": "🥔", + "sweet_potato": "🍠", + "croissant": "🥐", + "bagel": "🥯", + "bread": "🍞", + "french_bread": "🥖", + "pretzel": "🥨", + "cheese": "🧀", + "egg": "🥚", + "cooking": "🍳", + "pancakes": "🥞", + "waffle": "🧇", + "bacon": "🥓", + "cut_of_meat": "🥩", + "poultry_leg": "🍗", + "meat_on_bone": "🍖", + "hotdog": "🌭", + "hamburger": "🍔", + "fries": "🍟", + "pizza": "🍕", + "sandwich": "🥪", + "falafel": "🧆", + "stuffed_flatbread": "🥙", + "taco": "🌮", + "burrito": "🌯", + "salad": "🥗", + "shallow_pan_of_food": "🥘", + "canned_food": "🥫", + "spaghetti": "🍝", + "ramen": "🍜", + "stew": "🍲", + "curry": "🍛", + "sushi": "🍣", + "bento": "🍱", + "dumpling": "🥟", + "fried_shrimp": "🍤", + "rice_ball": "🍙", + "rice": "🍚", + "rice_cracker": "🍘", + "fish_cake": "🍥", + "fortune_cookie": "🥠", + "moon_cake": "🥮", + "oden": "🍢", + "dango": "🍡", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "icecream": "🍦", + "pie": "🥧", + "cupcake": "🧁", + "cake": "🍰", + "birthday": "🎂", + "custard": "🍮", + "lollipop": "🍭", + "candy": "🍬", + "chocolate_bar": "🍫", + "popcorn": "🍿", + "doughnut": "🍩", + "cookie": "🍪", + "chestnut": "🌰", + "peanuts": "🥜", + "honey_pot": "🍯", + "butter": "🧈", + "milk": "🥛", + "baby_bottle": "🍼", + "coffee": "☕", + "tea": "🍵", + "mate": "🧉", + "cup_with_straw": "🥤", + "beverage_box": "🧃", + "ice_cube": "🧊", + "sake": "🍶", + "beer": "🍺", + "beers": "🍻", + "champagne_glass": "🥂", + "wine_glass": "🍷", + "tumbler_glass": "🥃", + "cocktail": "🍸", + "tropical_drink": "🍹", + "champagne": "🍾", + "spoon": "🥄", + "fork_and_knife": "🍴", + "fork_knife_plate": "🍽️", + "bowl_with_spoon": "🥣", + "takeout_box": "🥡", + "chopsticks": "🥢", + "salt": "🧂", +} + +EMOJI_NATURE = { + "dog": "🐶", + "cat": "🐱", + "mouse": "🐭", + "hamster": "🐹", + "rabbit": "🐰", + "fox": "🦊", + "bear": "🐻", + "panda_face": "🐼", + "koala": "🐨", + "tiger": "🐯", + "lion_face": "🦁", + "cow": "🐮", + "pig": "🐷", + "pig_nose": "🐽", + "frog": "🐸", + "monkey_face": "🐵", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "monkey": "🐒", + "chicken": "🐔", + "penguin": "🐧", + "bird": "🐦", + "baby_chick": "🐤", + "hatching_chick": "🐣", + "hatched_chick": "🐥", + "duck": "🦆", + "eagle": "🦅", + "owl": "🦉", + "bat": "🦇", + "wolf": "🐺", + "boar": "🐗", + "horse": "🐴", + "unicorn": "🦄", + "bee": "🐝", + "bug": "🐛", + "butterfly": "🦋", + "snail": "🐌", + "shell": "🐚", + "lady_beetle": "🐞", + "ant": "🐜", + "mosquito": "🦟", + "cricket": "🦗", + "spider": "🕷️", + "spider_web": "🕸️", + "scorpion": "🦂", + "turtle": "🐢", + "snake": "🐍", + "lizard": "🦎", + "t_rex": "🦖", + "sauropod": "🦕", + "octopus": "🐙", + "squid": "🦑", + "shrimp": "🦐", + "lobster": "🦞", + "oyster": "🦪", + "crab": "🦀", + "blowfish": "🐡", + "tropical_fish": "🐠", + "fish": "🐟", + "dolphin": "🐬", + "whale": "🐳", + "whale2": "🐋", + "shark": "🦈", + "crocodile": "🐊", + "tiger2": "🐅", + "leopard": "🐆", + "zebra": "🦓", + "gorilla": "🦍", + "orangutan": "🦧", + "elephant": "🐘", + "hippopotamus": "🦛", + "rhino": "🦏", + "dromedary_camel": "🐪", + "camel": "🐫", + "giraffe": "🦒", + "kangaroo": "🦘", + "water_buffalo": "🐃", + "ox": "🐂", + "cow2": "🐄", + "racehorse": "🐎", + "pig2": "🐖", + "ram": "🐏", + "llama": "🦙", + "sheep": "🐑", + "goat": "🐐", + "deer": "🦌", + "dog2": "🐕", + "guide_dog": "🦮", + "service_dog": "🐕‍🦺", + "poodle": "🐩", + "cat2": "🐈", + "rooster": "🐓", + "turkey": "🦃", + "peacock": "🦚", + "parrot": "🦜", + "swan": "🦢", + "flamingo": "🦩", + "dove": "🕊️", + "rabbit2": "🐇", + "sloth": "🦥", + "otter": "🦦", + "skunk": "🦨", + "raccoon": "🦝", + "badger": "🦡", + "mouse2": "🐁", + "rat": "🐀", + "chipmunk": "🐿️", + "hedgehog": "🦔", + "dragon": "🐉", + "dragon_face": "🐲", + "feet": "🐾", + "cactus": "🌵", + "christmas_tree": "🎄", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "seedling": "🌱", + "herb": "🌿", + "shamrock": "☘️", + "four_leaf_clover": "🍀", + "bamboo": "🎍", + "tanabata_tree": "🎋", + "leaves": "🍃", + "fallen_leaf": "🍂", + "maple_leaf": "🍁", + "mushroom": "🍄", + "ear_of_rice": "🌾", + "bouquet": "💐", + "tulip": "🌷", + "rose": "🌹", + "wilted_rose": "🥀", + "hibiscus": "🌺", + "cherry_blossom": "🌸", + "blossom": "🌼", + "sunflower": "🌻", + "sun_with_face": "🌞", + "full_moon_with_face": "🌝", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "new_moon_with_face": "🌚", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "waxing_gibbous_moon": "🌔", + "crescent_moon": "🌙", + "earth_americas": "🌎", + "earth_africa": "🌍", + "earth_asia": "🌏", + "ringed_planet": "🪐", + "dizzy": "💫", + "star": "⭐", + "star2": "🌟", + "sparkles": "✨", + "zap": "⚡", + "comet": "☄️", + "boom": "💥", + "fire": "🔥", + "cloud_tornado": "🌪️", + "rainbow": "🌈", + "sunny": "☀️", + "white_sun_small_cloud": "🌤️", + "partly_sunny": "⛅", + "white_sun_cloud": "🌥️", + "cloud": "☁️", + "white_sun_rain_cloud": "🌦️", + "cloud_rain": "🌧️", + "thunder_cloud_rain": "⛈️", + "cloud_lightning": "🌩️", + "cloud_snow": "🌨️", + "snowflake": "❄️", + "snowman2": "☃️", + "snowman": "⛄", + "wind_blowing_face": "🌬️", + "dash": "💨", + "droplet": "💧", + "sweat_drops": "💦", + "umbrella": "☔", + "umbrella2": "☂️", + "ocean": "🌊", + "fog": "🌫️", +} + +EMOJIS = {**EMOJIS_SYMBOLS, + **EMOJI_FOOD, **EMOJI_NATURE, + **EMOJIS_ALPHABET, + **EMOJIS_MY_SYMBOLS, } + +HAPPY_FACES = ["😀", "🙂", "😌", "😍", "🥰", "😜", "😊", "😎", "🤠", "🤗", "🤩", "🥳", "😉", "🤪", "😋", "😛"] +DISRTUBED_FACES = ["😒", "😞", "😔", "🧐", "😕", "😫", "😩", "🥺", "😤", "😠", "😳", "🤔", "🤫", "😟"] diff --git a/modules/minigames.py b/modules/minigames.py new file mode 100644 index 0000000..3aab147 --- /dev/null +++ b/modules/minigames.py @@ -0,0 +1,72 @@ +import numpy as np +import asyncio +import random + +from .permissions import is_called_in_bot_channel, is_priv +from .definitions import bot +from .decorators import advanced_perm_check_function, advanced_args_function + + +@bot.command(aliases=['sweeper', 'minesweeper']) +# @advanced_args_function(bot) +@advanced_perm_check_function(is_called_in_bot_channel, is_priv) +async def mines(ctx, *args, **kwargs): + """ + Generates sweeper game, !sweeper (size) (bombs) + Args: + ctx: + *args: + + Returns: + + """ + if len(args) == 0: + size = 7 + bombs = None + elif len(args) == 1: + size = args[0] + size = int(size) + bombs = None + elif len(args) == 2: + size, bombs = args + size = int(size) + bombs = int(bombs) + else: + size = 7 + bombs = None + + "Check table size, 2k characters limit" + if size < 1: + size = 2 + elif size > 14: + size = 14 + + "Check bomb amount" + fields = size * size + if bombs is None or bombs < 0: + bombs = size * size // 3.5 + elif bombs >= fields: + bombs = fields - 1 + + bomb_list = [1 if fi < bombs else 0 for fi in range(fields)] + random.shuffle(bomb_list) + temp_arr = np.array(bomb_list).reshape(size, size) + bomb_arr = np.zeros((size + 2, size + 2), dtype=int) + for rind, row in enumerate(temp_arr): + rind += 1 + for cin, num in enumerate(row): + cin += 1 + if num: + bomb_arr[rind - 1:rind + 2, cin - 1:cin + 2] += 1 + await asyncio.sleep(0.01) + + bomb_arr = bomb_arr[1:-1, 1:-1] + mask = temp_arr == 1 + bomb_arr[mask] = -1 + hidden_text = '\n'.join( + "".join(f"||`{num:^2}`||" if num >= 0 else f"||`{'X':^2}`||" for num in row) + for row in bomb_arr) + text = f"Sweeper {size}x{size}, bombs: {bombs:.0f} ({bombs / fields * 100:.0f}%)" + sweeper_text = f"{text}\n{hidden_text}" + message = await ctx.send(sweeper_text) + return message \ No newline at end of file diff --git a/modules/permissions.py b/modules/permissions.py new file mode 100644 index 0000000..69f6273 --- /dev/null +++ b/modules/permissions.py @@ -0,0 +1,37 @@ +from .definitions import SENTDEX_ID, DHAN_ID, HELLO_CHARLES_CH_ID + + +class RestrictedError(PermissionError): + pass + + +class CommandWithoutPermissions(PermissionError): + pass + + +def is_admin(ctx, *args, **kwargs): + if ctx.message.author.id in [SENTDEX_ID, DHAN_ID]: + return True, None + else: + return False, RestrictedError("This command is restricted to Youshisu.") + + +def is_priv(ctx, *args, **kwargs): + if ctx.guild: + return False, RestrictedError("This command is restricted to private channels.") + else: + return True, None + + +def is_not_priv(ctx, *args, **kwargs): + if ctx.guild: + return True, None + else: + return False, RestrictedError("This command is restricted to server channels.") + + +def is_called_in_bot_channel(ctx, *args, **kwargs): + if ctx.channel.id == HELLO_CHARLES_CH_ID: + return True, None + else: + return False, RestrictedError("This command is restricted to channel 'hello-charles'.") diff --git a/sentdebot.py b/sentdebot.py index 1f4eb0a..0de579c 100644 --- a/sentdebot.py +++ b/sentdebot.py @@ -1,396 +1,11 @@ -import matplotlib.pyplot as plt -import matplotlib.ticker as mticker -import matplotlib.dates as mdates -import asyncio -import discord -import pandas as pd -import random -import numpy as np -import time import os -import re -# py -3.6 -m pip install --upgrade requests-html -from requests_html import HTMLSession -from collections import Counter -from matplotlib import style +from modules.background_tasks import start_all_background_tasks +from modules.bot import bot -style.use("dark_background") +if __name__ == "__main__": + token_path = os.path.abspath(os.path.join('sentdebot', 'token.txt')) + token = open(f"{token_path}", "r").read().split('\n')[0] - -path = '/sentdebot/' - -# days of history to work with -DAYS_BACK = 21 -RESAMPLE = "60min" -# when doing counters of activity, top n users. -MOST_COMMON_INT = 10 -# names of channels where we want to count activity. -COMMUNITY_BASED_CHANNELS = ["__main__", - "main", - "help", - "help_0", - "help_1", - "help_2", - "help_3", - "voice-channel-text", - "__init__", - "hello_technical_questions", - "help_overflow", - "dogs", - "show_and_tell","politics_enter_at_own_risk"] - -HELP_CHANNELS = ["help", - "hello_technical_questions", - "help_0", - "help_1", - "help_2", - "help_3", - "help_overflow"] - -DISCORD_BG_COLOR = '#36393E' - -intents = discord.Intents.all() - -client = discord.Client(intents=intents) - -token = open(f"{path}/token.txt", "r").read().split('\n')[0] - -commands_available = """```py -def commands(): - return { - sentdebot.member_count(): 'get member count', - sentdebot.community_report(): 'get some stats on community', - sentdebot.search("QUERY"): 'search for a string', - sentdebot.commands(): 'get commands', - sentdebot.logout(): 'Sentdex-only command to log Sentdebot off' - sentdebot.user_activity(): 'See some stats on top users', - } -```""" - -image_chan_ids = [408713676095488000, - 412620789133606914, - 476412789184004096, - 499945870473691146, - 484406428233367562, - ] - - -chatbots = [405511726050574336, - 428904098985803776, - 414630095911780353, - 500507500962119681] - - -admin_id = 405506750654054401 -mod_id = 405520180974714891 - -admins_mods_ids = [admin_id, mod_id] -# condition to restrict a command to admins/mods: len([r for r in author_roles if r.id in admins_mods_ids]) > 0 - -vanity_role_ids = [479433667576332289, - 501115401577431050, - 501115079572324352, - 501114732057460748, - 501115820273958912, - 501114928854204431, - 479433212561719296] - -channel_ids = [408713676095488000, # main - 412620789133606914] # help - - -def search_term(message): - try: - match = re.fullmatch('sentdebot.search\((["|\'])([^\)]+)(["|\'])\)', message) - if match.group(1) != match.group(3): - return False - for check in re.finditer(match.group(1), match.group(2)): - if match.group(2)[check.start()-1] != '\\': - return False - return match.group(2) - except: - return False - - -def community_report(guild): - online = 0 - idle = 0 - offline = 0 - - for m in guild.members: - if str(m.status) == "online": - online += 1 - elif str(m.status) == "offline": - offline += 1 - else: - idle += 1 - - return online, idle, offline - - -def df_match(c1, c2): - if c1 == c2: - return np.nan - else: - return c1 - - -async def user_metrics_background_task(): - await client.wait_until_ready() - global sentdex_guild - sentdex_guild = client.get_guild(405403391410438165) - - while not client.is_closed(): - try: - online, idle, offline = community_report(sentdex_guild) - with open(f"{path}/usermetrics.csv","a") as f: - f.write(f"{int(time.time())},{online},{idle},{offline}\n") - - df_msgs = pd.read_csv(f'{path}/msgs.csv', names=['time', 'uid', 'channel']) - df_msgs = df_msgs[(df_msgs['time'] > time.time()-(86400*DAYS_BACK))] - df_msgs['count'] = 1 - df_msgs['date'] = pd.to_datetime(df_msgs['time'], unit='s') - df_msgs.drop("time", 1, inplace=True) - df_msgs.set_index("date", inplace=True) - - - df_no_dup = df_msgs.copy() - df_no_dup['uid2'] = df_no_dup['uid'].shift(-1) - df_no_dup['uid_rm_dups'] = list(map(df_match, df_no_dup['uid'], df_no_dup['uid2'])) - - df_no_dup.dropna(inplace=True) - - - message_volume = df_msgs["count"].resample(RESAMPLE).sum() - - user_id_counts_overall = Counter(df_no_dup[df_no_dup['channel'].isin(COMMUNITY_BASED_CHANNELS)]['uid'].values).most_common(MOST_COMMON_INT) - #print(user_id_counts_overall) - - uids_in_help = Counter(df_no_dup[df_no_dup['channel'].isin(HELP_CHANNELS)]['uid'].values).most_common(MOST_COMMON_INT) - #print(uids_in_help) - - df = pd.read_csv(f"{path}/usermetrics.csv", names=['time', 'online', 'idle', 'offline']) - df = df[(df['time'] > time.time()-(86400*DAYS_BACK))] - df['date'] = pd.to_datetime(df['time'],unit='s') - df['total'] = df['online'] + df['offline'] + df['idle'] - df.drop("time", 1, inplace=True) - df.set_index("date", inplace=True) - - df = df.resample(RESAMPLE).mean() - df = df.join(message_volume) - - df.dropna(inplace=True) - #print(df.head()) - - fig = plt.figure(facecolor=DISCORD_BG_COLOR) - ax1 = plt.subplot2grid((2, 1), (0, 0)) - plt.ylabel("Active Users") - plt.title("Community Report") - ax1.set_facecolor(DISCORD_BG_COLOR) - ax1v = ax1.twinx() - plt.ylabel("Message Volume") - #ax1v.set_facecolor(DISCORD_BG_COLOR) - ax2 = plt.subplot2grid((2, 1), (1, 0)) - plt.ylabel("Total Users") - ax2.set_facecolor(DISCORD_BG_COLOR) - - ax1.plot(df.index, df.online, label="Active Users\n(Not Idle)") - #ax1v.bar(df.index, df["count"], width=0.01) - - ax1v.fill_between(df.index, 0, df["count"], facecolor="w", alpha=0.2, label="Message Volume") - ax1.legend(loc=2) - ax1v.legend(loc=9) - - ax2.plot(df.index, df.total, label="Total Users") - ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d\n%H:%M')) - - #for label in ax2.xaxis.get_ticklabels(): - # label.set_rotation(45) - ax2.xaxis.set_major_locator(mticker.MaxNLocator(nbins=5, prune='lower')) - ax2.legend() - - plt.subplots_adjust(left=0.11, bottom=0.10, right=0.89, top=0.95, wspace=0.2, hspace=0) - ax1.get_xaxis().set_visible(False) - - ax1v.set_ylim(0, 3*df["count"].values.max()) - - #plt.show() - plt.savefig(f"{path}/online.png", facecolor = fig.get_facecolor()) - plt.clf() - - - fig = plt.figure(facecolor=DISCORD_BG_COLOR) - ax1 = plt.subplot2grid((2, 1), (0, 0)) - - plt.xlabel("Message Volume") - plt.title(f"General User Activity (past {DAYS_BACK} days)") - ax1.set_facecolor(DISCORD_BG_COLOR) - - users = [] - msgs = [] - for pair in user_id_counts_overall[::-1]: - try: - users.append(sentdex_guild.get_member(pair[0]).name) # get member name from here - if "Dhanos" in sentdex_guild.get_member(pair[0]).name: - msgs.append(pair[1]/1.0) - else: - msgs.append(pair[1]) - except Exception as e: - print(str(e)) - y_pos = np.arange(len(users)) - ax1.barh(y_pos, msgs, align='center', alpha=0.5) - plt.yticks(y_pos, users) - - ax2 = plt.subplot2grid((2, 1), (1, 0)) - plt.title(f"Help Channel Activity (past {DAYS_BACK} days)") - plt.xlabel("Help Channel\nMsg Volume") - ax2.set_facecolor(DISCORD_BG_COLOR) - - users = [] - msgs = [] - for pair in uids_in_help[::-1]: - try: - users.append(sentdex_guild.get_member(pair[0]).name) # get member name from here - - if "Dhanos" in sentdex_guild.get_member(pair[0]).name: - msgs.append(pair[1]/1.0) - else: - msgs.append(pair[1]) - #users.append(pair[0]) - except Exception as e: - print(str(e)) - - y_pos = np.arange(len(users)) - ax2.barh(y_pos, msgs, align='center', alpha=0.5) - plt.yticks(y_pos, users) - - plt.subplots_adjust(left=0.30, bottom=0.15, right=0.99, top=0.95, wspace=0.2, hspace=0.55) - plt.savefig(f"{path}/activity.png", facecolor=fig.get_facecolor()) - plt.clf() - - - await asyncio.sleep(300) - - except Exception as e: - print(str(e)) - await asyncio.sleep(300) - - -@client.event # event decorator/wrapper -async def on_ready(): - print(f"We have logged in as {client.user}") - await client.change_presence(status = discord.Status.online, activity = discord.Game('help(sentdebot)')) - - -@client.event -async def on_message(message): - print(f"{message.channel}: {message.author}: {message.author.name}: {message.content}") - sentdex_guild = client.get_guild(405403391410438165) - author_roles = message.author.roles - #print(author_roles) - #author_role_ids = [r.id for r in author_roles] - - - - if random.choice(range(500)) == 30: - matches = [r for r in author_roles if r.id in vanity_role_ids] - #print(matches) - - if len(matches) == 0: - try: - role_id_choice = random.choice(vanity_role_ids) - actual_role_choice = sentdex_guild.get_role(role_id_choice) - #print(type(message.author)) - author_roles.append(actual_role_choice) - await message.author.edit(roles=author_roles) - except Exception as e: - print('EDITING ROLES ISSUE:',str(e)) - - - with open(f"{path}/msgs.csv","a") as f: - if message.author.id not in chatbots: - f.write(f"{int(time.time())},{message.author.id},{message.channel}\n") - - with open(f"{path}/log.csv","a") as f: - if message.author.id not in chatbots: - try: - f.write(f"{int(time.time())},{message.author.id},{message.channel},{message.content}\n") - except Exception as e: - f.write(f"{str(e)}\n") - - - if "sentdebot.member_count()" == message.content.lower(): - await message.channel.send(f"```py\n{sentdex_guild.member_count}```") - - - - elif "sentdebot.community_report()" == message.content.lower() and message.channel.id in image_chan_ids: - online, idle, offline = community_report(sentdex_guild) - - file = discord.File(f"{path}/online.png", filename=f"{path}/online.png") - await message.channel.send("", file=file) - - await message.channel.send(f'```py\n{{\n\t"Online": {online},\n\t"Idle/busy/dnd": {idle},\n\t"Offline": {offline}\n}}```') - - elif "sentdebot.p6()" == message.content.lower(): - await message.channel.send(f"```\nThe Neural Networks from Scratch videos will resume one day. https://nnfs.io```") - - - elif "sentdebot.user_activity()" == message.content.lower() and message.channel.id in image_chan_ids: # and len([r for r in author_roles if r.id in admins_mods_ids]) > 0: - - file = discord.File(f"{path}/activity.png", filename=f"{path}/activity.png") - await message.channel.send("", file=file) - - #await message.channel.send(f'```py\n{{\n\t"Online": {online},\n\t"Idle/busy/dnd": {idle},\n\t"Offline": {offline}\n}}```') - - - - elif "help(sentdebot)" == message.content.lower() or "sentdebot.commands()" == message.content.lower(): - await message.channel.send(commands_available) - - # if it doesnt work later. - #elif "sentdebot.logout()" == message.content.lower() and message.author.id == 324953561416859658: - elif "sentdebot.logout()" == message.content.lower() and str(message.author).lower() == "sentdex#7777": - await client.close() - elif "sentdebot.gtfo()" == message.content.lower() and str(message.author).lower() == "sentdex#7777": - await client.close() - - elif "sentdebot.get_history()" == message.content.lower() and str(message.author).lower() == "sentdex#7777": - - channel = sentdex_guild.get_channel(channel_ids[0]) - - async for message in channel.history(limit=999999999999999): - if message.author.id == 324953561416859658: - with open(f"{path}/history_out.csv", "a") as f: - f.write(f"{message.created_at},1\n") - - - else: - query = search_term(message.content) - if query: - #query = match.group(1) - print(query) - - - qsearch = query.replace(" ","%20") - full_link = f"https://pythonprogramming.net/search/?q={qsearch}" - session = HTMLSession() - r = session.get(full_link) - - specific_tutorials = [(tut.text, list(tut.links)[0]) for tut in r.html.find("a") if "collection-item" in tut.html] - - if len(specific_tutorials) > 0: - return_str = "\n---------------------------------------\n".join(f'{tut[0]}: ' for tut in specific_tutorials[:3]) - return_str = f"```Searching for '{query}'```\n" + return_str + f"\n----\n...More results: <{full_link}>" - - await message.channel.send(return_str) - else: - await message.channel.send(f"""```py -Traceback (most recent call last): - File "", line 1, in -NotFoundError: {query} not found```""") - - -client.loop.create_task(user_metrics_background_task()) -client.run(token, reconnect=True) + start_all_background_tasks() + bot.run(token)