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 commission_configure(bot, context): """Configures the channel and cooldown for the commission channel rules.""" rules = data.get(bot, __name__, 'rules', guild_id=context.guild.id, default={}) default_cooldown = configurations.get(bot, __name__, 'default_cooldown') replace_channel = context.options.get('channel', rules.get('channel')) replace_cooldown = context.options.get( 'cooldown', rules.get('cooldown', default_cooldown)) if not replace_channel: raise CBException("No commission channel configured.") # Reset advertisement data rules = {'channel': replace_channel, 'cooldown': replace_cooldown} data.add(bot, __name__, 'rules', rules, guild_id=context.guild.id) data.remove(bot, __name__, 'advertisements', guild_id=context.guild.id, volatile=True, safe=True) await _get_advertisement_data(bot, context.guild) description = 'Channel: {0.mention}\nCooldown: {1}'.format( data.get_channel(bot, replace_channel), utilities.get_time_string(replace_cooldown, text=True, full=True)) embed = discord.Embed(title='Commission channel configuration:', description=description) return Response(embed=embed)
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
def set_player(bot, server_id, player): """Sets the voice player of the given server.""" data.add(bot, 'base', 'voice_player', player, server_id=server_id, volatile=True)
def set_player(bot, guild_id, player): """Sets the voice player of the given guild.""" data.add(bot, 'core', 'voice_player', player, guild_id=guild_id, volatile=True)
async def create_client(bot): """Create a new wolframalpha client object and store in volatile data.""" config = configurations.get(bot, __name__) client = wap.WolframAlphaEngine(config['api_key'], config['server']) client.ScanTimeout = config['scan_timeout'] client.PodTimeout = config['pod_timeout'] client.FormatTimeout = config['format_timeout'] data.add(bot, __name__, 'client', client, volatile=True) data.add(bot, __name__, 'uses', {}, volatile=True)
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_setchannel(bot, context): """Sets the channel where logs will be dumped.""" data.add(bot, __name__, 'log_channel', context.arguments[0].id, guild_id=context.guild.id) return Response("The logging channel is now set to {}".format( context.arguments[0].mention))
async def translate_default(bot, context): """Sets the default translation language.""" language = context.arguments[0] if language: data.add(bot, __name__, 'default', language, guild_id=context.guild.id) else: data.remove(bot, __name__, 'default', guild_id=context.guild.id) return Response( content='Default language set to {}.'.format(language if language else 'English'))
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]))
async def verification_set(bot, context): """Sets the verification role.""" role = context.arguments[0] data.add(bot, __name__, 'verification_role', role.id, guild_id=context.guild.id) return Response(embed=discord.Embed( description='Verification role set to {}'.format(role.mention)))
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.')
async def get_response(bot, context): if context.index == 0: # Change avatar, status, or both if len(context.options) == 0: raise CBException( "Either the avatar, status, or both options must be used.") if 'avatar' in context.options: text = configurations.get(bot, __name__, extra='avatars', extension='txt') url = random.choice(text.splitlines()).rstrip() await _change_avatar(bot, url=url) if 'status' in context.options: text = configurations.get(bot, __name__, extra='statuses', extension='txt') status = random.choice(text.splitlines()).rstrip() try: await bot.change_presence(activity=discord.Game(name=status)) except Exception as e: raise CBException("Failed to update the status.", e=e) elif context.index == 1: # Change nickname try: await context.guild.me.edit(nick=context.arguments[0]) except Exception as e: raise CBException("Failed to change the nickname.", e=e) elif context.index == 2: # Change name if len(context.arguments[0]) > 20: raise CBException("Name is longer than 20 characters.") try: await bot.user.edit(username=context.arguments[0]) except Exception as e: raise CBException("Failed to update the name.", e=e) elif context.index == 3: # Change status try: if context.arguments[0]: activity = discord.Game(name=context.arguments[0]) else: activity = None await bot.change_presence(activity=activity) data.add(bot, __name__, 'status', context.arguments[0]) except Exception as e: raise CBException("Failed to update the status.", e=e) elif context.index == 4: # Change avatar await _change_avatar(bot, url=context.arguments[0]) return Response(content="Bot updated.")
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)
async def _get_advertisement_data(bot, guild, ignore_user_id=None): """Gets a dictionary of advertisements in the guild, or builds one if necessary. If ignore_user_id is provided, this will ignore the first message by that user. """ rules = data.get(bot, __name__, 'rules', guild_id=guild.id) if not rules: raise CBException( "Commission channel rules are not configured on this server.") advertisement_data = data.get(bot, __name__, 'advertisements', guild_id=guild.id, volatile=True) if advertisement_data: return advertisement_data # No data found. Fetch it manually channel = data.get_channel(bot, rules['channel'], safe=True) if not channel: raise CBException("The commission channel was not found.") # TODO: Add permission checks for channel access and deleting messages advertisement_data = {} whitelist = data.get(bot, __name__, 'whitelist', guild_id=guild.id, default=[]) async for message in channel.history(limit=100): author_id = message.author.id if (not message.author.bot and message.type is discord.MessageType.default and not message.pinned and author_id not in whitelist): if author_id in advertisement_data: logger.warn('Deleting previously undetected message %s', message.id) await message.delete() else: if ignore_user_id == author_id: ignore_user_id = None else: advertisement_data[author_id] = message data.add(bot, __name__, 'advertisements', advertisement_data, guild_id=guild.id, volatile=True) return advertisement_data
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 _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 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", 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)
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
async def upload_to_discord(bot, fp, filename=None, rewind=True, close=False): """Uploads the given file-like object to the upload channel. If the upload channel is specified in the configuration files, files will be uploaded there. Otherwise, a new server will be created, and used as the upload channel.""" channel_id = configurations.get(bot, 'core', 'upload_channel') if not channel_id: # Check to see if a server was already created channel_id = data.get(bot, 'core', 'upload_channel') channel = bot.get_channel(channel_id) if channel is None: # Create server logging.debug("Creating server for upload channel...") try: server = await bot.create_server('uploads') except Exception as e: raise BotException( EXCEPTION, "Failed to create upload server. This bot is not whitelisted " "to create servers.", e=e) data.add(bot, 'core', 'upload_channel', server.id) channel = bot.get_channel(server.id) if channel is None: # Shouldn't happen raise BotException(EXCEPTION, "Failed to get upload channel.") try: message = await bot.send_file(channel, fp, filename=filename) upload_url = message.attachments[0]['url'] except Exception as e: raise BotException(EXCEPTION, "Failed to upload file.", e=e) if close: try: fp.close() except: pass elif rewind: try: fp.seek(0) except: pass return upload_url
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]))
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.')
async def upload_to_discord(bot, fp, filename=None, rewind=True, close=False): """Uploads the given file-like object to the upload channel. If the upload channel is specified in the configuration files, files will be uploaded there. Otherwise, a new guild will be created, and used as the upload channel.""" channel_id = configurations.get(bot, 'core', 'upload_channel') if not channel_id: # Check to see if a guild was already created channel_id = data.get(bot, 'core', 'upload_channel') channel = data.get_channel(bot, channel_id, safe=True) # TODO: Remove. Guild creation via bots is a whitelisted process if channel is None: # Create guild logger.debug("Creating guild for upload channel...") try: guild = await bot.create_guild('uploads') except Exception as e: raise CBException( "Failed to create upload guild. This bot is not whitelisted " "to create guilds.", e=e) data.add(bot, 'core', 'upload_channel', guild.id) channel = bot.get_channel(guild.id) if channel is None: # Shouldn't happen raise CBException("Failed to get upload channel.") try: discord_file = discord.File(fp, filename=filename) message = await channel.send(file=discord_file) upload_url = message.attachments[0].url except Exception as e: raise CBException("Failed to upload file.", e=e) try: if close: fp.close() elif rewind: fp.seek(0) except: pass return upload_url
async def upload_to_discord(bot, fp, filename=None, rewind=True, close=False): """Uploads the given file-like object to the upload channel. If the upload channel is specified in the configuration files, files will be uploaded there. Otherwise, a new guild will be created, and used as the upload channel.""" channel_id = configurations.get(bot, 'core', 'upload_channel') if not channel_id: # Check to see if a guild was already created channel_id = data.get(bot, 'core', 'upload_channel') channel = data.get_channel(bot, channel_id, safe=True) if channel is None: # Create guild logger.debug("Creating guild for upload channel...") try: guild = await bot.create_guild('uploads') except Exception as e: raise CBException( "Failed to create upload guild. This bot is not whitelisted " "to create guilds.", e=e) data.add(bot, 'core', 'upload_channel', guild.id) channel = bot.get_channel(guild.id) if channel is None: # Shouldn't happen raise CBException("Failed to get upload channel.") try: discord_file = discord.File(fp, filename=filename) message = await channel.send(file=discord_file) upload_url = message.attachments[0].url except Exception as e: raise CBException("Failed to upload file.", e=e) try: if close: fp.close() elif rewind: fp.seek(0) except: pass return upload_url
async def tagremote_start(bot, context): """Starts a tag remote session.""" # Check for an existing session session_data = data.get(bot, __name__, 'data', guild_id=context.guild.id) if session_data: raise CBException("Session already exists.") if not context.channel.permissions_for(context.guild.me).manage_webhooks: raise CBException("Missing the `Manage Webhooks` permission.") # Retrieve and format tag data tag_dictionary = _get_tag_dictionary(bot, context.guild) # Check that the user is in an unblocked voice channel if not context.author.voice: raise CBException("You must be in a voice channel.") voice_channel = context.author.voice.channel await utilities.join_and_ready(bot, voice_channel, is_mod=context.elevation >= 1) # Create webhook webhook = await context.channel.create_webhook(name='Tag Remote []') # Upload session data session_code = await _upload_session_data( bot, context.channel, voice_channel, webhook, tag_dictionary) # Track session data session_data = { 'webhook': webhook.id, 'channel': context.channel.id, 'voice_channel': voice_channel.id, 'session': session_code } data.add(bot, __name__, 'data', session_data, guild_id=context.guild.id) data.list_data_append(bot, __name__, 'webhooks', webhook.id, duplicates=False) WEBHOOK_SET.add(webhook.id) return await tagremote(bot, context)
async def botowner_wrapper(bot, message, base, blueprint_index, options, arguments, keywords, cleaned_content): response, tts, message_type, extra = ('', False, 0, None) if blueprint_index == 0: # Halt await bot.send_message(message.channel, "Going down...") bot.shutdown() elif blueprint_index == 1: # Restart await bot.send_message(message.channel, "Restarting...") bot.restart() elif blueprint_index == 2: # Reload response = "Reloading..." message_type = 3 extra = ('reload', ) elif blueprint_index == 3: # IP s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('8.8.8.8', 80)) # Thanks Google ip = s.getsockname()[0] s.close() response = "Local IP: " + ip elif blueprint_index == 4: # Backup utilities.make_backup(bot) await bot.send_file(message.channel, '{}/temp/backup1.zip'.format(bot.path)) elif blueprint_index == 5: # Blacklist blacklist = data.get(bot, 'base', 'blacklist', default=[]) if not arguments[0]: response = "Blacklisted entries: {}".format(blacklist) else: user_id = arguments[0] if user_id in blacklist: data.list_data_remove(bot, 'base', 'blacklist', user_id) response = "User removed from blacklist." else: data.list_data_append(bot, 'base', 'blacklist', user_id) response = "User added to blacklist." elif blueprint_index == 6: # Toggle feedback status = data.get(bot, 'base', 'feedbackdisabled', default=False) action = "enabled" if status else "disabled" data.add(bot, 'base', 'feedbackdisabled', not status) response = "Feedback has been {}.".format(action) elif blueprint_index == 7: # Announcement if arguments[0]: text = '{0}:\n{1}'.format(time.strftime('%c'), arguments[0]) data.add(bot, 'base', 'announcement', text) response = "Announcement set!" else: data.add(bot, 'base', 'announcement', '') response = "Announcement cleared!" return (response, tts, message_type, extra)
async def _create_session(bot, owner, editing=None): """Creates a session for character creation or editing""" webhook = await DATA_CHANNEL.create_webhook(name='ready:{}'.format(owner.id)) # Upload data as a single file cursor = data.db_select( bot, from_arg='characters', where_arg='owner_id=%s', input_args=[owner.id]) characters = cursor.fetchall() if cursor else [] create_data = utilities.get_text_as_file(json.dumps({ "version": DATA_VERSION, "webhook": [str(webhook.id), webhook.token], "existing_names": list(character.clean_name for character in characters), "editing": editing })) url = await utilities.upload_to_discord(bot, create_data, filename='data', close=True) url_segments = [it[::-1] for it in url[::-1].split('/')[2:0:-1]] # sorry session_code = '{}:{}'.format(*url_segments) # Track webhook usage data.add(bot, __name__, 'tracker', webhook, user_id=owner.id, volatile=True) data.add(bot, __name__, 'owner', owner, user_id=webhook.id, volatile=True) data.add(bot, __name__, 'stage', 0, user_id=webhook.id, volatile=True) # Add webhook ID to global IDs global DATA_CHANNEL_WEBHOOK_IDS DATA_CHANNEL_WEBHOOK_IDS.append(webhook.id) # Send the owner the link embed = discord.Embed( title='Click here to access the character creator', url='https://jkchen2.github.io/character-template/#{}'.format(session_code), description='Your session code is:\n`{}`'.format(session_code)) await owner.send(embed=embed) # Schedule a session timeout utilities.schedule( bot, __name__, time.time() + 7200, _session_timeout_notify, search=str(webhook.id), destination='u{}'.format(owner.id), info='Character creator session timeout') return session_code
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 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)
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 _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 autolog_setchannel(bot, context): """Sets the channel where logs will be dumped.""" data.add(bot, __name__, 'log_channel', context.arguments[0].id, guild_id=context.guild.id) return Response("The logging channel is now set to {}".format(context.arguments[0].mention))
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 _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 subentries[0].startswith(' '): # Second column subentries = subentries[:2] 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}, new_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, new_time=scheduled) # Save data data.add(bot, __name__, 'schedule', schedule_data, volatile=True) try: _update_current_game(bot) except: pass
async def _set_debug_weeks(bot, weeks): """Change the week delta programmatically for testing purposes.""" data.add(bot, __name__, 'debug_weeks', int(weeks), volatile=True) await _update_schedule(bot) return "Schedule updated."
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