async def can_interact(bot, member, channel_id=None): """Checks that the given member can be interacted with. This ensures that the user is: Not a bot Not blocked in the server Additionally, if the user is a member (guild exists): Not in a blocked channel Not blacklisted by the botowners If given a channel ID, also checks that the bot is not muted in there This also checks for maintenace mode """ if data.is_owner(bot, member.id): return True elif member.bot or member.id in data.get(bot, 'core', 'blacklist', default=[]): return False elif bot.maintenance_mode: return False # Guild specific check guild = getattr(member, 'guild', None) if guild: if data.is_mod(bot, member=member): return True guild_data = data.get(bot, 'core', None, guild.id, default={}) if (guild_data.get('muted', False) or (channel_id in guild_data.get('muted_channels', [])) or (member.id in guild_data.get('blocked', []))): return False return True
def convert_tags(bot, guild): if not data.get(bot, 'tags.py', 'tags', guild_id=guild.id): logger.warn("Guild %s (%s) already had tags converted", guild.name, guild.id) return tags = data.get(bot, 'tags.py', 'tags', guild_id=guild.id, default={}) add_tag = bot.plugins['tags.py']._add_tag #key,value,length,volume,name,flags,author,hits,created,last_used,last_used_by,complex,extra for key, tag in tags.items(): to_insert = [ key, # key tag['value'], # value tag['length'], # length tag['volume'], # volume tag['name'], # name tag['flags'], # flags int(tag['author']), # author tag['hits'], # hits int(tag['created']), # created int(tag['last_used']), # last_used None, # last_used_by {}, # complex {} # extra ] add_tag(bot, to_insert, guild.id) data.remove(bot, 'tags.py', 'tags', guild_id=guild.id, safe=True)
async def autolog_dump(bot, context): """Dumps logs into the set channel.""" log_channel = _check_log_channel(bot, context.guild) logged_channel_ids = data.get(bot, __name__, 'channels', guild_id=context.guild.id, default=[]) logged_channels = [] if context.arguments[0]: for channel in context.arguments: if channel.id not in logged_channel_ids: raise CBException("{} is not being logged.".format(channel.mention)) else: logged_channels.append(channel) else: for channel_id in logged_channel_ids: channel = data.get_channel(bot, channel_id, safe=True) if channel: logged_channels.append(channel) else: channel = discord.Object(id=channel_id) channel.mention = 'Deleted channel ({})'.format(channel_id) logged_channels.append(channel) # Build dump data logs = data.get(bot, __name__, 'logs', guild_id=context.guild.id, volatile=True) details = 'Manual dump by {0} (<@{0.id}>): {1}'.format( context.author, context.options['details']) dump_data = _build_dump_data(bot, logs, log_channel, details=details) # Upload dump data logged_messages = await _dump( bot, dump_data, log_channel, details=details, query=context.options['query'], moderator_id=context.author.id, logged_channels=logged_channels) if not logged_messages: raise CBException("No messages to log.") return Response("Messages dumped in {}".format(log_channel.mention))
def _set_logger(bot, channel): """Sets a logger for the given channel.""" default_message_limit = configurations.get(bot, __name__, key='message_limit') message_limit = data.get( bot, __name__, 'message_limit', guild_id=channel.guild.id, default=default_message_limit) logs = data.get( bot, __name__, 'logs', guild_id=channel.guild.id, default={}, create=True, volatile=True) logs[channel.id] = collections.deque(maxlen=message_limit)
async def autolog_channels(bot, context): """Sets the channels that will be logged.""" log_channel = _check_log_channel(bot, context.guild) # Toggle channels for logging if context.arguments[0]: changes = [] for channel in context.arguments: appended = data.list_data_toggle( bot, __name__, 'channels', channel.id, guild_id=context.guild.id) if appended: changes.append('Now logging {}'.format(channel.mention)) _set_logger(bot, channel) else: changes.append('No longer logging {}'.format(channel.mention)) _delete_logger(bot, channel) embed = discord.Embed(title='Logging changes', description='\n'.join(changes)) # Show channels that are currently logged else: default_message_limit = configurations.get(bot, __name__, key='message_limit') message_limit = data.get( bot, __name__, 'message_limit', guild_id=context.guild.id, default=default_message_limit) logged_channel_ids = data.get(bot, __name__, 'channels', guild_id=context.guild.id) if not logged_channel_ids: raise CBException("No channels are currently logged.") # Check logged channels # Removes channels that were deleted and have no logged messages logged_channels = [] logs = data.get(bot, __name__, 'logs', guild_id=context.guild.id, volatile=True) for channel_id in logged_channel_ids: channel = data.get_channel(bot, channel_id, safe=True) if channel: logged_channels.append(channel) else: if len(logs[channel_id]): channel = discord.Object(id=channel_id) channel.mention = 'Deleted channel ({})'.format(channel_id) logged_channels.append(channel) else: # No logged messages for removed channel. Delete log. del logs[channel_id] embed = discord.Embed(title='Logging info') embed.add_field( inline=False, name='Logged channels', value=', '.join(it.mention for it in logged_channels)) embed.add_field(name='Dump channel', value=log_channel.mention) embed.add_field(name='Logged messages', value=message_limit) return Response(embed=embed)
def _awoo_check(bot, message, show_filtered=''): """ Checks for awoo violations. Tier 1: Standard match Tier 2: Bypass attempt match Tier 3: Legalization plea """ # Initial content check content = show_filtered or (message.clean_content.lower() if message.content else '') author, channel = message.author, message.channel if not content or author.bot or isinstance(channel, discord.abc.PrivateChannel): return # Ignore muted guilds, channels, and users guild_data = data.get(bot, 'core', None, message.guild.id, default={}) if (guild_data.get('muted', False) or channel.id in guild_data.get('muted_channels', []) or author.id in guild_data.get('blocked', [])): return # Ignore disabled guilds, disabled channels and whitelisted users guild_awoo_data = data.get(bot, __name__, None, guild_id=message.guild.id, default={}) if (not guild_awoo_data.get('enabled', False) or channel.id in guild_awoo_data.get('disabled_channels', []) or author.id in guild_awoo_data.get('whitelist', [])): return # Tier 3: Legalization plea if PLEA_MATCH.search(content): return 3 # Tier 1: Basic check if BASIC_MATCH.search(content): return 1 # Tier 2: Advanced check filtered = content for key, values in substitutions: for value in values: filtered = filtered.replace(value, key) _check = lambda c: c.isalpha() or c.isspace() filtered = ''.join(c.lower() for c in unicodedata.normalize('NFKD', filtered) if _check(c)) if ADVANCED_MATCH.search(filtered): return 2 # Debug if show_filtered: return filtered
async def awoo_toggle(bot, context): """Toggles awoo detection for either the guild or the given channel.""" guild_awoo_data = data.get( bot, __name__, None, guild_id=context.guild.id, default={}, create=True) # Channel if context.arguments[0]: changes = [] for channel in context.arguments: if channel.id in guild_awoo_data.get('disabled_channels', []): action = 'is now' data.list_data_remove( bot, __name__, 'disabled_channels', value=channel.id, guild_id=context.guild.id) else: action = 'is no longer' data.list_data_append( bot, __name__, 'disabled_channels', channel.id, guild_id=context.guild.id) changes.append('{} {} being monitored.'.format(channel.mention, action)) return Response(content='\n'.join(changes)) # Guild else: guild_awoo_data['enabled'] = not guild_awoo_data.get('enabled', False) return Response(content='Detection is now {}abled'.format( 'en' if guild_awoo_data['enabled'] else 'dis'))
async def character_create(bot, context): """Creates a new character entry.""" # Check if an entry is currently being created/edited tracker = data.get(bot, __name__, 'tracker', user_id=context.author.id, volatile=True) if tracker: return Response( content=( "You are currently already creating or editing a character entry. " "Would you like to cancel your current session?"), message_type=MessageTypes.INTERACTIVE, extra_function=_cancel_menu, extra={'buttons': ['🇾', '🇳']}) # 10 character limit cursor = data.db_select( bot, from_arg='characters', where_arg='owner_id=%s', input_args=[context.author.id]) characters = cursor.fetchall() if cursor else [] if len(characters) >= 10: raise CBException("Cannot create more than 10 characters.") # Use the provided character file if context.message.attachments: content = await _process_data( bot, context.author, context.message.attachments[0].url, propagate_error=True) return Response(content=content) # Use the online entry creator else: await _create_session(bot, context.author) if not context.direct: await context.message.add_reaction('📨')
async def tagremote_update(bot, context): """Renames the webhook with an updated tag list file.""" # Check for an existing session session_data = data.get(bot, __name__, 'data', guild_id=context.guild.id) if not session_data: raise CBException("No session available.") channel = data.get_channel(bot, session_data['channel']) if not channel: await _delete_session(bot, context.guild) raise CBException("Failed to get the channel.") voice_channel = data.get_channel(bot, session_data['voice_channel']) if not voice_channel: await _delete_session(bot, context.guild) raise CBException("Failed to get the voice channel.") webhooks = await channel.webhooks() if not webhooks: await _delete_session(bot, context.guild) raise CBException("No webhooks available.") for webhook in webhooks: if webhook.id == session_data['webhook']: break else: await _delete_session(bot, context.guild) raise CBException("Webhook not found.") tag_dictionary = _get_tag_dictionary(bot, context.guild) session_code = await _upload_session_data(bot, channel, voice_channel, webhook, tag_dictionary) updated_code = session_code.split(':')[1] await webhook.edit(name='Tag Remote [{}]'.format(updated_code)) return Response( content="Tag data refreshed. Update the remote on your phone via the options menu.")
async def translate(bot, context): """Translates the given text.""" source = context.options.get('from', 'auto') if 'to' in context.options: destination = context.options['to'] else: if context.direct: destination = 'en' else: destination = data.get( bot, __name__, 'default', guild_id=context.guild.id, default='en') try: result = await utilities.future( TRANSLATOR.translate, context.arguments[0], src=source, dest=destination) except ValueError as e: if 'source' in e.args[0]: issue, language = 'source', source else: issue, language = 'destination', destination raise CBException("Invalid {} language (`{}`).\n{}".format(issue, language, LANGUAGE_LINK)) except Exception as e: raise CBException("Failed to translate the text.", e) full_source = googletrans.constants.LANGUAGES[result.src.lower()].title() full_destination = googletrans.constants.LANGUAGES[result.dest.lower()].title() embed = discord.Embed( title=':arrows_counterclockwise: Google Translate', color=discord.Color(0x3b88c3)) embed.add_field(name=full_source, value=context.arguments[0], inline=False) embed.add_field(name=full_destination, value=result.text, inline=False) return Response(embed=embed)
def _update_current_game(bot, safe=False, include_setup_status=False): """Updates the index of the latest/current game.""" schedule_data = data.get(bot, __name__, 'schedule', volatile=True, default=[]) current_time = datetime.datetime.utcnow() for index, game in enumerate(schedule_data): start_time, end_time = game['scheduled'], game['end'] if start_time <= current_time < end_time: # Update latest index data.add(bot, __name__, 'current_index', index, volatile=True) data.add(bot, __name__, 'current_game', game, volatile=True) if include_setup_status: setup_time = datetime.timedelta(seconds=game['setup_seconds']) return index, (current_time < start_time + setup_time) else: return index elif current_time < start_time: logger.debug("The current time is less than the start time. Index: %s", index) break else: # GDQ over, or past schedule game, index = None, 999 if safe: data.add(bot, __name__, 'current_index', index, volatile=True) data.add(bot, __name__, 'current_game', game, volatile=True) if include_setup_status: return index, True else: return index raise CBException("No current game was found.")
def _delete_logger(bot, channel): """Removes a logger for the given channel.""" logs = data.get( bot, __name__, 'logs', guild_id=channel.guild.id, default={}, create=True, volatile=True) if channel.id in logs: del logs[channel.id]
async def role_joinleave(bot, context): """Adds/removes the given role(s) to/from the member.""" # Check for a verified role verified_role = data.get_custom_role(bot, __name__, 'verified', context.guild) if verified_role: if not data.has_custom_role(bot, __name__, 'verified', member=context.author): raise CBException("You must have the role {} in order to self-assign roles.".format( verified_role.mention)) joining = context.id == 'join' available_role_ids = data.get(bot, __name__, 'roles', guild_id=context.guild.id, default=[]) for role in context.arguments: if role.id not in available_role_ids: raise CBException("The role {} is not self-assignable.".format(role.mention)) try: action_function = context.author.add_roles if joining else context.author.remove_roles await action_function(*context.arguments, reason="Self-assignable role") except discord.Forbidden: if not context.guild.me.guild_permissions.manage_roles: raise CBException("The bot is missing the `Manage Roles` permission.") _check_roles(bot, context.guild) action = 'assign' if joining else 'remov' raise CBException("The role(s) could not be {}ed due to a hierarchy issue.".format(action)) embed = discord.Embed( title='You have {} the role{}:'.format( 'joined' if joining else 'left', '' if len(context.arguments) == 1 else 's'), description='\n'.join(it.mention for it in context.arguments)) return Response(embed=embed)
def get_permission_bits(bot): """Calculates all of the permissions for each plugin.""" dummy = discord.Permissions() for plugin in bot.plugins.keys(): for permission in data.get( bot, plugin, 'permissions', volatile=True, default={}): setattr(dummy, permission.lower(), True) return dummy.value
async def check_webhook_messages(bot, message): """Intercepts webhook messages to the data channel. There are 3 separate stages: 0 - Starting stage (webhook exists) 1 - User has submitted the file, edit webhook name with return code 2 - User acknowledges result, requests that the webhook be deleted """ if message.channel != DATA_CHANNEL: return # Check for valid webhook messages webhook_id = message.author.id if webhook_id not in DATA_CHANNEL_WEBHOOK_IDS: return stage = data.get(bot, __name__, 'stage', user_id=webhook_id, volatile=True) if stage is not None: if message.content == '1' and stage == 0: # Progress to stage 1 owner = data.get(bot, __name__, 'owner', user_id=webhook_id, volatile=True) webhook = data.get(bot, __name__, 'tracker', user_id=owner.id, volatile=True) result = await _process_data(bot, owner, message.attachments[0].url) # Parse result data.add(bot, __name__, 'stage', 1, user_id=webhook_id, volatile=True) await webhook.edit(name='ok' if result == 0 else 'err:{}'.format(result)) elif message.content == '2' and stage == 1: # Progress to stage 2 await _clear_webhook(bot, webhook_id) else: # Invalid state progression detected (likely duplicate) logger.warn("Invalid state progression detected. Message content: %s", message.content) await _clear_webhook(bot, webhook_id) pass # TODO: Consider notifying user? else: # Desync logger.warn("Webhook state desynchronization detected.") await _clear_webhook(bot, webhook_id) webhooks = await DATA_CHANNEL.webhooks() for webhook in webhooks: # In case the webhook ID was invalid if webhook.id == webhook_id: await webhook.delete() break
def _get_message_logger(bot, channel=None): """Gets the message logger if it exists.""" if isinstance(channel, discord.abc.PrivateChannel): return None message_loggers = data.get( bot, __name__, 'logs', guild_id=channel.guild.id, volatile=True, default={}) if channel: return message_loggers.get(channel.id, None) return message_loggers
async def set_ip_address(bot, context): if context.arguments[0]: data.add(bot, __name__, 'server_ip', context.arguments[0], guild_id=context.guild.id) response = "IP address set!" else: # Get current IP default_ip = configurations.get(bot, __name__, key='default_ip') response = "The current IP address is: {}".format( data.get(bot, __name__, 'server_ip', guild_id=context.guild.id, default=default_ip)) return Response(content=response)
async def autolog_messages(bot, context): """Sets the number of messages to log in each channel.""" _check_log_channel(bot, context.guild) data.add(bot, __name__, 'message_limit', context.arguments[0], guild_id=context.guild.id) logged_channels = data.get(bot, __name__, 'channels', guild_id=context.guild.id, default=[]) for channel_id in logged_channels: channel = context.guild.get_channel(channel_id) _set_logger(bot, channel) return Response("{} messages will be logged for each channel.".format(context.arguments[0]))
def _get_verified_role(bot, guild, member=None): """Checks for the verified role and returns it unless the member has the role.""" role_id = data.get(bot, __name__, 'verification_role', guild_id=guild.id) verified_role = data.get_role(bot, role_id, guild=guild, safe=True) if not (role_id or verified_role): raise CBException_vc("The verified role has not been set.") if member and verified_role in member.roles: raise CBException_vc("{} already has the role {}.".format( member.mention, verified_role.mention)) return verified_role
async def setup_loggers(bot): """Sets up the loggers for each guild.""" for guild in bot.guilds: logged_channels = data.get(bot, __name__, 'channels', guild_id=guild.id, default=[]) for channel_id in logged_channels[:]: channel = guild.get_channel(channel_id) if channel: _set_logger(bot, channel) else: logged_channels.remove(channel_id)
def add_bot_permissions(bot, plugin_name, **permissions): """Adds the given permissions to the bot for authentication generation.""" dummy = discord.Permissions() for permission in permissions: try: getattr(dummy, permission.lower()) except: # Permission not found raise CBException("Permission '{}' does not exist".format(permission)) current = data.get( bot, plugin_name, 'permissions', create=True, volatile=True) if current is None: data.add(bot, plugin_name, 'permissions', permissions, volatile=True)
def _get_next_games(bot, retrieve, guild_id): """Gets the current/next game(s) and the defined number of extra games.""" latest_index, in_setup = _update_current_game(bot, safe=True, include_setup_status=True) schedule_data = data.get(bot, __name__, 'schedule', volatile=True, default=[]) if not in_setup: latest_index += 1 games_list = schedule_data[latest_index:latest_index + retrieve] embed_data = _embed_games_information(bot, games_list, guild_id) if embed_data: return embed_data else: raise CBException("Game information not found.")
def get_advertisement(bot, location): """Retrieves an advertisement if one is scheduled.""" all_uses = data.get(bot, __name__, 'uses', volatile=True) ad_uses = configurations.get(bot, __name__, 'ad_uses') ad_uses = ad_uses if ad_uses > 0 else 30 current_uses = all_uses.get(location.id, 0) if current_uses >= ad_uses - 1: # Show advertisement if location.id in all_uses: del all_uses[location.id] content = random.choice(( "Consider supporting Wolfram|Alpha by trying out Wolfram|Alpha " "Pro! It helps keep Wolfram|Alpha free, and provides you with " "a much more complete knowledge database experience.", "Do you work/study in a STEM field? Wolfram|Alpha Pro can help!", "Need help with STEM homework? Wolfram|Alpha Pro has you covered " "with step-by-step instructions on how to solve almost any " "calculus problems.", "Experience professional-grade computational knowledge with " "Wolfram|Alpha Pro.", "Student or educator in STEM? Wolfram|Alpha brings you the " "professional features you need to excel.", "Love Wolfram|Alpha? Get more out of your Wolfram|Alpha " "experience by going pro!", "Need beautifully crafted interactive data visuals? Wolfram|Alpha " "Pro can do that for you!", "Professional-grade data analysis and visualization can " "greatly expedite completing projects and presentations.", "Need help with math homework? Get step-by-step solutions for " "complexity ranging from arithmetic to calculus and beyind!", "Having trouble with learning mathematics? It doesn't matter " "if it's algebra or differential equations, Wolfram|Alpha Pro " "gives you step-by-step solutions.", "Need extra math practice? Wolfram|Alpha Pro can generate an " "infinite number of practice problems with step-by-step " "solutions to help you ace your exams.", "Frequent Wolfram|Alpha user? Tailor your experience for your own " "needs with Wolfram|Alpha Pro!", "Are your queries timing out? Wolfram|Alpha Pro extends " "computation times.", "Need powerful visualization and analysis tools for your data? " "Wolfram|Alpha Pro is for you!", "Directly interact with and download computed data with " "Wolfram|Alpha Pro." )) link = random.choice(( 'See more at', 'For more information, visit', 'See what upgrading can do at', 'Interested? Check out', 'Click here for more:', 'Ready to upgrade? See', 'Curious? Learn more at', 'Check it out at')) + ' <https://www.wolframalpha.com/pro/>' return content + ' ' + link else: all_uses[location.id] = current_uses + 1
async def set_units(bot, context): default_units = configurations.get(bot, __name__, key='default_units') units = data.get( bot, __name__, 'server_units', guild_id=context.guild.id, default=default_units) if units == 'metric': new_units = 'nonmetric' response = 'US standard' else: new_units = 'metric' response = 'Metric' data.add(bot, __name__, 'server_units', new_units, guild_id=context.guild.id) return Response(content=response + ' units are now set as the default.')
def _check_roles(bot, guild): """Checks/ensures the validity of available self-assignable roles in the guild.""" available_role_ids = data.get(bot, __name__, 'roles', guild_id=guild.id, default=[]) guild_roles = dict((it.id, it) for it in guild.roles) top_role = guild.me.top_role remaining_roles = [] for role_id in available_role_ids: if role_id not in guild_roles or guild_roles[role_id] > top_role: data.list_data_remove(bot, __name__, 'roles', role_id, guild_id=guild.id) else: remaining_roles.append(guild_roles[role_id]) return remaining_roles
async def _get_buffered_donation_stats(bot): """Pulls buffered donation information if it is 1 minute old or less.""" last_pull = data.get(bot, __name__, 'last_pull', volatile=True, default=0) buffer_time = configurations.get(bot, __name__, 'stats_buffer_time') if time.time() - last_pull > buffer_time: # Pull information data.add(bot, __name__, 'last_pull', time.time(), volatile=True) tracker_url = configurations.get(bot, __name__, 'tracker_url') try: donate_html = (await utilities.future(requests.get, tracker_url)).text soup = BeautifulSoup(donate_html, 'html.parser') donation_text = soup.find('small').text.splitlines()[1:] total_raised, total_donations, _unused = donation_text[1].split() total_donations = total_donations.strip('()') max_average = donation_text[3] except Exception as e: raise CBException("Failed to retrieve donation data.", e=e) donation_stats = [total_raised, total_donations, max_average] data.add(bot, __name__, 'donation_stats', donation_stats, volatile=True) else: donation_stats = data.get(bot, __name__, 'donation_stats', volatile=True) return donation_stats
async def role_delete(bot, context): """Deletes the given roles.""" available_role_ids = data.get(bot, __name__, 'roles', guild_id=context.guild.id, default=[]) for role in context.arguments: if role.id not in available_role_ids: raise CBException("The role {} is not self-assignable.".format(role.mention)) try: for role in context.arguments: await role.delete(reason='Deleted by {0} ({0.id})'.format(context.author)) except discord.Forbidden: raise CBException("The bot is missing the `Manage Roles` permission.") return Response(embed=discord.Embed(description='Roles deleted.'))
async def join_and_ready(bot, voice_channel, is_mod=False, reconnect=False): """Joins the voice channel and stops any audio playing. Returns the voice_client object from voice_channel.connect() """ guild = voice_channel.guild muted_channels = data.get(bot, 'core', 'muted_channels', guild_id=guild.id, default=[]) if voice_channel == guild.afk_channel: raise CBException("This is the AFK channel.") if voice_channel.id in muted_channels and not is_mod: raise CBException("The bot is muted in this voice channel.") if reconnect: try: await stop_audio(bot, guild) except: pass voice_client = guild.voice_client if not voice_client: try: voice_client = await asyncio.wait_for( voice_channel.connect(timeout=5.0, reconnect=False), timeout=10.0, loop=bot.loop) except asyncio.TimeoutError as e: try: await stop_audio(bot, guild, force=True) except: pass raise CBException("Timed out trying to join the voice channel.") except Exception as e: try: await stop_audio(bot, guild) except: pass raise CBException("Failed to join the voice channel.", e=e) if voice_client.is_playing(): voice_client.stop() else: if voice_client.is_playing(): voice_client.stop() if voice_client.channel != voice_channel: try: await voice_client.move_to(voice_channel) except Exception as e: try: await stop_audio(bot, guild) except: pass raise CBException("Failed to move to the voice channel.", e=e) return voice_client
async def gdq_menu(bot, context, response, result, timed_out): if timed_out: response.update_stats = False if response.update_task: response.update_task.cancel() return if not result and not response.update_task: response.update_task = asyncio.ensure_future(_update_menu(bot, response)) return selection = ['⬅', '⏺', '➡'].index(result[0].emoji) schedule_data = data.get(bot, __name__, 'schedule', volatile=True) guild_id = context.guild.id if context.guild else None if selection in (0, 2): # Page navigation offset = -5 if selection == 0 else 5 response.game_index = max(min(response.game_index + offset, len(schedule_data) - 5), 0) else: response.game_index = data.get(bot, __name__, 'current_index', volatile=True, default=0) games_list = schedule_data[response.game_index:response.game_index + 5] game_data = _embed_games_information(bot, games_list, guild_id) values = [(it + response.game_index + 1, *c) for it, c in enumerate(game_data)] value = '\n\n'.join('**`[{}]` {}**\n{}'.format(*it) for it in values) response.embed.set_field_at(1, name='Schedule', value=value, inline=False) await response.message.edit(embed=response.embed)
def convert_core(bot, guild): if data.get(bot, 'core', None, guild_id=guild.id): logger.warn("Guild %s (%s) already had core converted", guild.name, guild.id) return base_data = data.get(bot, 'base', None, guild_id=guild.id, default={}) if 'disabled' in base_data: # TODO: Iterate through toggled commands pass if 'blocked' in base_data: replacement = [] for entry in base_data['blocked']: replacement.append(int(entry)) base_data['blocked'] = replacement if 'muted_channels' in base_data: replacement = [] for entry in base_data['muted_channels']: replacement.append(int(entry)) base_data['muted_channels'] = replacement if 'moderators' in base_data: del base_data['moderators'] if base_data: for key, value in base_data.items(): data.add(bot, 'core', key, value, guild_id=guild.id) data.remove(bot, 'base', None, guild_id=guild.id)
async def wolfram_alpha_query(bot, query, user_ip, indices='', format_param='plaintext,image', units='metric'): """Returns a query result from Wolfram|Alpha.""" client = data.get(bot, __name__, 'client', volatile=True) client_query = client.GetQuery(query=query) client_query.ToURL() if indices: client_query.AddPodIndex(podindex=indices) # client_query.AddFormat(format_param=format_param) # need both client_query.AddIp(ip=user_ip) client_query.AddUnits(units=units) #query_result = await async_query(client, client_query.Query) try: query_result = await utilities.future(client.PerformQuery, client_query.Query) except Exception as e: raise CBException("The query could not be processed.", e=e) result = wap.WolframAlphaQueryResult(query_result) element = ElementTree.fromstring(result.XmlResult) return ElementTree.ElementTree(element=element).getroot()
async def autolog_channels(bot, context): """Sets the channels that will be logged.""" log_channel = _check_log_channel(bot, context.guild) # Toggle channels for logging if context.arguments[0]: changes = [] for channel in context.arguments: appended = data.list_data_toggle(bot, __name__, 'channels', channel.id, guild_id=context.guild.id) if appended: changes.append('Now logging {}'.format(channel.mention)) _set_logger(bot, channel) else: changes.append('No longer logging {}'.format(channel.mention)) _delete_logger(bot, channel) embed = discord.Embed(title='Logging changes', description='\n'.join(changes)) # Show channels that are currently logged else: default_message_limit = configurations.get(bot, __name__, key='message_limit') message_limit = data.get(bot, __name__, 'message_limit', guild_id=context.guild.id, default=default_message_limit) logged_channel_ids = data.get(bot, __name__, 'channels', guild_id=context.guild.id) if not logged_channel_ids: raise CBException("No channels are currently logged.") # Check logged channels # Removes channels that were deleted and have no logged messages logged_channels = [] logs = data.get(bot, __name__, 'logs', guild_id=context.guild.id, volatile=True) for channel_id in logged_channel_ids: channel = data.get_channel(bot, channel_id, safe=True) if channel: logged_channels.append(channel) else: if len(logs[channel_id]): channel = discord.Object(id=channel_id) channel.mention = 'Deleted channel ({})'.format(channel_id) logged_channels.append(channel) else: # No logged messages for removed channel. Delete log. del logs[channel_id] embed = discord.Embed(title='Logging info') embed.add_field(inline=False, name='Logged channels', value=', '.join(it.mention for it in logged_channels)) embed.add_field(name='Dump channel', value=log_channel.mention) embed.add_field(name='Logged messages', value=message_limit) return Response(embed=embed)
async def _violation_notification(bot, message, awoo_tier, send_message=True): """ Logs the violation and (optionally) sends the user a notification. Standard notification: once per violation, up to 1 time None: 2 violations Silence notification: 1 violation Reset period for notifications is 1 minute. Stress indicates a number of users making a violation within a 60 second period. Tier 1: 3 members Tier 2: 5 members Tier 3: 8 members """ author, channel = message.author, message.channel current_time = time.time() violation_data = data.get(bot, __name__, 'user_violation', user_id=author.id, volatile=True) channel_violation_data = data.get(bot, __name__, 'channel_violation', channel_id=channel.id, volatile=True) if not violation_data or current_time - violation_data['time'] >= 60: violation_data = {'time': 0, 'violations': 0} data.add(bot, __name__, 'user_violation', violation_data, user_id=author.id, volatile=True) if not channel_violation_data or current_time - channel_violation_data[ 'time'] >= 60: channel_violation_data = { 'time': 0, 'violators': set(), 'sent_tier': 0 } data.add(bot, __name__, 'channel_violation', channel_violation_data, channel_id=channel.id, volatile=True) violation_data['violations'] += 1 violation_data['time'] = current_time channel_violation_data['violators'].add(author.id) channel_violation_data['time'] = current_time # Update table set_arg = 'debt = debt+%s, violations = violations+1' if awoo_tier == 2: set_arg += ', sneaky = sneaky+1' cursor = data.db_select(bot, from_arg='awoo', where_arg='user_id=%s', input_args=[author.id]) entry = cursor.fetchone() if cursor else None if entry: data.db_update(bot, 'awoo', set_arg=set_arg, where_arg='user_id=%s', input_args=[fine, author.id]) else: data.db_insert( bot, 'awoo', input_args=[author.id, fine, 1, 1 if awoo_tier == 2 else 0]) # Add a snarky message depending on the tier if awoo_tier == 2: # Attempted bypass snark = random.choice(statements['bypass']) + '\n' elif awoo_tier == 3: # Legalization plea snark = random.choice(statements['legalize']) + '\n' else: snark = '' # Notify user logger.debug("Violations: %s", violation_data['violations']) text = '' if violation_data['violations'] <= 1: text = "{}{} has been fined ${} for an awoo violation.".format( snark, author.mention, fine) elif violation_data['violations'] == 4: text = "{} {}".format(author.mention, random.choice(statements['silence'])) elif awoo_tier == 3 and violation_data[ 'violations'] <= 3: # Legalization plea, but silent text = snark if send_message and text: await channel.send(content=text) else: await message.add_reaction( random.choice(['🚩', '🛑', '�', '⛔', '🚫'])) # Stress violators, sent_tier = channel_violation_data[ 'violators'], channel_violation_data['sent_tier'] if (len(violators) == 3 and sent_tier == 0 or len(violators) == 5 and sent_tier == 1 or len(violators) == 8 and sent_tier == 2): if send_message: await message.channel.send( random.choice(statements['stress'][sent_tier])) channel_violation_data['sent_tier'] += 1
async def get_query_result(bot, guild, query, text_result=False, result_lines=0): """Gets a query result and formats it.""" default_ip = configurations.get(bot, __name__, key='default_ip') default_units = configurations.get(bot, __name__, key='default_units') if guild is None: server_ip = default_ip units = default_units else: server_ip = data.get(bot, __name__, 'server_ip', guild_id=guild.id, default=default_ip) units = data.get(bot, __name__, 'server_units', guild_id=guild.id, default=default_units) indices = ','.join((str(index) for index in range(1, result_lines + 2))) format_param = 'plaintext' + ('' if text_result else ',image') root = await wolfram_alpha_query(bot, query, server_ip, indices=indices, format_param=format_param, units=units) pods = root.findall('pod') warning = None query_url = 'http://www.wolframalpha.com/input/?i={}'.format( urllib.parse.quote_plus(query)) # Error handling if root.get('success') == 'false': suggestions = root.find('didyoumeans') if suggestions: suggestions = suggestions.findall('didyoumean') suggestion_text = [suggestion.text for suggestion in suggestions] raise CBException( "Wolfram|Alpha could not interpret your query.{}".format( '' if suggestions is None else ' Suggestion(s): {}'. format(', '.join(suggestion_text[:3])))) elif root.get('timedout'): if len(pods) == 0: raise CBException("Query timed out.", query_url) elif len(pods) < result_lines: warning = "Query timed out but returned some results" elif len(pods) == 0: raise CBException("No result given (general error).", query_url) # Format answer result_list = [] if root.find('pod').get('id') != 'Input': result_lines -= 1 if root.find('warnings') is not None: spellchecks = root.find('warnings').findall('spellcheck') for spellcheck in spellchecks: result_list.append(('spellcheck', None, spellcheck.get('text'))) for pod in root.findall('pod')[:1 + result_lines]: for index, sub_pod in enumerate(pod.findall('subpod')): image = sub_pod.find('img') image_url = '' if image is None else image.get('src') text = sub_pod.find('plaintext').text title = pod.get('title') if index > 0: title = None result_list.append((title, image_url, text)) if text_result: result = [] for query_result in result_list: text = query_result[2] if text: if query_result[0] == 'spellcheck': result.append(('Spell check', text)) elif query_result[0]: result.append((query_result[0], text)) else: result.append(('\u200b', text)) else: result.append( (query_result[0], '[`Image`]({})'.format(query_result[1]))) else: # Get the image result = await get_result_as_image(bot, result_list) return query_url, result, warning
async def on_message(self, message, replacement_message=None): # Ensure bot can respond properly try: initial_data = self.can_respond(message) except Exception as e: # General error logger.error(e) logger.error(traceback.format_exc()) self.last_exception = e return if not initial_data: return # Ensure command is valid content = initial_data[0] elevation = 3 - (initial_data[4:0:-1] + [True]).index(True) split_content = content.split(' ', 1) if len(split_content) == 1: # No spaces split_content.append('') base, parameters = split_content base = base.lower() try: command = self.commands[base] except KeyError: logger.debug("Suitable command not found: " + base) return # Check that user is not spamming author_id = message.author.id direct = isinstance(message.channel, PrivateChannel) spam_value = self.spam_dictionary.get(author_id, 0) if elevation > 0 or direct: # Moderators ignore custom limit spam_limit = self.spam_limit else: spam_limit = min( self.spam_limit, data.get(self, 'core', 'spam_limit', guild_id=message.guild.id, default=self.spam_limit)) if spam_value >= spam_limit: if spam_value == spam_limit: self.spam_dictionary[author_id] = spam_limit + 1 plugins.broadcast_event(self, 'bot_on_user_ratelimit', message.author) await self.send_message( message.channel, "{0}, you appear to be issuing/editing " "commands too quickly. Please wait {1} seconds.". format(message.author.mention, self.spam_timeout)) return # Parse command and reply try: context = None with message.channel.typing(): logger.debug(message.author.name + ': ' + message.content) subcommand, options, arguments = parser.parse( self, command, parameters, message) context = self.Context( message, base, subcommand, options, arguments, subcommand.command.keywords, initial_data[0], elevation, message.guild, message.channel, message.author, direct, subcommand.index, subcommand.id, self) plugins.broadcast_event(self, 'bot_on_command', context) logger.info([subcommand, options, arguments]) response = await commands.execute(self, context) if response is None: response = Response() if self.selfbot and response.content: response.content = '\u200b' + response.content except Exception as e: # General error response = Response() destination = message.channel message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) else: # Attempt to respond send_arguments = response.get_send_kwargs(replacement_message) try: destination = response.destination if response.destination else message.channel message_reference = None if replacement_message: try: await replacement_message.edit(**send_arguments) message_reference = replacement_message except discord.NotFound: # Message deleted response = Response() message_reference = None elif (not response.is_empty() and not (self.selfbot and response.message_type is MessageTypes.REPLACE)): message_reference = await destination.send( **send_arguments) response.message = message_reference plugins.broadcast_event(self, 'bot_on_response', response, context) except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) # Incremement the spam dictionary entry if author_id in self.spam_dictionary: self.spam_dictionary[author_id] += 1 else: self.spam_dictionary[author_id] = 1 # MessageTypes: # NORMAL - Normal. The issuing command can be edited. # PERMANENT - Message is not added to the edit dictionary. # REPLACE - Deletes the issuing command after 'extra' seconds. Defaults # to 0 seconds if 'extra' is not given. # ACTIVE - The message reference is passed back to the function defined # with 'extra_function'. If 'extra_function' is not defined, it will call # plugin.handle_active_message. # INTERACTIVE - Assembles reaction buttons given by extra['buttons'] and # calls 'extra_function' whenever one is pressed. # WAIT - Wait for event. Calls 'extra_function' with the result, or None # if the wait timed out. # # Only the NORMAL message type can be edited. response.message = message_reference if message_reference and isinstance(message_reference.channel, PrivateChannel): permissions = self.user.permissions_in( message_reference.channel) elif message_reference: permissions = message_reference.guild.me.permissions_in( message_reference.channel) else: permissions = None self.last_response = message_reference if response.message_type is MessageTypes.NORMAL: # Edited commands are handled in base.py if message_reference is None: # Forbidden exception return wait_time = self.edit_timeout if wait_time: self.edit_dictionary[str(message.id)] = message_reference await asyncio.sleep(wait_time) if str(message.id) in self.edit_dictionary: del self.edit_dictionary[str(message.id)] if message_reference.embeds: embed = message_reference.embeds[0] if embed.footer.text and embed.footer.text.startswith( '\u200b' * 3): embed.set_footer() try: await message_reference.edit(embed=embed) except: pass elif response.message_type is MessageTypes.REPLACE: try: if self.selfbot and not replacement_message: # Edit instead await message.edit(**send_arguments) else: if response.extra: await asyncio.sleep(response.extra) try: await message.delete(reason='Automatic') except: # Ignore permissions errors pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.ACTIVE: if message_reference is None: # Forbidden exception return try: await response.extra_function(self, context, response) except Exception as e: # General error message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.INTERACTIVE: try: buttons = response.extra['buttons'] kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 if 'check' not in kwargs: kwargs['check'] = (lambda r, u: r.message.id == message_reference.id and not u.bot) for button in buttons: await message_reference.add_reaction(button) reaction_check = await destination.get_message( message_reference.id) for reaction in reaction_check.reactions: if not reaction.me or reaction.count > 1: async for user in reaction.users(): if user != self.user and permissions.manage_messages: asyncio.ensure_future( message_reference.remove_reaction( reaction, user)) await response.extra_function(self, context, response, None, False) process_result = True while process_result is not False: try: if not permissions.manage_messages: add_task = self.wait_for( 'reaction_add', **kwargs) remove_task = self.wait_for( 'reaction_remove', **kwargs) done, pending = await asyncio.wait( [add_task, remove_task], return_when=FIRST_COMPLETED) result = next(iter(done)).result() for future in pending: future.cancel() else: # Can remove reactions result = await self.wait_for( 'reaction_add', **kwargs) if result[1] != self.user: asyncio.ensure_future( message_reference.remove_reaction( *result)) else: continue is_mod = data.is_mod(self, message.guild, result[1].id) if (response.extra.get('reactionlock', True) and not result[0].me or (response.extra.get('userlock', True) and not (result[1] == message.author or is_mod))): continue except (asyncio.futures.TimeoutError, asyncio.TimeoutError): await response.extra_function( self, context, response, None, True) process_result = False else: process_result = await response.extra_function( self, context, response, result, False) try: await response.message.clear_reactions() except: pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.WAIT: try: kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 process_result = True while process_result is not False: try: result = await self.wait_for( response.extra['event'], **kwargs) except asyncio.TimeoutError: await response.extra_function( self, context, response, None) process_result = False else: process_result = await response.extra_function( self, context, response, result) if not response.extra.get('loop', False): process_result = False except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference else: logger.error("Unknown message type: {}".format( response.message_type)) '''
def can_respond(self, message): """Determines whether or not the bot can respond. Checks that the message has text, matches an invoker, and that the guild/channel/user is not muted or blocked. Admins and mods override this. """ if self.fresh_boot is None: # Ignore until bot is ready return False # Ignore empty messages and messages by bots if (not message.content or message.author.bot or message.author.id == self.user.id) and not self.selfbot: return False # Check that the message starts with a valid invoker content = message.content if isinstance(message.channel, PrivateChannel): # No custom invoker or data guild_data = {} invokers = self.command_invokers else: guild_data = data.get(self, 'core', None, message.guild.id, default={}) invokers = [guild_data.get('command_invoker', None)] if not invokers[0]: invokers = self.command_invokers has_mention_invoker = False has_name_invoker = False has_nick_invoker = False has_regular_invoker = False for invoker in invokers: if content.startswith(invoker): has_regular_invoker = True break if not has_regular_invoker: has_mention_invoker = content.startswith( ('<@' + str(self.user.id) + '>', '<@!' + str(self.user.id) + '>')) if not has_mention_invoker: clean_content = content.lower() has_name_invoker = clean_content.startswith( self.user.name.lower()) if (not has_name_invoker and not isinstance(message.channel, PrivateChannel) and message.guild.me.nick): has_nick_invoker = clean_content.startswith( message.guild.me.nick.lower()) if has_nick_invoker: # Clean up content (nickname) content = content[len(message.guild.me.nick ):].strip() else: # Clean up content (name) content = content[len(self.user.name):].strip() else: # Clean up content (mention) content = content.partition(' ')[2].strip() else: # Clean up content (invoker) content = content.partition(invoker)[2].strip() if guild_data.get('mention_mode', False): # Mention mode enabled if not (has_mention_invoker or has_name_invoker or has_nick_invoker): return False else: # Any invoker will do if not (has_regular_invoker or has_mention_invoker or has_name_invoker or has_nick_invoker): return False if self.selfbot: # Selfbot check if message.author.id == self.owners[0]: return [content, False, False, True] else: return False # Respond to direct messages author = message.author is_owner = author.id in self.owners if isinstance(message.channel, PrivateChannel): return [content, False, False, is_owner] modrole = data.get(self, 'core', 'modrole', guild_id=message.guild.id, volatile=True) is_mod = modrole in author.roles or author.guild_permissions.administrator is_admin = author == message.guild.owner result = [content, is_mod, is_admin, is_owner] # Owners/moderators override everything # This is faster than calling the function in jshbot.data channel_id = message.channel.id if is_mod or is_admin or is_owner: return result # Server/channel muted, or user is blocked if (guild_data.get('muted', False) or (channel_id in guild_data.get('muted_channels', [])) or (author.id in guild_data.get('blocked', []))): return False else: return result # Clear to respond
async def automated_dump_message(bot, guild, details, query=None, moderator_id=None): """Generates an automated log menu that asks the user to select channels to log.""" try: log_channel = _check_log_channel(bot, guild) except BotException: return # Fetch and build preliminary dump data logs = data.get(bot, __name__, 'logs', guild_id=guild.id, volatile=True) if not logs: return dump_data = _build_dump_data(bot, logs, log_channel, details=details) channel_ids = list(logs.keys()) channel_mentions = '<#' + '>, <#'.join(str(it) for it in channel_ids) + '>' # Show confirmation box embed = discord.Embed(title=':warning: Autolog event triggered', color=discord.Color(0xffcc4d)) embed.add_field(name='Details', value=details, inline=False) embed.add_field( name='\u200b', inline=False, value='By default, these channels will be logged:\n{}'.format( channel_mentions)) embed.add_field( name='\u200b', inline=False, value= ('Listed channels will be logged in 5 minutes.\n' 'Click :x: to cancel, :next_track: to log now, or :grey_question: to specify channels.' )) message = await log_channel.send(embed=embed) async def _menu(bot, context, response, result, timed_out): if not result and not timed_out: return # Timed out or skipped waiting period if timed_out or (result[0].emoji == '⏭'): response.embed.remove_field(2) response.embed.set_field_at(1, name='\u200b', value='Logging started.') await response.message.edit(embed=response.embed) asyncio.ensure_future( _dump(bot, dump_data, log_channel, details=details, query=query, moderator_id=moderator_id)) # Cancelled elif result[0].emoji == '❌': response.embed.remove_field(2) response.embed.add_field(name='\u200b', value='Logging cancelled.') await response.message.edit(embed=response.embed) # Specify channels else: response.embed.set_field_at( 2, name='\u200b', inline=False, value= '**Type the channels you want to log, separated by spaces.**') await response.message.edit(embed=response.embed) try: await response.message.clear_reactions() except: pass # Read response kwargs = { 'timeout': 300, 'check': lambda m: m.author == result[1] and m.channel == context. channel } channels = [] response.embed.remove_field(2) try: result = await bot.wait_for('message', **kwargs) for it in result.content.split(): channel = data.get_channel(bot, it, constraint=discord.TextChannel) if channel.id not in channel_ids: raise CBException("Channel {} not logged.".format( channel.mention)) channels.append(channel) try: await result.delete() except: pass except BotException as e: logger.debug("Error!") response.embed.set_field_at( 1, name='\u200b', value='{}\nDefault channels will be logged.'.format( e.error_details)) except Exception as e: logger.debug("Timeout!") response.embed.set_field_at( 1, name='\u200b', value= 'Channel selection timed out. Default channels will be logged.' ) # Upload dump data await response.message.edit(embed=response.embed) await _dump(bot, dump_data, log_channel, details=details, query=query, moderator_id=moderator_id, logged_channels=channels) return False extra = { 'buttons': ['❌', '⏭', '❔'], 'elevation': Elevation.BOT_MODERATORS, 'autodelete': 30, 'kwargs': { 'timeout': 300 } } response = Response(embed=embed, message_type=MessageTypes.INTERACTIVE, extra=extra, extra_function=_menu) await bot.handle_response(message, response, message_reference=message)
async def check_commission_advertisement(bot, message): """Checks new messages in the commissions channel.""" if isinstance(message.channel, discord.abc.PrivateChannel): return guild_data = data.get(bot, __name__, None, guild_id=message.guild.id, default={}) if (not guild_data.get('rules') or message.channel.id != guild_data['rules']['channel'] or message.author.id in guild_data.get('whitelist', []) or message.author.bot): return cooldown = guild_data['rules']['cooldown'] advertisement_data = await _get_advertisement_data( bot, message.guild, ignore_user_id=message.author.id) deleted_persistence = data.get(bot, __name__, 'recently_deleted', guild_id=message.guild.id, default={}) time_delta = cooldown # Assume cooldown has been passed author_id = message.author.id # Check the last advertisement's creation time (if it exists) if str(author_id) in deleted_persistence: time_delta = time.time() - deleted_persistence[str(author_id)] if author_id in advertisement_data: last_message = advertisement_data[author_id] time_delta = time.time() - last_message.created_at.replace( tzinfo=tz.utc).timestamp() # Not enough time has passed if time_delta < cooldown: # content_backup = message.content # TODO: Consider sending the user a content backup? await message.delete() wait_for = utilities.get_time_string(cooldown - time_delta, text=True, full=True) warning = ('You cannot send another advertisement at this time. ' 'You must wait {}.').format(wait_for) await message.author.send(embed=discord.Embed( colour=discord.Colour(0xffcc4d), description=warning)) return # Enough time has passed - delete the last message elif author_id in advertisement_data: try: await advertisement_data[author_id].delete() except: # User deleted their advertisement already logger.warn("Failed to delete the last advertisement.") # Schedule a notification for when a new advertisement post is eligible utilities.schedule(bot, __name__, time.time() + cooldown, _notify_advertisement_available, search='c_ad_{}'.format(message.guild.id), destination='u{}'.format(author_id), info='Commission advertisement post eligibility.') advertisement_data[author_id] = message notification = ( 'Hello! Your advertisement post in the commissions channel has been recorded. ' '**Please remember that there can only be one message per advertisement**.\n\n' 'If you want to revise your advertisement [(like adding an image)]' '(https://imgur.com/a/qXB2v "Click here for a guide on how to add an image ' 'with a message"), you can delete your advertisement and submit it again, ' 'although this only works within the next 10 minutes and if nobody else has ' 'posted another advertisement after yours.\n\nYou are eligible to post a ' 'new advertisement after the waiting period of {}. When you post a new ' 'advertisement, your previous one will be automatically deleted.\n\n' 'For convenience, you will be notified when you are eligible to make ' 'a new post.').format( utilities.get_time_string(cooldown, text=True, full=True)) await message.author.send(embed=discord.Embed( colour=discord.Colour(0x77b255), description=notification))
async def mod_wrapper(bot, message, base, blueprint_index, options, arguments, keywords, cleaned_content): response, tts, message_type, extra = ('', False, 0, None) mod_action = '' if blueprint_index == 0: # info server_data = data.get(bot, 'base', None, server_id=message.server.id, default={}) disabled_commands = server_data.get('disabled', []) display_list = [] for disabled_command in disabled_commands: display_list.append('{0} ({1})'.format( disabled_command[0], 'all' if disabled_command[1] == -1 else disabled_command[1] + 1)) response = ('```\n' 'Information for server {0}\n' 'ID: {0.id}\n' 'Owner: {0.owner.id}\n' 'Moderators: {1}\n' 'Blocked users: {2}\n' 'Muted: {3}\n' 'Muted channels: {4}\n' 'Command invoker: {5}\n' 'Mention mode: {6}\n' 'Disabled commands: {7}```').format( message.server, server_data.get('moderators', []), server_data.get('blocked', []), server_data.get('muted', []), server_data.get('muted_channels', []), server_data.get('command_invoker', None), server_data.get('mention_mode', False), display_list) elif blueprint_index == 1: # Toggle command try: # Explicit index split_arguments = arguments[0].split() command = bot.commands[split_arguments[0]] guess = [command.base, int(split_arguments[1]) - 1] assert -1 < guess[1] < len(command.blueprints) except IndexError: # No index guess = [command.base, -1] except: # Guess the help index guess = list(parser.guess_index(bot, arguments[0])) if guess[0] is None: raise BotException(EXCEPTION, "Invalid base.") command = bot.commands[guess[0]] if command.plugin is bot.commands['base'].plugin: raise BotException(EXCEPTION, "The base commands cannot be disabled.") pass_in = (bot, 'base', 'disabled', guess) pass_in_keywords = {'server_id': message.server.id} disabled_commands = data.get(*pass_in[:-1], **pass_in_keywords, default=[]) if guess in disabled_commands: data.list_data_remove(*pass_in, **pass_in_keywords) response = "Enabled" else: data.list_data_append(*pass_in, **pass_in_keywords) response = "Disabled" response += " the `{0}` command {1}.".format( guess[0], "and all associated subcommands" if guess[1] == -1 else "(subcommand {})".format(guess[1] + 1)) mod_action = response elif blueprint_index in (2, 3): # Block or unblock user = data.get_member(bot, arguments[0], message.server) block = blueprint_index == 2 mod_action = 'Blocked {}' if block else 'Unblocked {}' mod_action = mod_action.format('{0} ({0.id})'.format(user)) blocked = data.is_blocked(bot, message.server, user.id, strict=True) mod = data.is_mod(bot, message.server, user.id) if mod: raise BotException(EXCEPTION, "Cannot block or unblock a moderator.") elif block: if blocked: raise BotException(EXCEPTION, "User is already blocked.") else: data.list_data_append(bot, 'base', 'blocked', user.id, server_id=message.server.id) response = "User is now blocked." else: if not blocked: raise BotException(EXCEPTION, "User is already unblocked.") else: data.list_data_remove(bot, 'base', 'blocked', user.id, server_id=message.server.id) response = "User is now unblocked." elif blueprint_index == 4: # Clear response = ('' + '\n' * 80 + "The chat was pushed up by a bot moderator.") elif blueprint_index in (5, 6): # Mute or unmute server_id = message.server.id mute = blueprint_index == 5 mod_action = 'Muted {}' if mute else 'Unmuted {}' if arguments[0]: channel = data.get_channel(bot, arguments[0], message.server) muted = channel.id in data.get(bot, 'base', 'muted_channels', server_id=server_id, default=[]) mod_action = mod_action.format(channel.name) if mute: if muted: raise BotException(EXCEPTION, "Channel is already muted.") else: data.list_data_append(bot, 'base', 'muted_channels', channel.id, server_id=server_id) if str(channel.type) == 'voice': # disconnect await utilities.leave_and_stop(bot, message.server) response = "Channel muted." else: # unmute if not muted: raise BotException(EXCEPTION, "Channel is already unmuted.") else: data.list_data_remove(bot, 'base', 'muted_channels', channel.id, server_id=server_id) response = "Channel unmuted." else: # server mod_action = mod_action.format('the server') muted = data.get(bot, 'base', 'muted', server_id=server_id, default=False) if not (muted ^ mute): response = "Server is already {}muted.".format( '' if muted else 'un') raise BotException(EXCEPTION, response) else: data.add(bot, 'base', 'muted', mute, server_id=server_id) response = "Server {}muted.".format('' if mute else 'un') elif blueprint_index == 7: # Invoker if len(arguments[0]) > 10: raise BotException( EXCEPTION, "The invoker can be a maximum of 10 characters long.") data.add(bot, 'base', 'command_invoker', arguments[0] if arguments[0] else None, server_id=message.server.id) response = "Custom command invoker {}.".format( 'set' if arguments[0] else 'cleared') if arguments[0]: response = "Custom command invoker set." mod_action = "Set the server command invoker to '{}'.".format( arguments[0]) else: response = "Custom command invoker cleared." mod_action = "Removed the custom command invoker." elif blueprint_index == 8: # Mention current_mode = data.get(bot, 'base', 'mention_mode', server_id=message.server.id, default=False) data.add(bot, 'base', 'mention_mode', not current_mode, server_id=message.server.id) response = "Mention mode {}activated.".format( 'de' if current_mode else '') mod_action = "{}activated mention mode.".format( 'de' if current_mode else '').capitalize() # Send notification if configured send_notifications = data.get(bot, 'base', 'notifications', server_id=message.server.id, default=True) if mod_action and send_notifications: if message.edited_timestamp: timestamp = message.edited_timestamp else: timestamp = message.timestamp notification = ('Moderator {0} ({0.id}) from {0.server} on {1}:\n\t' '{2}').format(message.author, timestamp, mod_action) logs = await utilities.get_log_text(bot, message.channel, limit=20, before=message) logs += '\n{}'.format(utilities.get_formatted_message(message)) await bot.send_message(message.server.owner, notification) await utilities.send_text_as_file(bot, message.server.owner, logs, 'context') return (response, tts, message_type, extra)
async def base_wrapper(bot, message, base, blueprint_index, options, arguments, keywords, cleaned_content): response, tts, message_type, extra = ('', False, 0, None) if blueprint_index == 0: # version response = '`{}`\n{}'.format(bot.version, bot.date) elif blueprint_index == 1: # source response = random.choice([ "It's shit. I'm sorry.", "You want to see what the Matrix is like?", "Script kiddie level stuff in here.", "Beware the lack of PEP 8 guidelines inside!", "Snarky comments inside and out.", "Years down the road, this will all just be a really " "embarrassing but funny joke.", "Made with ~~love~~ pure hatred.", "At least he's using version control.", "Yes, I know I'm not very good. Sorry...", "Take it easy on me, okay?", "You're going to groan. A lot.", "You might be better off *not* looking inside." ]) response += ("\nhttps://github.com/jkchen2/JshBot\n" "https://github.com/jkchen2/JshBot-plugins") elif blueprint_index == 2: # uptime uptime_total_seconds = int(time.time()) - bot.time uptime_struct = time.gmtime(uptime_total_seconds) days = int(uptime_total_seconds / 86400) hours = uptime_struct.tm_hour minutes = uptime_struct.tm_min seconds = uptime_struct.tm_sec response = ("The bot has been on since **{0}**\n{1} days, {2} hours, " "{3} minutes, and {4} seconds").format( bot.readable_time, days, hours, minutes, seconds) elif blueprint_index == 3: # Announcement announcement = data.get(bot, 'base', 'announcement') if not announcement: response = "No announcement right now!" else: response = announcement elif blueprint_index == 4: # Invite if bot.selfbot: raise BotException(EXCEPTION, "Nope.") response_list = [] if 'details' in options: for plugin in bot.plugins.keys(): permission_items = data.get(bot, plugin, 'permissions', volatile=True, default={}).items() if permission_items: response_list.append('***`{}`***'.format(plugin)) response_list.append('\t' + '\n\t'.join([ '**`{0[0]}`** -- {0[1]}'.format(item) for item in permission_items ]) + '\n') permissions_number = utilities.get_permission_bits(bot) app_id = (await bot.application_info()).id response_list.append( 'https://discordapp.com/oauth2/authorize?&client_id={0}' '&scope=bot&permissions={1}\n**Remember: you must have the ' '"Administrator" role on the server you are trying to add the ' 'bot to.**'.format(app_id, permissions_number)) response = '\n'.join(response_list) elif blueprint_index in (5, 6): # Join/leave voice channel if message.channel.is_private: raise BotException( EXCEPTION, "This command cannot be used in direct messages.") voice_channel = message.author.voice_channel if not voice_channel: raise BotException(EXCEPTION, "You are not in a voice channel.") try: if blueprint_index == 5: await utilities.join_and_ready(bot, voice_channel, reconnect=True, is_mod=data.is_mod( bot, message.server, message.author.id)) response = "Joined {}.".format(voice_channel.name) else: await utilities.leave_and_stop(bot, message.server, member=message.author, safe=False) response = "Left {}.".format(voice_channel.name) except BotException as e: raise e # Pass up except Exception as e: action = 'join' if blueprint_index == 5 else 'leave' raise BotException( EXCEPTION, "Failed to {} the voice channel.".format(action), e=e) return (response, tts, message_type, extra)
async def setup_globals(bot): global WEBHOOK_SET, TAG_CONVERTER TAG_CONVERTER = bot.plugins['tags.py'].TagConverter( apply_checks=True, voice_channel_bypass=True) WEBHOOK_SET = set(data.get(bot, __name__, 'webhooks', default=[]))
async def owner_wrapper(bot, message, base, blueprint_index, options, arguments, keywords, cleaned_content): response, tts, message_type, extra = ('', False, 0, None) mod_action = '' send_notifications = data.get(bot, 'base', 'notifications', server_id=message.server.id, default=True) if blueprint_index in (0, 1): # Add or remove moderator user = data.get_member(bot, arguments[0], server=message.server) user_is_mod = data.is_mod(bot, message.server, user.id, strict=True) user_is_elevated = data.is_mod(bot, message.server, user.id) blocked = data.is_blocked(bot, message.server, user.id, strict=True) mod_action = 'Added {}' if blueprint_index == 0 else 'Removed {}' mod_action = mod_action.format( '{0} ({0.id}) as a moderator'.format(user)) if blocked: raise BotException(EXCEPTION, "User is blocked.") elif blueprint_index == 0: # add if user_is_mod or user_is_elevated: raise BotException(EXCEPTION, "User is already a moderator.") else: data.list_data_append(bot, 'base', 'moderators', user.id, server_id=message.server.id) response = "User is now a moderator." else: # remove if not user_is_mod: raise BotException(EXCEPTION, "User is not in the moderators list.") else: data.list_data_remove(bot, 'base', 'moderators', user.id, server_id=message.server.id) response = "User is no longer a moderator." elif blueprint_index == 2: # Send feedback if data.get(bot, 'base', 'feedbackdisabled', default=False): response = ("Feedback has been temporarily disabled, probably " "due to some troll spammers.") else: text = arguments[0] if len(text) > 1500: raise BotException( EXCEPTION, "Whoa! That's a lot of feedback. " "1500 characters or fewer, please.") text = ('{0} ({0.id}) on {1.timestamp}:' '\n\t{2}').format(message.author, message, text) await utilities.notify_owners(bot, text, user_id=message.author.id) response = "Message sent to bot owners." elif blueprint_index == 3: # Toggle notifications response = ("Bot moderator activity notifications are now turned " "{}").format("OFF." if send_notifications else "ON.") data.add(bot, 'base', 'notifications', not send_notifications, server_id=message.server.id) # Send notification if configured if mod_action and send_notifications: if message.edited_timestamp: timestamp = message.edited_timestamp else: timestamp = message.timestamp notification = 'From {0.server} on {1}, you:\n\t{2}'.format( message.author, timestamp, mod_action) logs = await utilities.get_log_text(bot, message.channel, limit=20, before=message) logs += '\n{}'.format(utilities.get_formatted_message(message)) await bot.send_message(message.server.owner, notification) await utilities.send_text_as_file(bot, message.server.owner, logs, 'context') return (response, tts, message_type, extra)
def _get_current_game(bot, guild_id): latest_index = _update_current_game(bot) schedule_data = data.get(bot, __name__, 'schedule', volatile=True) return _embed_games_information(bot, [schedule_data[latest_index]], guild_id)
async def _update_schedule(bot): """Reads the GDQ schedule and updates the information in the database.""" schedule_url = configurations.get(bot, __name__, 'schedule_url') html_data = (await utilities.future(requests.get, schedule_url)).text soup = BeautifulSoup(html_data, 'html.parser') run_table = soup.find('table', {'id': 'runTable'}) schedule_data = [] game_list = [] if run_table is None: raise CBException('Run table not found!') debug_weeks = data.get(bot, __name__, 'debug_weeks', default=0, volatile=True) current_data = {} for entry in run_table.find_all('tr'): entry_class = entry.get('class', [''])[0] if entry_class == 'day-split': continue subentries = [subentry.text for subentry in entry.find_all('td')] if entry_class == 'second-row': # Extra data for the last game estimation, run_type = subentries split_estimate = estimation.split(':') estimation_seconds = (int(split_estimate[0]) * 3600 + int(split_estimate[1]) * 60 + int(split_estimate[2])) end_time = (current_data['scheduled'] + datetime.timedelta( seconds=(estimation_seconds + current_data['setup_seconds']))) key_name = utilities.get_cleaned_filename(current_data['game'] + run_type, cleaner=True) current_data.update({ 'estimation': estimation.strip(), 'seconds': estimation_seconds, 'type': run_type, 'end': end_time, 'key': key_name }) game_list.append(key_name) schedule_data.append(current_data) else: # Happens first while len(subentries) < 4: subentries.append('') start_time_string, game, runners, setup_time = subentries start_time = datetime.datetime.strptime(start_time_string, '%Y-%m-%dT%H:%M:%SZ') setup_time = setup_time.strip() split_setup = setup_time.split(':') if len(split_setup) > 1: setup_seconds = (int(split_setup[0]) * 3600 + int(split_setup[1]) * 60 + int(split_setup[2])) else: setup_seconds = 0 current_data = { 'scheduled': start_time - datetime.timedelta(weeks=debug_weeks), 'game': game, 'runners': runners, 'setup': setup_time, 'setup_seconds': setup_seconds } # Add finale entry run_type = 'Party%' end_time = current_data['scheduled'] + datetime.timedelta(minutes=30) key_name = utilities.get_cleaned_filename(current_data['game'] + run_type, cleaner=True) current_data.update({ 'estimation': '0:30:00', 'seconds': 60 * 30, 'end': end_time, 'type': run_type, 'key': key_name }) game_list.append(key_name) schedule_data.append(current_data) # Update scheduled notifications entries = utilities.get_schedule_entries(bot, __name__) for entry in entries: payload, key = entry[3:5] if key not in game_list: # Not found error error_message = ( ":warning: Warning: The game {} has been removed, renamed, or " "recategorized. You have been removed from the notification list " "for this game. Please check the schedule at {}.".format( payload['text'], configurations.get(bot, __name__, 'schedule_url'))) utilities.update_schedule_entries(bot, __name__, search=key, payload={'error': error_message}, time=time.time()) else: game = schedule_data[game_list.index(key)] start_time, end_time = game['scheduled'], game['end'] setup_delta = datetime.timedelta(seconds=game['setup_seconds']) scheduled = start_time.replace( tzinfo=datetime.timezone.utc).timestamp() current_time = datetime.datetime.utcnow() if start_time + setup_delta < current_time < end_time: stream_url = configurations.get(bot, __name__, 'stream_url') payload = { 'error': ("Uh oh. The schedule shifted drastically and I didn't notice " "fast enough - sorry! {} is live right now at {}").format( payload['text'], stream_url) } else: payload.update({'end': scheduled + game['seconds']}) utilities.update_schedule_entries(bot, __name__, search=key, payload=payload, time=scheduled) # Save data data.add(bot, __name__, 'schedule', schedule_data, volatile=True) try: _update_current_game(bot) except: pass
async def get_response(bot, context): response = Response() use_plugin = configurations.get(bot, __name__, key='enable') if not use_plugin: response.content = ( "The GDQ plugin is currently disabled. (GDQ is probably over or hasn't started yet)" ) return response embed_template = discord.Embed( title='Games Done Quick', url='https://gamesdonequick.com/', colour=discord.Colour(0x00aff0), description='\[ [Stream]({}) ] \[ [Schedule]({}) ] \[ [Donate]({}) ]'. format(configurations.get(bot, __name__, 'stream_url'), configurations.get(bot, __name__, 'schedule_url'), configurations.get(bot, __name__, 'donate_url'))) embed_template.set_thumbnail(url='http://i.imgur.com/GcdqhUR.png') guild_id = context.guild.id if context.guild else None if context.index == 0: embed_template.add_field(name='Donation stats', value='Loading...', inline=False) response.game_index = data.get(bot, __name__, 'current_index', volatile=True, default=0) schedule_data = data.get(bot, __name__, 'schedule', volatile=True) games_list = schedule_data[response.game_index:response.game_index + 5] game_data = _embed_games_information(bot, games_list, guild_id) value = '\n\n'.join('**{}**\n{}'.format(*it) for it in game_data) embed_template.add_field(name='Schedule', value=value, inline=False) response.update_stats = True response.update_task = None response.message_type = MessageTypes.INTERACTIVE response.extra_function = gdq_menu response.extra = {'buttons': ['⬅', '⏺', '➡']} elif context.index == 1: # About embed_template.add_field( name='About', value= ("Games Done Quick (GDQ) is a week-long charity gaming marathon that " "brings together speedrunners from around the globe to raise money on a " "livestream. They are currently supporting {0}, and all donations go " "directly to the charity.\n\nCheck out the links above for the Twitch " "stream, games schedule, and the donation portal!").format( configurations.get(bot, __name__, 'charity'))) elif context.index == 2: # Status status_text = await _get_status(bot) embed_template.add_field(name='Status', value=status_text, inline=False) elif context.index == 3: # Current game embed_data = _get_current_game(bot, guild_id)[0] embed_template.add_field(name=embed_data[0], value=embed_data[1], inline=False) elif context.index == 4: # Next game(s) embed_data = _get_next_games(bot, context.arguments[0], guild_id) for name, value in embed_data: embed_template.add_field(name=name, value=value, inline=False) elif context.index == 5: # Search embed_data = _search_games(bot, context.arguments[0], guild_id=guild_id) embed_template.add_field(name=embed_data[0], value=embed_data[1], inline=False) elif context.index == 6: # Notify game = _search_games(bot, context.arguments[0], return_game=True) response.content = _toggle_notification(bot, game, context, use_channel='channel' in context.options) embed_template = None response.embed = embed_template return response
async def check_recently_deleted(bot, message): """Checks if a user wants to revise their last advertisement.""" if isinstance(message.channel, discord.abc.PrivateChannel): return guild_data = data.get(bot, __name__, None, guild_id=message.guild.id, default={}) if (not guild_data.get('rules') or message.channel.id != guild_data['rules']['channel'] or message.author.id in guild_data.get('whitelist', []) or message.author.bot): return cooldown = guild_data['rules']['cooldown'] message_time = message.created_at.replace(tzinfo=tz.utc).timestamp() advertisement_data = await _get_advertisement_data(bot, message.guild) author_id = message.author.id # Message mismatch. Ignore deletion if advertisement_data.get(author_id) != message: return # User wants to replace their last message (limit within 10 minutes) future_message = await message.channel.history(limit=1, after=message).flatten() time_delta = time.time() - message_time if not future_message and time_delta < 60: del advertisement_data[author_id] utilities.remove_schedule_entries(bot, __name__, search='c_ad_{}'.format( message.guild.id), destination='u{}'.format(author_id)) notification = ( 'Heads up, you have deleted your last advertisement within 10 minutes of posting it ' '(and nobody else posted an advertisement during that time).\n\n' 'You can submit a revised advertisement now if you wish.') await message.author.send(embed=discord.Embed(description=notification) ) # User deleted their advertisement for some reason? # Keep message creation time to prevent users from circumventing the cooldown elif time_delta < cooldown: deleted_persistence = data.get(bot, __name__, 'recently_deleted', guild_id=message.guild.id, default={}) # Clear any expired entries to_remove = [ k for k, v in deleted_persistence.items() if time.time() - v > cooldown ] for remove_id in to_remove: del deleted_persistence[str(remove_id)] # Add persistence entry deleted_persistence[str(author_id)] = message_time data.add(bot, __name__, 'recently_deleted', deleted_persistence, guild_id=message.guild.id)
async def set_status_on_boot(bot): """Checks to see if the status was set previously.""" previous_status = data.get(bot, __name__, 'status') if previous_status: await bot.change_presence(activity=discord.Game(name=previous_status)) logger.info("Detected old status - setting it now!")