class Nyoom(commands.Cog): def __init__(self, client): self.client = client self.client_extras = ClientTools(client) self.database_extras = DatabaseTools(self.client_extras) async def get_times(self, user_id=None): """ username : user you want to get messages for Returns: times: list of all timestamps of users messages """ if user_id is None: get_time = "SELECT `time` FROM `messages_detailed` ORDER BY TIME ASC" cursor.execute(get_time) else: get_time = "SELECT `time` FROM `messages_detailed` WHERE `user_id` = %s ORDER BY TIME ASC" cursor.execute(get_time, (user_id,)) timesA = cursor.fetchall() times = [] for time in timesA: times.append(time[0]) return times async def calculate_nyoom(self, output, user_id=None): # load interval between messages we're using from the configs interval = config['discord']['nyoom_interval'] times = await self.get_times(user_id=user_id) # group them into periods of activity periods = [] curPeriod = [times[0], times[0], 0] # begining of period, end of period, number of messages in period for time in times: if time > curPeriod[1] + datetime.timedelta(0, interval): # if theres more than a 10min dif between this time and last time # make a new period periods.append(curPeriod) curPeriod = [time, time, 1] else: curPeriod[1] = time # the period now ends with the most recent timestamp curPeriod[2] += 1 # add the message to the period # sum the total length of activity periods and divide by total number of messages totalT = 0 totalM = len(times) for period in periods: totalT += ((period[1] - period[ 0]).total_seconds() / 60) + 1 # total number of minutes for the activity period, plus a fudge factor to prevent single message periods from causing a divide by zero issue later totalT /= 60 # makes the total active time and nyoom_metric count hours of activity rather than minutes nyoom_metric = totalM / totalT # number of message per minute during periods of activity return totalM, totalT, nyoom_metric @commands.command() async def nyoom(self, ctx, user: discord.Member = None): """ Calculate the specified user's nyoom metric. e.g. The number of messages per hour they post while active (posts within 10mins of each other count as active) user : user to get nyoom metric for, if not author """ if user is None: user = ctx.message.author output = await ctx.send(strings['nyoom_calc']['status']['calculating'] + strings['emojis']['loading']) username = self.database_extras.opted_in(user_id=user.id) if not username: return await output.edit(content=output.content + '\n' + strings['nyoom_calc']['status']['not_opted_in']) # grab a list of times that user has posted totalM, totalT, nyoom_metric = await self.calculate_nyoom(output, user_id=user.id) await output.delete() embed = discord.Embed(title="Nyoom metrics", color=colours.blue) embed.add_field(name="Message count", value=totalM) embed.add_field(name="Total hours", value=totalT) embed.add_field(name="Nyoom metric", value=nyoom_metric) embed.set_footer(text="These values may not be 100% accurate") return await ctx.send(embed=embed) @commands.command(aliases=["nyoomserver", "ns", "n_s"]) async def nyoom_server(self, ctx): """ Calculate nyoom metric for entire server """ output = await ctx.send(strings['nyoom_calc']['status']['calculating'] + strings['emojis']['loading']) totalM, totalT, nyoom_metric = await self.calculate_nyoom(output) # prepare the final embed to send embed = discord.Embed(title="Nyoom metrics", color=colours.blue) embed.add_field(name="Message count", value=totalM) embed.add_field(name="Total hours", value=totalT) embed.add_field(name="Nyoom metric", value=nyoom_metric) embed.set_footer(text="These values may not be 100% accurate") await output.delete() return await ctx.send(embed=embed)
class Tagger(commands.Cog): def __init__(self, client): self.client = client self.client_extras = ClientTools(client) self.database_extras = DatabaseTools(self.client_extras) @commands.command(aliases=["t"]) async def tagger(self, ctx, nsfw: bool = False, selected_channel: discord.TextChannel = None): """ Generates tags for you based on what you talk about """ if (not ctx.message.channel.is_nsfw()) and nsfw: return await ctx.send(strings['tagger']['errors']['nsfw'].format( str(ctx.author))) output = await ctx.send(strings['tagger']['title'] + strings['emojis']['loading']) await output.edit(content=output.content + "\n" + strings['tagger']['status']['messages']) async with ctx.channel.typing(): username = self.database_extras.opted_in(user_id=ctx.author.id) if not username: return await output.edit( content=output.content + strings['tagger']['errors']['not_opted_in']) messages, channels = await self.database_extras.get_messages( ctx.author.id, config['limit']) text = [] text = await self.client_extras.build_messages( ctx, nsfw, messages, channels, selected_channel=selected_channel) text1 = "" for m in text: text1 += str(m) + "\n" await output.edit(content=output.content + strings['emojis']['success'] + "\n" + strings['tagger']['status']['analytical_data']) algo = algo_client.algo('nlp/AutoTag/1.0.1') await output.delete() response = algo.pipe(text1) tags = list(response.result) tag_str = "" for tag in tags: tag_str = "- " + tag + "\n" + tag_str em = await self.client_extras.markov_embed( "Tags for " + str(ctx.author), tag_str) output = await ctx.send(embed=em) emoji = await self.client_extras.get_delete_emoji() emoji = emoji[1] return await self.client_extras.delete_option(self.client, output, ctx, emoji)
class Admin(commands.Cog): def __init__(self, client): self.client = client self.database_tools = DatabaseTools(client) self.client_tools = ClientTools(client) @commands.group(hidden=True) async def debug(self, ctx): """Debug utilities for AGSE and Discord""" if ctx.invoked_subcommand is None: await ctx.send( "Invalid params. Run `help debug` to get all commands.") @is_server_allowed() @debug.command(aliases=["isprocessed", "processed"]) async def is_processed(self, ctx, user=None): """ Admin command used to check if a member has opted in """ if user is None: user = ctx.author.name msg = await ctx.send(strings['process_check']['status']['checking']) if not self.database_tools.opted_in(user=user): return await msg.edit( content=strings['process_check']['status']['not_opted_in']) return await ctx.edit( content=strings['process_check']['status']['opted_in']) @is_owner_or_admin() @debug.command(aliases=["dumproles"]) async def dump_roles(self, ctx): """ Dump all roles to a text file on the host """ to_write = "" for guild in self.client.guilds: to_write += "\n\n=== {} ===\n\n".format(str(guild)) for role in guild.roles: to_write += "{} : {}\n".format(role.name, role.id) roles = open("roles.txt", "w") roles.write(to_write) roles.close() em = discord.Embed(title="Done", description="Check roles.txt") await ctx.channel.send(embed=em) @debug.command(aliases=["lag"]) async def latency(self, ctx, detailed=None): detailed = bool(detailed) # this is a tuple, with [0] being the shard_id, and [1] being the latency latencies = self.client.latencies lowest_lag = latencies[0] highest_lag = latencies[0] sum = 0 for i in latencies: if i[1] < lowest_lag[1]: lowest_lag = i if i[1] > highest_lag[1]: highest_lag = i # could probably do this in a one liner, but may as well as we have to iterate anyway sum += i[1] avg = (sum / len(latencies)) embed = discord.Embed(title="Latency") # add specific information about latency embed.add_field(name="Avg", value="{}".format(str(avg))) embed.add_field(name="Lowest Latency", value="{} on shard {}".format(lowest_lag[1], lowest_lag[0])) embed.add_field(name="Highest Latency", value="{} on shard {}".format(highest_lag[1], highest_lag[0])) if detailed: embed.add_field(name="RawData", value=str(latencies)) return await ctx.channel.send(embed=embed) @debug.command(aliases=["role_id"]) async def roleid(self, ctx, role_name): for role in ctx.guild.roles: if role_name.lower() == role.name.lower(): return await ctx.send(role.id) return await ctx.send(embed=discord.Embed( title="Could not find role {}".format(role_name))) @is_server_allowed() @commands.group(aliases=["rolem"]) async def role_manage(self, ctx): """Manages AGSE roles (ping groups)""" if ctx.invoked_subcommand is None: await ctx.send( "Invalid params. Run `help rolem` to get all commands.") @role_manage.command() async def add(self, ctx, *, role_name): """Add a role. Note: by default, it isn't joinable""" if role_name[0] == '"' and role_name[-1] == '"': role_name = role_name[1:-1] role_check = get_role(ctx.guild.id, role_name) em = discord.Embed(title="Success", description="Created role {}".format(role_name), color=green) if role_check is not None: em = discord.Embed(title="Error", description="Role is already in the DB", color=red) else: query = "INSERT INTO `gssp`.`roles` (`role_name`, `guild_id`) VALUES (%s, %s);" cursor.execute(query, (role_name, ctx.guild.id)) cnx.commit() return await ctx.channel.send(embed=em) @role_manage.command(aliases=["remove"]) async def delete(self, ctx, *, role_name): """Deletes a role - cannot be undone!""" if role_name[0] == '"' and role_name[-1] == '"': role_name = role_name[1:-1] role_check = get_role(ctx.guild.id, role_name) em = discord.Embed(title="Success", description="Deleted role {}".format(role_name), color=green) if role_check is None: em = discord.Embed( title="Error", description="{} is not in the DB".format(role_name), color=red) else: query = "DELETE FROM `gssp`.`roles` WHERE `role_name` = %s AND `guild_id` = %s" cursor.execute(query, (role_name, ctx.guild.id)) cnx.commit() return await ctx.channel.send(embed=em) @role_manage.command(aliases=["togglepingable"]) async def pingable(self, ctx, *, role_name): """Change a role from not pingable to pingable or vice versa""" if role_name[0] == '"' and role_name[-1] == '"': role_name = role_name[1:-1] role = get_role(ctx.guild.id, role_name) if role is None: return await ctx.channel.send( embed=discord.Embed(title='Error', description='Could not find that role', color=red)) if role['is_pingable'] == 1: update_query = "UPDATE `gssp`.`roles` SET `is_pingable`='0' WHERE `role_id`=%s AND `guild_id` = %s;" text = "not pingable" else: update_query = "UPDATE `gssp`.`roles` SET `is_pingable`='1' WHERE `role_id`=%s AND `guild_id` = %s;" text = "pingable" cursor.execute(update_query, ( role['role_id'], ctx.guild.id, )) cnx.commit() await ctx.channel.send( embed=discord.Embed(title="SUCCESS", description="Set {} ({}) to {}".format( role['role_name'], role['role_id'], text), color=green)) @role_manage.command( aliases=["togglejoinable", "togglejoin", "toggle_join"]) async def joinable(self, ctx, *, role_name): """ Toggles whether a role is joinable """ if role_name[0] == '"' and role_name[-1] == '"': role_name = role_name[1:-1] role = get_role(ctx.guild.id, role_name) if role is None: em = discord.Embed( title="Error", description="Could not find role {}".format(role_name), color=red) return await ctx.channel.send(embed=em) if role['is_joinable'] == 1: update_query = "UPDATE `gssp`.`roles` SET `is_joinable`='0' WHERE `role_id`=%s;" text = "not joinable" else: update_query = "UPDATE `gssp`.`roles` SET `is_joinable`='1' WHERE `role_id`=%s;" text = "joinable" cursor.execute(update_query, (role['role_id'], )) em = discord.Embed(title="Success", description="Set {} ({} to {}".format( role['role_name'], role['role_id'], text), color=green) cnx.commit() await ctx.channel.send(embed=em) @is_owner_or_admin() @commands.group(aliases=["config"]) async def settings(self, ctx): """Manages settings of AGSE""" if ctx.invoked_subcommand is None: await ctx.send( "Invalid params. Run `help settings` to get all commands.") @settings.command(aliases=[ "resyncroles", "syncroles", "rolesync", "role_sync", "sync_roles" ]) async def resync_roles(self, ctx): """ Force refresh the roles in the database with the roles discord has. """ for guild in self.client.guilds: for role in guild.roles: if role.name != "@everyone": try: cursor.execute(insert_role, (role.id, role.name)) except mysql.connector.errors.IntegrityError: pass # this is designed to assist with migration, by moving old discord role members over to the new # system seamlessly member_ids = [] for member in role.members: member_ids.append(member.id) role_db = DbRole(role.id, role.name, 0, members=member_ids) role_db.save_members() cursor.execute(update_role, (emoji.demojize(role.name), role.id)) await ctx.send(embed=discord.Embed( title="Success", description="Resynced roles.", color=green)) @is_owner_or_admin() @settings.group(aliases=["permissions"]) async def perms(self, ctx): """Manages AGSE roles (ping groups)""" if ctx.invoked_subcommand is None: await ctx.send( "Run `help settings perms` to get info on subcommands") @perms.command() async def promote_role(self, ctx, role_id): """ Add a role to the list of allowed roles """ role = ctx.guild.get_role(int(role_id)) if role is None: return await ctx.send( embed=discord.Embed(title="Error", description="That role does not exist", color=red)) settings = guild_settings.get_settings(guild=ctx.guild) if role_id in settings['staff_roles']: return await ctx.send( embed=discord.Embed(title="Error", description="Role already has admin perms", color=red)) settings['staff_roles'].append(role_id) guild_settings.write_settings(settings) return await ctx.send(embed=discord.Embed( title="Success", description="Role {} added to admin list".format(role.name), color=green)) @perms.command() async def demote_role(self, ctx, role_id): role_id = int(role_id) role_to_remove = ctx.guild.get_role(int(role_id)) if role_to_remove is None: return await ctx.send( embed=discord.Embed(title="Error", description="That role does not exist", color=red)) settings = guild_settings.get_settings(guild=ctx.guild) if role_id in ctx.author.roles: # this means the user is removing a role that gives them perms users_permitted_roles = [ ] # list of roles that give user permission to run this for role in ctx.author.roles: for role_existing in settings['staff_roles']: if role_existing == role.id: users_permitted_roles.append(role) if len(users_permitted_roles) <= 1: return await ctx.send(embed=discord.Embed( title="Error", description= "You cannot remove a role that gives permissions without another role which has permissions to do so", color=red)) try: settings['staff_roles'].remove(str(role_id)) guild_settings.write_settings(settings) return await ctx.send(embed=discord.Embed( title="Success", description="Removed {} from permitted role list".format( role_to_remove.name), color=green)) except ValueError: return await ctx.send(embed=discord.Embed( title="Error", description= "That role does not exist in the permitted role list", color=red)) @is_owner_or_admin() @commands.command() async def sync(self, ctx): clone_target = self.client.get_guild( config['discord'].get("clone_server_target")) def generate_progress_embed(m_text, colour=yellow, url=None): em = discord.Embed( title="Server Clone Progress", description="Status: {text}".format(text=m_text), colour=colour) if url is not None: em.add_field(name="Invite link", value=url) return em guild = ctx.guild # we just need to now create an instant invite to *somewhere* on the server progress = await ctx.send(embed=generate_progress_embed( "Dumping existing data from {guild.name}".format(guild=guild))) channels = [] roles = [] def get_channel_position(old_id=None, new_id=None): if new_id is None and old_id is None: raise AttributeError for x in range(0, len(channels)): channel = channels[x] # the and is not None prevent us from returning whatever channel has None as an attribute if (channel.get("old_id") == old_id and old_id is not None) or (channel.get("new_id") == new_id and new_id is not None): return x return None def get_channel(old_id=None, new_id=None): position = get_channel_position(old_id=old_id, new_id=new_id) if position is None: return None return channels[position] def add_channel(old_channel, new_channel=None): to_append = (dict(old_id=old_channel.id, old_channel=old_channel)) if new_channel is None: to_append['new_id'] = None to_append['new_channel'] = None else: to_append['new_id'] = new_channel.id to_append['new_channel'] = new_channel channels.append(to_append) def set_new_channel(old_channel_id, new_channel): # we don't use the new_channel id, as not merged yet position = get_channel_position(old_id=old_channel_id) new_channel = get_channel_object_dict(new_channel) channels[position]['new_channel'] = new_channel channels[position]['new_id'] = new_channel['id'] def get_role_position(old_id=None, new_id=None): if new_id is None and old_id is None: return None # means we don't have to do unnecesary searches if old_id is not None: old_id = int(old_id) if new_id is not None: new_id = int(new_id) for x in range(0, len(roles)): role = roles[x] # the and is not None prevent us from returning whatever channel has None as an attribute if (role.get("old_id") == old_id and old_id is not None) or ( role.get("new_id") == new_id and new_id is not None): return x return None def get_role(old_id=None, new_id=None): position = get_role_position(old_id=old_id, new_id=new_id) if position is None: return None return roles[position] def add_role(old_role, new_role=None): to_append = (dict(old_id=old_role.id, old_role=old_role)) if new_role is None: to_append['new_id'] = None to_append['new_role'] = None else: to_append['new_id'] = new_role.id to_append['new_role'] = new_role roles.append(to_append) def set_new_role(old_role_id, new_role): # we don't use the new_role id, as not merged yet position = get_role_position(old_id=old_role_id) roles[position]['new_role'] = new_role roles[position]['new_id'] = new_role.id def get_role_object_dict(role): if type(role) == dict: return role # if already role, just return it return dict(id=role.id, name=role.name, permissions=role.permissions.value, colour=role.colour.value, hoist=role.hoist, mentionable=role.mentionable) def get_role_dicts(roles=roles): backup_roles = roles role_dicts = [] for role in roles: if role.get('old_role') is not None: role['old_role'] = get_role_object_dict(role['old_role']) if role.get('new_role') is not None: role['new_role'] = get_role_object_dict(role['new_role']) role_dicts.append(role) return role_dicts def get_channel_type(channel): if type(channel) == discord.channel.TextChannel: return "Text" if type(channel) == discord.channel.VoiceChannel: return "Voice" if type(channel) == discord.channel.CategoryChannel: return "Category" return "Unknown" def get_channel_object_dict(channel): if type(channel) == dict: return channel # already converted new_overwrites = [] overwrites = channel.overwrites for overwrite in overwrites: allow, deny = overwrite[1].pair() if type(overwrite[0]) == discord.role.Role: role = get_role(old_id=overwrite[0].id) if role is None: to_append = dict( grantee=dict(old_id=overwrite[0].id, type="Role")) else: to_append = dict(grantee=dict( old_id=role.get('old_id'), new_id=role.get('new_id'), old_name=role.get('old_role', dict()).get('name'), type="Role")) else: # user overwrite to_append = dict( grantee=dict(id=overwrite[0].id, type="User")) to_append['allow_permission'] = allow.value to_append['deny_permission'] = deny.value new_overwrites.append(to_append) to_return = dict(id=channel.id, name=channel.name, type=get_channel_type(channel), overwrites=new_overwrites, position=channel.position) if to_return['type'] != "Category": if channel.category is not None: to_return['category'] = get_channel_object_dict( channel.category) else: to_return['category'] = None if to_return['type'] == "Text": # do text to_return['topic'] = channel.topic to_return['slowmode_delay'] = channel.slowmode_delay to_return['nsfw'] = channel.nsfw elif to_return['type'] == "Voice": # do voice to_return['bitrate'] = channel.bitrate to_return['user_limit'] = channel.user_limit return to_return def get_channel_dicts(channels=channels): backup_channels = channels channel_dicts = [] for channel in channels: if channel.get('old_channel') is not None: channel['old_channel'] = get_channel_object_dict( channel['old_channel']) if channel.get('new_channel') is not None: channel['new_role'] = get_channel_object_dict( channel['new_channel']) channel_dicts.append(channel) channels = backup_channels return channel_dicts def generate_overwrites(old_channel): overwrites = dict() for overwrite in old_channel['overwrites']: allow = discord.Permissions(overwrite['allow_permission']) deny = discord.Permissions(overwrite['deny_permission']) permission_pair = discord.PermissionOverwrite.from_pair( allow, deny) target = None # we do this incase down the road there's a new type of grantee we haven't handled # if a user isn't in the new server, we can't add overwrites for them if overwrite['grantee']['type'] == "User": target = clone_target.get_member( overwrite['grantee']['id']) # this is the code which will convert the old_id in the overwrite into the new_id elif overwrite['grantee']['type'] == "Role": role = get_role(old_id=overwrite['grantee']['old_id']) if role is not None: target = clone_target.get_role(role['new_id']) else: old_role = ctx.guild.get_role( overwrite['grantee']['old_id']) if old_role.name == "@everyone": target = clone_target.default_role else: print( "Could not find new role pair for old role with ID {}" .format(overwrite['grantee']['old_id'])) if target is not None: overwrites[target] = permission_pair return overwrites s_channels = guild.channels s_channels.sort(key=lambda x: x.position, reverse=False) for channel in s_channels: add_channel(channel) s_roles = guild.roles s_roles.reverse() for role in s_roles: if role.name != "@everyone": add_role(role) await progress.edit(embed=generate_progress_embed( "Wiping roles of {clone_target.name}".format( clone_target=clone_target))) for role in clone_target.roles: try: await role.delete(reason="Cleaning for copying") except discord.errors.HTTPException: pass await progress.edit(embed=generate_progress_embed( "Wiping channels of {clone_target.name}".format( clone_target=clone_target))) for channel in clone_target.channels: await channel.delete(reason="Cleaning for copying") print("Wiped channels") await progress.edit(embed=generate_progress_embed( "Creating new roles in {clone_target.name}".format( clone_target=clone_target))) for role in get_role_dicts(): old_role = role['old_role'] logger.info("Adding role{id} - {name}".format( id=role['old_id'], name=role['old_role']['name'])) new_role_to_merge = await clone_target.create_role( name=old_role['name'], permissions=discord.Permissions( permissions=old_role['permissions']), colour=discord.Colour(old_role['colour']), hoist=old_role['hoist'], mentionable=old_role['mentionable']) set_new_role(old_role['id'], new_role_to_merge) get_role_dicts( ) # this converts all the new channels into nice dictionary formats await progress.edit(embed=generate_progress_embed( "Creating new channels in {clone_target.name}".format( clone_target=clone_target))) # first, we add the categories channels_to_add = get_channel_dicts() channels.sort(key=lambda x: x['old_channel']['position'], reverse=False) for channel in channels_to_add: old_channel = channel['old_channel'] if old_channel['type'] == "Category": logger.info("Adding category {id} - {name}".format( id=old_channel['id'], name=old_channel['name'])) # build overwrites overwrites = generate_overwrites(old_channel) channel = await clone_target.create_category_channel( old_channel['name'], overwrites=overwrites, reason="Syncing of channels") set_new_channel(old_channel['id'], channel) # this makes sure everything is still in dictionary formats channels_to_add = get_channel_dicts() # now we know all our categories are added, we can add all other channels for channel in channels_to_add: old_channel = channel['old_channel'] if old_channel['type'] != "Category": logger.info("Adding {type} channel {id} - {name}".format( type=old_channel['type'], id=old_channel['id'], name=old_channel['name'])) overwrites = generate_overwrites(old_channel) category = get_channel( old_id=old_channel['category'].get('id')) if category is not None: category_id = category['new_id'] # gets the role object that we need to create the channel category = clone_target.get_channel(category_id) if old_channel['type'] == "Text": channel = await clone_target.create_text_channel( old_channel['name'], overwrites=overwrites, reason="Syncing of channels", position=old_channel['position'], topic=old_channel['topic'], slowmode_delay=old_channel['slowmode_delay'], nsfw=old_channel['nsfw'], category=category) elif old_channel['type'] == "Voice": channel = await clone_target.create_voice_channel( old_channel['name'], overwrites=overwrites, reason="Syncing of channels", position=old_channel['position'], bitrate=old_channel['bitrate'], user_limit=old_channel['user_limit'], category=category) channel = get_channel_object_dict(channel) set_new_channel(old_channel['id'], channel) with open(".last_sync.json", "w") as f: f.write( json.dumps(dict(roles=get_role_dicts(roles=roles), channels=get_channel_dicts(channels=channels)), indent=4)) invite = await clone_target.text_channels[0].create_invite( max_age=0, max_uses=0, unique=True, reason="Generating invite link to join with") await progress.edit(embed=generate_progress_embed( "Done!", colour=green, url=invite.url))
class Sentiment(commands.Cog): def __init__(self, client): self.client = client self.client_extras = ClientTools(client) self.database_extras = DatabaseTools(self.client_extras) @commands.command(aliases=["s"]) async def sentiment(self, ctx, raw: bool = False, nsfw: bool = False, selected_channel: discord.TextChannel = None): """ Calculate sentiment.json from your messages! """ raw = bool(raw) if (not ctx.message.channel.is_nsfw()) and nsfw: return await ctx.send(strings['tagger']['errors']['nsfw'].format(str(ctx.author))) output = await ctx.send(strings['tagger']['title'] + strings['emojis']['loading']) await output.edit(content=output.content + "\n" + strings['tagger']['status']['messages']) async with ctx.channel.typing(): username = self.database_extras.opted_in(user_id=ctx.author.id) if not username: return await output.edit(content=output.content + strings['tagger']['errors']['not_opted_in']) messages, channels = await self.database_extras.get_messages(ctx.author.id, config['limit'] / 10) text = [] text = await self.client_extras.build_messages(ctx, nsfw, messages, channels, selected_channel=selected_channel) await output.edit( content=output.content + strings['emojis']['success'] + "\n" + strings['tagger']['status'][ 'analytical_data']) algo = algo_client.algo('nlp/SocialSentimentAnalysis/0.1.4') response = algo.pipe({"sentenceList": text}) tags = list(response.result) await output.delete() file = open("sentiment.json", "w") file.write(str(tags)) positive = 0 negative = 0 neutral = 0 compound = 0 for tag in tags: positive += tag['positive'] negative += tag['negative'] neutral += tag['neutral'] compound += tag['compound'] em = discord.Embed(title="Sentiment", color=colours.blue) positive = (math.ceil((positive / len(tags)) * 10000)) / 100 negative = (math.ceil((negative / len(tags)) * 10000)) / 100 neutral = (math.ceil((neutral / len(tags)) * 10000)) / 100 compound = (math.ceil((compound / len(tags)) * 10000)) / 100 if raw: file.close() em.add_field(name="Positivity", value=str(positive)) em.add_field(name="Negativity", value=str(negative)) em.add_field(name="Neutrality", value=str(neutral), inline=False) em.add_field(name = "Flip-floppity-ness", value = str(compound)) em.add_field(name="Info", value="*Max value for these are 100 points, min are -100 points*", inline=False) else: positive = math.floor(positive / 2) negative = math.floor(negative / 2) em.add_field(name="Niceness", value=(":heart:" * positive), inline=False) em.add_field(name="Evilness", value=(":smiling_imp:" * math.floor(negative)), inline=False) shoe = random.randint(0, 5) if shoe == 0: emoji_flip = ":sandal:" elif shoe == 1: emoji_flip = ":arrows_counterclockwise:" elif shoe == 2: emoji_flip = ":yin_yang:" elif shoe == 3: emoji_flip = ":libra:" else: emoji_flip = "<a:zthonkspin:399368569500205056>" em.add_field(name = "Flip-floppitiness", value = (emoji_flip * math.floor(compound)), inline = False) output = await ctx.send(embed=em) emoji = await self.client_extras.get_delete_emoji() emoji = emoji[1] return await self.client_extras.delete_option(self.client, output, ctx, emoji)
class ClientTools(): def __init__(self, client): self.database_tools = DatabaseTools(client) self.client = client def channel_allowed(self, channel_id, existing_channel, nsfw=False): """ Check if a channel is allowed in current context channel_id: ID of channel existing_channel: channel object of existing channel nsfw: whether to only return NSFW channels """ channel = self.client.get_channel(int(channel_id)) if channel is None: return False enabled = False for group in enabled_groups: if str(channel.category).lower() == str(group).lower(): enabled = True break if not enabled: return False if not existing_channel.is_nsfw() and bool(nsfw): return False if channel.is_nsfw(): # checks if user wants / is allowed explicit markovs return bool(nsfw) # this means that if the channel *is* NSFW, we return True, but if it isn't, we return False else: # channel is SFW if bool(nsfw): return False # this stops SFW chats from being included in NSFW markovs return True async def build_messages(self, ctx, nsfw, messages, channels, selected_channel=None): """ Returns/appends to a list messages from a user Params: messages: list of messages channel: list of channels for messages selected_channel: Not required, but channel to filter to. If none, filtering is disabled. text = list of text that already exists. If not set, we just create one """ text = [] for counter, m in enumerate(messages): if self.channel_allowed(channels[counter], ctx.message.channel, nsfw): if selected_channel is not None: if self.client.get_channel(int( channels[counter])).id == selected_channel.id: text.append(m) else: text.append(m) return text async def get_delete_emoji(self): delete_emoji = self.client.get_emoji(int(strings['emojis']['delete'])) if delete_emoji is not None: emoji_name = delete_emoji.name else: emoji_name = "❌" return emoji_name, delete_emoji async def error_embed(self, ctx, error, message=None, colour=discord.Embed.Empty): embed = discord.Embed(title='Command Error', colour=colour) embed.description = str(error) embed.add_field(name='Server', value=ctx.guild) embed.add_field(name='Channel', value=ctx.channel.mention) embed.add_field(name='User', value=ctx.author) embed.add_field(name='Message', value=ctx.message.content) embed.timestamp = datetime.datetime.utcnow() await ctx.send(content=message, embed=embed) async def markov_embed(self, title, message): em = discord.Embed(title=title, description=message) name = await self.get_delete_emoji() name = name[0] em.set_footer(text=strings['markov']['output']['footer'].format(name)) return em async def delete_option(self, client, message, ctx, delete_emoji, timeout=config['discord']['delete_timeout']): """Utility function that allows for you to add a delete option to the end of a command. This makes it easier for users to control the output of commands, esp handy for random output ones.""" await message.add_reaction(delete_emoji) def check(r, u): return str( r) == str(delete_emoji ) and u == ctx.author and r.message.id == message.id try: await client.wait_for("reaction_add", timeout=timeout, check=check) await message.remove_reaction(delete_emoji, client.user) await message.remove_reaction(delete_emoji, ctx.author) em = discord.Embed(title=str(ctx.message.author) + " deleted message", description="User deleted this message.") return await message.edit(embed=em) except concurrent.futures._base.TimeoutError: await message.remove_reaction(delete_emoji, client.user) async def build_data_profile(self, members, limit=50000): """ Used for building a data profile based on a user Members: list of members we want to import for Guild: Guild object Limit: limit of messages to be imported """ for guild in self.client.guilds: for cur_channel in guild.text_channels: adding = False for group in enabled_groups: if str(cur_channel.category).lower() == str(group).lower(): adding = True break if adding: logger.info("Logging from {}".format(cur_channel.name)) counter = 0 already_added = 0 async for message in cur_channel.history(limit=limit, reverse=True): if message.author in members: self.database_tools.add_message_to_db(message) logger.info( "{} scraped for {} users - added {} messages, found {} already added" .format(cur_channel.name, len(members), counter, already_added)) async def process_message(self, message): await self.check_flags(message) user_exp = self.database_tools.opted_in(user_id=message.author.id) if user_exp is not False: self.database_tools.add_message_to_db(message) logger.debug("Message from {}".format(user_exp)) # this records analytical data - don't adjust this without reading # Discord TOS first try: cursor.execute(add_message, (int(message.id), str(message.channel.id), message.created_at.strftime('%Y-%m-%d %H:%M:%S'))) cnx.commit() except mysql.connector.errors.IntegrityError: pass try: # if its double(or more) prefixed then it cant be a command (?word is a command, ????? is not) if message.content[len(config['discord'] ['prefix'])] == config['discord']['prefix']: return except IndexError: return async def check_flags(self, message): if type(message.channel) == discord.DMChannel: return if message.author.id == self.client.user.id: return matches = [] flag_settings = guild_settings.get_bad_words(message.guild) flags = flag_settings['words'] regexes = flag_settings.get('regex') if regexes is None: regexes = [] channel_id = flag_settings['alert_channel'] if channel_id is None: return for flag in flags: if flag.lower() != "" and flag in message.content.lower(): matches.append(flag) for regex in regexes: try: temp_regex = re.compile(regex, re.IGNORECASE) for word in message.content.lower().split(" "): if temp_regex.match(word): matches.append(word) except: pass # we do this because some regex may be bad if len(matches) > 0: embed = discord.Embed(title="Potential usage of flag detected", color=colours.dark_red) embed.add_field(name="Flag(s)", value=str(matches)) embed.add_field(name="Message", value=str(message.content)) embed.add_field(name="Author", value=str(message.author.mention)) embed.add_field(name="Author ID", value=str(message.author.id)) embed.add_field(name="Channel", value=str(message.channel.mention)) embed.add_field(name="Time", value=str(message.created_at)) embed.add_field(name="Message ID", value=str(message.id)) embed.add_field(name="Guild ID", value=str(message.channel.guild.id)) embed.add_field(name="Guild", value=str(message.channel.guild)) embed.add_field( name="Message Link", value=str("https://discordapp.com/channels/{}/{}/{}".format( message.guild.id, message.channel.id, message.id)), inline=False) channel = self.client.get_channel(channel_id) await channel.send(embed=embed) async def optout_user(self, user): """ Opt a user out of experiments, and delete their data Returns number of messages deleted """ logger.info("Deleting data for user ID {}".format(user.id)) cursor.execute("DELETE FROM users WHERE user_id = %s", (user.id, )) result = cursor.execute( "DELETE FROM messages_detailed WHERE user_id = %s", (user.id, )) cnx.commit() logger.info("Data deleted.")
class Markov(commands.Cog): def __init__(self, client): self.client = client self.database_tools = DatabaseTools(client) self.client_tools = ClientTools(client) @commands.command(aliases=["m_s"]) async def markov_server(self, ctx, nsfw: bool = False, selected_channel: discord.TextChannel = None): """ Generates markov output based on entire server's messages. """ nsfw_mismatch = False if selected_channel is not None: if selected_channel.is_nsfw() and not nsfw: nsfw_mismatch = True elif not selected_channel.is_nsfw() and nsfw: nsfw_mismatch = True if nsfw_mismatch: return await ctx.send(embed=discord.Embed( title="Error", description= "The selected channel and the NSFW flag do not match. Please ensure these are both correct.", color=colours.red)) output = await ctx.send(strings['markov']['title'] + strings['emojis']['loading']) await output.edit(content=output.content + "\n" + strings['markov']['status']['messages']) async with ctx.channel.typing(): text = [] messages, channels = await self.database_tools.get_messages( ctx.author.id, config['limit_server'], server=True) text = await self.client_tools.build_messages( ctx, nsfw, messages, channels, selected_channel=selected_channel) text1 = "" for m in text: text1 += str(m) + "\n" if len(text) < 10: return await output.edit( content=output.content + strings['markov']['errors']['low_activity']) try: await output.edit( content=output.content + strings['emojis']['success'] + "\n" + strings['markov']['status']['building_markov']) # text_model = POSifiedText(text) text_model = markovify.NewlineText( text, state_size=config['state_size']) except KeyError: return ctx.send('Not enough data yet, sorry!') await output.edit(content=output.content + strings['emojis']['success'] + "\n" + strings['markov']['status']['making']) text = text_model.make_short_sentence(140) attempt = 0 while (True): attempt += 1 if attempt >= 10: return await ctx.send( strings['markov']['errors']['failed_to_generate']) message_formatted = str(text) if message_formatted != "None": break await output.delete() em = await self.client_tools.markov_embed( strings['markov']['output']['title_server'], message_formatted) output = await ctx.send(embed=em) return await self.client_tools.delete_option( self.client, output, ctx, self.client.get_emoji(int(strings['emojis']['delete'])) or "❌") @commands.command(aliases=["m"]) async def markov(self, ctx, nsfw: bool = False, selected_channel: discord.TextChannel = None): """ Generates markov output for user who ran this command """ if (not ctx.message.channel.is_nsfw()) and nsfw: return await ctx.send(strings['markov']['errors']['nsfw'].format( str(ctx.author))) output = await ctx.send(strings['markov']['title'] + strings['emojis']['loading']) await output.edit(content=output.content + "\n" + strings['markov']['status']['messages']) async with ctx.channel.typing(): username = self.database_tools.opted_in(user_id=ctx.author.id) if not username: return await output.edit( content=output.content + strings['markov']['errors']['not_opted_in']) messages, channels = await self.database_tools.get_messages( ctx.author.id, config['limit']) text = [] text = await self.client_tools.build_messages( ctx, nsfw, messages, channels, selected_channel=selected_channel) text1 = "" for m in text: text1 += str(m) + "\n" try: await output.edit( content=output.content + strings['emojis']['success'] + "\n" + strings['markov']['status']['building_markov']) # text_model = POSifiedText(text) text_model = markovify.NewlineText( text, state_size=config['state_size']) except KeyError: return ctx.send('Not enough data yet, sorry!') attempt = 0 while (True): attempt += 1 if attempt >= 10: await output.delete() return await ctx.send( strings['markov']['errors']['failed_to_generate']) new_sentance = text_model.make_short_sentence(140) message_formatted = str(new_sentance) if message_formatted != "None": break await output.edit(content=output.content + strings['emojis']['success'] + "\n" + strings['markov']['status']['analytical_data']) await self.database_tools.save_markov(text_model, ctx.author.id) await output.edit(content=output.content + strings['emojis']['success'] + "\n" + strings['markov']['status']['making']) await output.delete() em = await self.client_tools.markov_embed(str(ctx.author), message_formatted) output = await ctx.send(embed=em) return await self.client_tools.delete_option( self.client, output, ctx, self.client.get_emoji(int(strings['emojis']['delete'])) or "❌")
class Controls(commands.Cog): def __init__(self, client): self.client = client self.database_tools = DatabaseTools(client) self.client_tools = ClientTools(client) @commands.command(aliases=["optin", "opt_in"]) async def experiments(self, ctx): """ Opt into data analysis and experiments. """ message = ctx.message channel = message.channel author = message.author create_user = "******" try: cursor.execute(create_user, (author.id, author.name)) cnx.commit() em = discord.Embed( title=strings['data_collection']['opt_in_title'], description=opt_in_message) em.set_footer(text=strings['data_collection']['opt_in_footer']) return await channel.send(embed=em) except mysql.connector.errors.IntegrityError: get_user = "******" cursor.execute(get_user, (author.id,)) opt_in_user = "******" cursor.execute(opt_in_user, (author.id,)) await channel.send(strings['data_collection']['data_track_start'] + " for " + str(ctx.message.author)) await self.client_tools.build_data_profile([author]) await channel.send(strings['data_collection']['complete'].format(author.name)) @commands.command(aliases=["opt_in_automated", "optinautomated"]) async def automated(self, ctx): """ Opt in to automated messages. Run this again to opt out. """ if not self.database_tools.opted_in(user_id=ctx.author.id): return await ctx.channel.send(embed=discord.Embed(title="Error", description=strings['tagger']['errors']['not_opted_in'], color=colours.red)) if self.database_tools.is_automated(ctx.author): output = await ctx.channel.send("Opting you out of automation.") query = "UPDATE `users` SET `automate_opted_in`=b'0' WHERE `user_id`=%s;" cursor.execute(query, (ctx.author.id,)) cnx.commit() await output.delete() return await ctx.channel.send(embed=discord.Embed(title="Success", description='You will be removed from the pool on the next refresh (IE: when the bot goes back around in a loop again)')) else: output = await ctx.channel.send("Opting you into automation") query = "UPDATE`users` SET `automate_opted_in`=b'1' WHERE `user_id`=%s;" cursor.execute(query, (ctx.author.id,)) cnx.commit() await output.delete() return await ctx.channel.send(embed=discord.Embed(title="Success", description='Opted in!', color=colours.green)) @commands.command(aliases=["blacklist", "block_list", "black_list"]) async def blocklist(self, ctx, command=None, word=None): """ Prevents words from being shown publicly through methods such as markov and markov_server. Note: they will still be logged, and this just prevents them being shown in chat. Command: option to use Word: Word to add or remove from blocklist """ pm_channel = (discord.channel.DMChannel == type(ctx.channel)) if not pm_channel: try: await ctx.message.delete() except discord.errors.Forbidden: logger.warn( "Could not delete blacklist command, lacking permissions") if command is None: return await ctx.send(""" No subcommand selected - please enter a subcommand for your blocklist. ?blocklist add [word] : Add word to blocklist ?blocklist remove [word] : Remove word from blocklist ?blocklist get : Get PM of current blocklist """) # fetch current blocklist blockL = await self.database_tools.get_blocklist(ctx.author.id) update_blocklist = "UPDATE blocklists SET blocklist = %s WHERE user_id = %s" if command == "add": if word is None: return await ctx.send(strings['blocklist']['status']['no_word'], delete_after=config['discord']['delete_timeout']) msg = await ctx.send(strings['blocklist']['status']['adding']) # check if the word is already on the list. throw error if it is if word.lower() not in blockL: # if its not then add it blockL.append(word.lower()) # update DB with new list new_json = json.dumps(blockL) cursor.execute(update_blocklist, (new_json, ctx.author.id,)) else: await msg.delete() return await ctx.send(strings['blocklist']['status']['exist']) elif command == "remove": if word is None: return await ctx.send(strings['blocklist']['status']['no_word'], delete_after=config['discord']['delete_timeout']) msg = await ctx.send(strings['blocklist']['status']['removing']) # try and remove it from list (use a try statement, catching ValueError) try: blockL.remove(word.lower()) except ValueError: return await msg.edit(content=strings['blocklist']['status']['not_exist']) # update DB with new list new_json = json.dumps(blockL) cursor.execute(update_blocklist, (new_json, ctx.author.id,)) elif command == "get": # make it nice to look at if blockL == []: msg = strings['blocklist']['status']['empty'] else: msg = strings['blocklist']['status']['list'] for item in blockL: # done so that the merge with the long string is only done once per word part = ' ' + item + ',' msg += part msg = msg[:-1] # trim off the trailing , # send a private message with the nice to look at blocklist # this prevents the next commands from running return await ctx.author.send(msg) else: return await ctx.send(""" No subcommand selected - please enter a subcommand for your blocklist. ?blocklist add [word] : Add word to blocklist ?blocklist remove [word] : Remove word from blocklist ?blocklist get : Get PM of current blocklist """) await msg.edit(content=strings['blocklist']['status']['complete']) @commands.command(aliases=["opt_out"]) async def optout(self, ctx): """ Run this to optout of experiments, and delete your data """ em = discord.Embed( title=strings['data_collection']['opt_out_starting_title'], description=strings['data_collection']['opt_out_starting_message'], color=colours.red) current_embed = await ctx.send(embed=em) await self.client_tools.optout_user(ctx.author) em = discord.Embed(title=strings['data_collection']['opt_out_finish_title'], description=strings['data_collection']['opt_out_finish_message'], color=colours.green) await current_embed.edit(embed=em) @commands.command(aliases=["datainfo", "data", "aboutme", "mydata", "my_data", "about_me"]) async def data_info(self, ctx): """Returns the data we store on you.""" async with ctx.channel.typing(): username = self.database_tools.opted_in(user_id=ctx.author.id) if not username: em = discord.Embed(title="You are not opted in", description="As you are opted out, we have no info on you", color=colours.red) return await ctx.author.send(embed=em) message_count = await self.database_tools.get_message_count( user_id=ctx.author.id) blocklist = await self.database_tools.get_blocklist(ctx.author.id) blocklist_count = len(blocklist) # just to prevent this being used later as it contains sensitive info del(blocklist) em = discord.Embed(title="Data info", color=colours.blue) em.add_field(name="Message count", value=message_count) em.add_field(name="Blocklist count", value=blocklist_count) # we send to the author, so the data is kept private as per GDPR await ctx.author.send(embed=em) await ctx.message.delete()