def add_setting(guild_id: int, setting: str, value: Union[str, int], active=True, set_by="", set_date=datetime.now()): """ Add an entry to the settings database :param guild_id: id the setting is in :param value: value of the setting - probably name of a word-list :param set_by: user id or name of the member who entered that setting - could be neat for logs :param set_date: date the setting was configured :param setting: setting type to add :param active: if setting shall be active, not used at the moment """ if type(value) is int: value = str(value) session = db.open_session() entry = db.Settings(guild_id=guild_id, setting=setting, value=value, is_active=active, set_by=set_by, set_date=set_date) session.add(entry) session.commit() session.close()
def add_channel(voice_channel_id: int, text_channel_id: str, guild_id: int, internal_type: str, category=None, set_by='unknown', set_date=datetime.now()): """ :param voice_channel_id: id of created voice channel :param text_channel_id: id of linked text_channel :param guild_id: id of the guild the channels were created on :param internal_type: type of the channels 'public' or 'private' etc :param category: optional category the channels are in :param set_by: optional which module issued creation :param set_date: date the channel was created - default is datetime.now() """ session = db.open_session() entry = db.CreatedChannels( voice_channel_id=voice_channel_id, text_channel_id=text_channel_id, guild_id=guild_id, internal_type=internal_type, category=category, set_by=set_by, set_date=set_date ) session.add(entry) session.commit() session.close()
def get_voice_channel_by_id(channel_id: int, session=db.open_session()): statement = select(db.CreatedChannels).where( db.CreatedChannels.voice_channel_id == channel_id ) entry = session.execute(statement).first() return entry[0] if entry else None
def del_channel(voice_channel_id: int): session = db.open_session() statement = delete(db.CreatedChannels).where( db.CreatedChannels.voice_channel_id == voice_channel_id ) session.execute(statement) session.commit() session.close()
def del_setting_by_setting(guild_id: int, setting: str): """ Delete an entry from the settings table by giving only the settings name :param guild_id: id the setting is in :param setting: the setting - like archive or prefix """ session = db.open_session() statement = delete(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.setting == setting)) session.execute(statement) session.commit() session.close()
def get_first_setting_for( guild_id: int, setting: str, session=db.open_session()) -> Union[db.Settings, None]: """ Wrapper around get_all_settings_for() that extracts the first entry from returned list :param guild_id: id of the guild to search for :param setting: name of the setting to search for :param session: session to search with, helpful if object shall be edited, since the same session is needed for this :return: first setting to match the query """ entries = get_all_settings_for(guild_id, setting, session) return entries[0] if entries else None
def get_all_settings_for( guild_id: int, setting: str, session=db.open_session()) -> Union[List[db.Settings], None]: """ Searches db for setting in a guild that matches the setting name :param guild_id: id of the guild to search for :param setting: name of the setting to search for :param session: session to search with, helpful if object shall be edited, since the same session is needed for this :return: list of settings that match the given given setting name """ sel_statement = select(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.setting == setting)) entries = session.execute(sel_statement).all() return [entry[0] for entry in entries] if entries else None
def del_setting_by_value(guild_id: int, value: Union[str, int]): """ Delete an entry from the settings table by giving only the value :param guild_id: id the setting is in :param value: value of the setting like a channel id """ if type(value) is int: value = str(int) session = db.open_session() statement = delete(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.value == value)) session.execute(statement) session.commit() session.close()
def get_setting_by_value( guild_id: int, value: Union[str, int], session=db.open_session()) -> Union[db.Settings, None]: """ Used to extract a setting that has a channel id as value and an unknown setting-name :param guild_id: guild to search on :param value: settings value to search for :param session: will be created if none is passed in :return: database entry if exists with those specific parameters, else None """ statement = select(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.value == str(value))) entry = session.execute(statement).first() return entry[0] if entry else None
def get_setting( guild_id: int, setting: str, value: str, session=db.open_session()) -> Union[db.Settings, None]: """ Searches db for one specific setting and returns if if exists :param guild_id: id of the guild to search for :param value: value of the setting to search for :param setting: name of the setting to search for :param session: session to search with, helpful if object shall be edited, since the same session is needed for this. :return: database entry if exists with those specific parameters, else None """ sel_statement = select(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.setting == setting, db.Settings.value == value)) entry = session.execute(sel_statement).first() return entry[0] if entry else None
def del_setting(guild_id: int, setting: str, value: Union[str, int]): """ Delete an entry from the settings table :param guild_id: id the setting is in :param value: value of the setting - probably name of a word-list :param setting: setting type to delete """ if type(value) is int: value = str(int) session = db.open_session() statement = delete(db.Settings).where( and_(db.Settings.guild_id == guild_id, db.Settings.setting == setting, db.Settings.value == value)) session.execute(statement) session.commit() session.close()
async def update_value_or_create_entry(ctx: commands.Context, setting_name: str, set_value: str, value_name: str): """ Check if an entry exists based on setting name, alter its value\n Create a new entry if no setting was found\n -> If there is a setting with the name 'prefix' update setting.value = new_prefix\n Send update messages on discord :param ctx: command context :param setting_name: name of the setting :param set_value: value the setting shall be set to :param value_name: name the set value should have in bot message """ # check if there is an entry for that setting - toggle it session = db_models.open_session() entry = settings_db.get_first_setting_for(ctx.guild.id, setting_name, session=session) if entry: entry.value = set_value session.add(entry) session.commit() # send reply await Settings.send_setting_updated(ctx, setting_name, value_name) return settings_db.add_setting(guild_id=ctx.guild.id, setting=setting_name, value=set_value, set_by=f"{ctx.author.id}") session.close() # send reply await Settings.send_setting_added(ctx, setting_name, value_name)
def get_channels_by_type(guild_id: int, internal_type: str, session=db.open_session()) -> Union[List[db.CreatedChannels], None]: """ Get all channels of an internal type by it's name :param guild_id: guild to search on :param internal_type: type of channels to search - e.g. 'public_channel' :param session: optional if an entry shall be updated :return: list of all channels of that 'class' """ statement = select(db.CreatedChannels).where( and_( db.CreatedChannels.guild_id == guild_id, db.CreatedChannels.internal_type == internal_type ) ) entries = session.execute(statement).all() return [entry[0] for entry in entries] if entries else None
async def get_settings(self, ctx): """ prints setting on guild """ tracked_channels = [] # conversion to set since some keys appear multiple times due to aliases # create session externally to remove flawed entries # this should never happen, except during development. # But we can prevent crashes due to unexpected circumstances session = db_models.open_session() for setting in set(settings.values()): entries = settings_db.get_all_settings_for(ctx.guild.id, setting, session=session) if entries: tracked_channels.extend(entries) stc = "__Static Channels:__\n" # TODO: Implement static channels in settings - extra db access + loop needed pub = "__Public Channels:__\n" priv = "__Private Channels:___\n" log = "__Log Channel:__\n" archive = "__Archive Category:__\n" prefix = "__Prefixes:__\n" if not tracked_channels: emb = utils.make_embed( color=utils.yellow, name="No configuration yet", value="You didn't configure anything yet.\n" f"Use the help command " f"or try the quick-setup command `{PREFIX}setup @role` to get started :)" ) await ctx.send(embed=emb) return for i, elm in enumerate(tracked_channels): # building strings # security check to prevent the command from crashing # if an entry has a NoneType value it's useless an can be deleted if elm.setting is None or elm.value is None: session.delete(elm) logger.warning( f"Deleting setting, because containing NoneType values:\n{elm}" ) continue if elm.setting == "public_channel": pub += f"`{ctx.guild.get_channel(int(elm.value))}` with ID `{elm.value}`\n" elif elm.setting == "private_channel": priv += f"`{ctx.guild.get_channel(int(elm.value))}` with ID `{elm.value}`\n" elif elm.setting == "log_channel": log += f"`{ctx.guild.get_channel(int(elm.value)).mention}` with ID `{elm.value}`\n" elif elm.setting == "archive_category": archive += f"`{ctx.guild.get_channel(int(elm.value))}` with ID `{elm.value}`\n" elif elm.setting == "prefix": prefix += f"`{elm.value}` example `{elm.value}help`\n" session.commit() # delete all flawed entries session.close() emby = utils.make_embed(color=utils.blue_light, name="Server Settings", value=f"\n" f"{stc}\n" f"{pub}\n" f"{priv}\n" f"{archive}\n" f"{log}\n" f"{prefix}") await ctx.send(embed=emby)
async def set_setting(self, ctx: commands.Context, *params): # first param setting name, second param setting value syntax = "Example: `set [setting name] [setting value]`" try: setting = params[0] except IndexError: msg = (f"Please ensure that you've entered a valid setting \ and channel-id or role-id for that setting.\n{syntax}" ) emby = utils.make_embed(color=discord.Color.orange(), name="Can't get setting name", value=msg) await ctx.send(embed=emby) return try: value = params[1] except IndexError: msg = (f"Please ensure that you've entered a valid \ channel-id / role-id or other required parameter for that setting.\n{syntax}" ) emby = utils.make_embed(color=discord.Color.orange(), name="Can't get value", value=msg) await ctx.send(embed=emby) return # look if setting name is valid setting = setting.lower( ) # dict only handles lower case, so do we all the time setting_type = settings.get(setting, None) if not setting_type: await ctx.send(embed=utils.make_embed( name=f"'{setting}' is no valid setting name", value= "Use the help command to get an overview of possible settings", color=utils.yellow)) return # setting is validated, let's see if the value matches the required setting value = value.strip() # just in case set_value, set_name = None, None # first check 'easy' cases if setting_type in ['archive_category', 'log_channel']: # trying to get a corresponding channel (id: str, name/ mention: str) set_value, set_name = await self.channel_from_input( ctx, setting_type, value) elif setting_type == "prefix": # need to await since it sends the error message if we can't match set_value, set_name = await self.prefix_validation(ctx, value) elif setting_type == "allow_public_rename": set_value, set_name = await self.validate_toggle(ctx, value) # enter to database if value is correct if set_value: print(f"{set_value=}") await self.update_value_or_create_entry(ctx, setting_type, set_value, set_name) return # we're done - the other handling isn't needed # handle the addition of static channels # those are voice channels that are persistent but receive a new text channel every time a member joins if setting_type in ['static_channel']: # trying to get a corresponding channel (id: str, name/ mention: str) set_value, set_name = await self.channel_from_input( ctx, setting_type, value) # given channel id seems flawed - returning if not set_value: return channel: discord.VoiceChannel = await self.validate_channel( ctx, set_value) if channel is None: return session = db_models.open_session() entry: Union[db_models.CreatedChannels, None] = channels_db.get_voice_channel_by_id( set_value, session) if entry: entry.internal_type = setting_type session.add(entry) session.commit() await self.send_setting_updated(ctx, setting_type, set_name) return # delete old setting if channels was e.g. public-channel before settings_db.del_setting_by_value(ctx.guild.id, set_value) channels_db.add_channel(voice_channel_id=int(set_value), text_channel_id=None, guild_id=ctx.guild.id, internal_type=setting_type, category=channel.category_id, set_by=ctx.author.id) session.close() await self.send_setting_added(ctx, setting_type, set_name) # now handle tracked channels # tracked channels require an other database handling as the other settings # we need to check if there is a setting with that value and adjust the setting_name # in all other cases it's we check the settings name and alter the value... # e.g. if there is a setting for setting.value == channel_id change setting.name to channel_type if setting_type in ['public_channel', 'private_channel']: # trying to get a corresponding channel (id: str, name/ mention: str) set_value, set_name = await self.channel_from_input( ctx, setting_type, value) session = db_models.open_session() entry: Union[db_models.Settings, None] = settings_db.get_setting_by_value( ctx.guild.id, set_value, session) # given channel id seems flawed - returning if not set_value: return # if channel is already registered - update if entry: entry.setting = setting_type session.add(entry) session.commit() # send reply await self.send_setting_updated(ctx, setting_type, set_name) # create new entry, channel not tracked yet else: # delete old entry in channels db if it exists - maybe channel was static channel before channels_db.del_channel(int(set_value)) # write entry to db settings_db.add_setting( guild_id=ctx.guild.id, setting=setting_type, value=set_value, set_by=ctx.author.id, ) # send reply await self.send_setting_updated(ctx, setting_type, set_name) session.close()
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): # this is the case that a state update happens that is not a channel switch, but a mute or something like that if before.channel and after.channel and before.channel.id == after.channel.id: return # as shorthand - we'll need this a few times guild: discord.Guild = member.guild bot_member_on_guild: discord.Member = guild.get_member(self.bot.user.id) after_channel: Union[discord.VoiceChannel, None] = after.channel before_channel: Union[discord.VoiceChannel, None] = before.channel # open db session session = db.open_session() # get settings for archive and log channel log_entry = settings_db.get_first_setting_for(guild.id, "log_channel", session) # get entry if exists archive_entry = settings_db.get_first_setting_for(guild.id, "archive_category", session) # get channels from entries if existing log_channel: Union[discord.TextChannel, None] = guild.get_channel(int(log_entry.value)) if log_entry else None archive_category: Union[discord.CategoryChannel, None] = guild.get_channel( int(archive_entry.value)) if archive_entry else None # check if member has a voice channel after the state update # could trigger the creation of a new channel or require an update for an existing one if after_channel: # check db if channel is a channel that was created by the bot created_channel: Union[db.CreatedChannels, None] = channels_db.get_voice_channel_by_id(after_channel.id, session) # check if joined (after) channel is a channel that triggers a channel creation tracked_channel = settings_db.get_setting_by_value(guild.id, after_channel.id, session) if tracked_channel: voice_channel, text_channel = await create_new_channels(member, after, tracked_channel.setting, bot_member_on_guild) # write to log channel if configured if log_entry: await log_channel.send( embed=utl.make_embed( name="Created voice channel", value=f"{member.mention} created `{voice_channel.name if voice_channel else '`deleted`'}` " f"with {text_channel.mention if text_channel else '`deleted`'}", color=utl.green ) ) # moving creator to created channel try: await member.move_to(voice_channel, reason=f'{member} issued creation') await send_welcome_message(text_channel, voice_channel) # send message explaining text channel # if user already left already except discord.HTTPException as e: print("Handle HTTP exception during creation of channels - channel was already empty") await clean_after_exception(voice_channel, text_channel, self.bot, archive=archive_category, log_channel=log_channel) # channel is in our database - add user to linked text_channel elif created_channel: # static channels need a new linked text-channel if they were empty before if created_channel.internal_type == 'static_channel' and created_channel.text_channel_id is None: try: tc_overwrite = generate_text_channel_overwrite(after_channel, self.bot.user) text_channel = await guild.create_text_channel(f"{tc_sign_prefix}{after_channel.name}", overwrites=tc_overwrite, category=after_channel.category, reason="User joined linked voice channel") created_channel.text_channel_id = text_channel.id session.add(created_channel) session.flush() await send_welcome_message(text_channel, after_channel) # send message explaining text channel except discord.HTTPException as e: # TODO: log this pass # processing 'normal', existing linked channel else: # update overwrites to add user to joined channel # TODO we can skip this API call when the creator just got moved await update_channel_overwrites(after_channel, created_channel, bot_member_on_guild) if before_channel: # check db if before channel is a channel that was created by the bot created_channel: Union[db.CreatedChannels, None] = channels_db.get_voice_channel_by_id(before_channel.id, session) if created_channel: # member left but there are still members in vc if before_channel.members: # remove user from left linked channel await update_channel_overwrites(before_channel, created_channel, bot_member_on_guild) # left channel is now empty else: # fetch needed information before_channel_id: int = before_channel.id # extract id before deleting, needed for db deletion text_channel: Union[discord.TextChannel, None] = guild.get_channel(created_channel.text_channel_id) # delete channels - catch AttributeErrors to still do the db access and the logging # delete VC only if it's not a static_channel if created_channel.internal_type != 'static_channel': try: await before_channel.delete(reason="Channel is empty") except AttributeError: pass # archive or delete linked text channel try: archived_channel = await delete_text_channel(text_channel, self.bot, archive=archive_category) except AttributeError: archived_channel = None except discord.errors.HTTPException: # occurs when category that the channel shall be moved to is full archived_channel = None await log_channel.send( embed=utl.make_embed( name="ERROR handling linked text channel", value=f"This error probably means that the archive `{archive_category.mention}` is full.\n" "Please check the category and it and set a new one or delete older channels.\n" "Text channel was not deleted", color=utl.red)) if log_channel: static = True if created_channel.internal_type == 'static_channel' else False # helper variable await log_channel.send( embed=utl.make_embed( name=f"Removed {text_channel.name}" if static else f"Deleted {before_channel.name}", value=f"{text_channel.mention} was linked to {before_channel.name} and is " if static else f"The linked text channel {text_channel.mention} is " f"{'moved to archive' if archived_channel is not None and archive_category else 'deleted'}", color=utl.green ) ) if created_channel.internal_type == 'static_channel': # remove reference to now archived channel created_channel.text_channel_id = None session.add(created_channel) session.flush() else: # remove deleted channel from database channels_db.del_channel(before_channel_id) session.commit() session.close()