diff --git a/build_tools/cached_yapf.py b/build_tools/cached_yapf.py index 142db91..30ee6a6 100644 --- a/build_tools/cached_yapf.py +++ b/build_tools/cached_yapf.py @@ -10,7 +10,7 @@ import subprocess from dataclasses import dataclass -from typing import List, Literal, Final, Dict +from typing import List, Literal, Final, Dict, AsyncIterator @dataclass @@ -153,12 +153,12 @@ async def run_single_yapf(mode: Literal["diff", "inplace"], filename: str) -> Pr return ProcessedData(filename, stdout, stderr, proc.returncode or 0) -async def run_yapf_async(mode: Literal["diff", "inplace"], files: List[str]) -> List[ProcessedData]: +async def run_yapf_async(mode: Literal["diff", "inplace"], files: List[str]) -> AsyncIterator[ProcessedData]: """ Runs multiple yapf instances in async subprocesses and provides returncode and info for each """ tasks = [asyncio.create_task(run_single_yapf(mode, i)) for i in files] - return [await task for task in tasks] + return (await task for task in tasks) def run_yapf_once(mode: Literal["diff", "inplace"], files: List[str]) -> "subprocess.CompletedProcess[bytes]": @@ -184,12 +184,12 @@ def process_inplace(cache: Dict[str, CacheEntry], files: List[str]) -> int: return proc.returncode -def process_diff(cache: Dict[str, CacheEntry], files: List[str]) -> int: +async def process_diff(cache: Dict[str, CacheEntry], files: List[str]) -> int: returncode = 0 safe_cached = [] - for proc in asyncio.run(run_yapf_async("diff", files)): + async for proc in await run_yapf_async("diff", files): if proc.stdout: print(proc.stdout.decode("utf8")) @@ -231,7 +231,7 @@ def main(args: List[str]) -> int: if parsed.mode == "inplace": return process_inplace(cache, process) else: - return process_diff(cache, process) + return asyncio.run(process_diff(cache, process)) if __name__ == "__main__": diff --git a/build_tools/cmds_to_html.py b/build_tools/cmds_to_html.py index 1db5e50..1c21337 100644 --- a/build_tools/cmds_to_html.py +++ b/build_tools/cmds_to_html.py @@ -73,6 +73,19 @@ raise SyntaxError(f"ERROR IN [{execmodule} : {command}] PERMISSION TYPE({cmd.permission}) IS NOT VALID") +# Test for pretty_name starting with the keyname +for command in command_modules_dict: + if "alias" in command_modules_dict[command]: + continue + + # cmd.execute might point to lib_sonnetcommands if it builds a closure for backwards compat, so get the raw value + execmodule = command_modules_dict[command]['execute'].__module__ + + if command_modules_dict[command]["pretty_name"].startswith(command): + continue + + raise SyntaxError(f"ERROR IN [{execmodule} : {command}] pretty_name does not start with command name (malformed helptext)") + # Test for aliases pointing to existing commands that are not aliases for command in command_modules_dict: if "alias" not in command_modules_dict[command]: diff --git a/cmds/cmd_automod.py b/cmds/cmd_automod.py index bfa88ab..6c0b7f1 100644 --- a/cmds/cmd_automod.py +++ b/cmds/cmd_automod.py @@ -284,6 +284,29 @@ async def set_blacklist_infraction_level(message: discord.Message, args: List[st if ctx.verbose: await message.channel.send(f"Updated blacklist action to `{action}`") +async def set_antispam_command(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> int: + if not message.guild: + return 1 + + if args: + action = args[0].lower() + else: + await message.channel.send(f"antispam action is `{ctx.conf_cache['antispam-action']}`") + return 0 + + if action not in ["timeout", "mute"]: + await message.channel.send("ERROR: Antispam action is not valid\nValid Actions: `timeout` and `mute`") + return 1 + + with db_hlapi(message.guild.id) as database: + database.add_config("antispam-action", action) + + if ctx.verbose: + await message.channel.send(f"Updated antispam action to `{action}`") + + return 0 + + async def change_rolewhitelist(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> Any: return await parse_role(message, args, "blacklist-whitelist", verbose=ctx.verbose) @@ -376,21 +399,21 @@ async def antispam_time_set(message: discord.Message, args: List[str], client: d return 1 else: mutetime = int(ctx.conf_cache["antispam-time"]) - await message.channel.send(f"Antispam mute time is {mutetime} seconds") + await message.channel.send(f"Antispam timeout is {format_duration(mutetime)}") return 0 if mutetime < 0: - await message.channel.send("ERROR: Mutetime cannot be negative") + await message.channel.send("ERROR: timeout cannot be negative") return 1 elif mutetime >= 60 * 60 * 256: - await message.channel.send("ERROR: Mutetime cannot be greater than 256 hours") + await message.channel.send("ERROR: timeout cannot be greater than 256 hours") return 1 with db_hlapi(message.guild.id) as db: db.add_config("antispam-time", str(mutetime)) - if ctx.verbose: await message.channel.send(f"Set antispam mute time to {format_duration(mutetime)}") + if ctx.verbose: await message.channel.send(f"Set antispam timeout to {format_duration(mutetime)}") class NoGuildError(Exception): @@ -669,15 +692,26 @@ async def add_joinrule(message: discord.Message, args: List[str], client: discor 'execute': antispam_set }, 'mutetime-set': { - 'alias': 'set-mutetime' + 'alias': 'set-antispam-timeout' }, - 'set-mutetime': + 'set-mutetime': { + 'alias': 'set-antispam-timeout' + }, + 'set-antispam-timeout': + { + 'pretty_name': 'set-antispam-timeout ', + 'description': 'Set how many seconds a person should be out for with antispam auto mute/timeout', + 'permission': 'administrator', + 'cache': 'regenerate', + 'execute': antispam_time_set, + }, + 'set-antispam-action': { - 'pretty_name': 'set-mutetime ', - 'description': 'Set how many seconds a person should be muted for with antispam automute', + 'pretty_name': 'set-antispam-action [timeout|mute]', + 'description': 'set whether to use mute or timeout for antispam triggers', 'permission': 'administrator', 'cache': 'regenerate', - 'execute': antispam_time_set + 'execute': set_antispam_command, }, 'add-regexnotifier': { @@ -697,4 +731,4 @@ async def add_joinrule(message: discord.Message, args: List[str], client: discor }, } -version_info: str = "1.2.14" +version_info: str = "2.0.0" diff --git a/cmds/cmd_moderation.py b/cmds/cmd_moderation.py index 981ccd6..2fdffcb 100644 --- a/cmds/cmd_moderation.py +++ b/cmds/cmd_moderation.py @@ -1,41 +1,18 @@ # Moderation commands # bredo, 2020 -import importlib - +from datetime import timedelta import discord, asyncio, json from dataclasses import dataclass -import lib_db_obfuscator - -importlib.reload(lib_db_obfuscator) -import lib_loaders - -importlib.reload(lib_loaders) import lib_parsers - -importlib.reload(lib_parsers) -import lib_constants - -importlib.reload(lib_constants) -import lib_goparsers - -importlib.reload(lib_goparsers) -import lib_compatibility - -importlib.reload(lib_compatibility) -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) import lib_sonnetcommands -importlib.reload(lib_sonnetcommands) - from lib_goparsers import ParseDurationSuper from lib_loaders import generate_infractionid, load_embed_color, load_message_config, embed_colors, datetime_now from lib_db_obfuscator import db_hlapi from lib_parsers import parse_user_member, format_duration, parse_core_permissions, parse_boolean_strict -from lib_compatibility import user_avatar_url +from lib_compatibility import user_avatar_url, to_snowflake, GuildMessageable from lib_sonnetconfig import BOT_NAME from lib_sonnetcommands import CommandCtx import lib_constants as constants @@ -182,7 +159,8 @@ async def process_infraction( ramfs: lexdpyk.ram_filesystem, infraction: bool = True, automod: bool = False, - modifiers: Optional[List[InfractionModifier]] = None + modifiers: Optional[List[InfractionModifier]] = None, + require_in_guild: bool = False ) -> InfractionInfo: if not message.guild or not isinstance(message.author, discord.Member): raise InfractionGenerationError("User is not member, or no guild") @@ -199,6 +177,10 @@ async def process_infraction( except lib_parsers.errors.user_parse_error: raise InfractionGenerationError("Could not parse user") + if require_in_guild and member is None: + await message.channel.send(f"User is not in guild (required to {i_type} user)") + raise InfractionGenerationError(f"Attempted to {i_type} a user but they were not a member") + local_conf_cache = load_message_config(message.guild.id, ramfs) # Test if user is a moderator @@ -212,7 +194,7 @@ async def process_infraction( if bool(int(local_conf_cache["moderator-protect"])): await message.channel.send(f"Cannot {i_type} specified user, user is a moderator+\n" f"(to disable this behavior see {get_help})") - raise InfractionGenerationError("Attempted to warn a moderator+ but mprotect was on") + raise InfractionGenerationError(f"Attempted to {i_type} a moderator+ but mprotect was on") # Test if user is self if member and moderator.id == member.id: @@ -306,7 +288,7 @@ async def kick_user(message: discord.Message, args: List[str], client: discord.C modifiers = parse_infraction_modifiers(message.guild, args) try: - member, _, reason, _, dm_sent, warn_text = await process_infraction(message, args, client, "kick", ramfs, automod=automod, modifiers=modifiers) + member, _, reason, _, dm_sent, warn_text = await process_infraction(message, args, client, "kick", ramfs, automod=automod, modifiers=modifiers, require_in_guild=True) except InfractionGenerationError: return 1 @@ -315,7 +297,7 @@ async def kick_user(message: discord.Message, args: List[str], client: discord.C try: if dm_sent: await dm_sent # Wait for dm to be sent before kicking - await message.guild.kick((member), reason=reason[:512]) + await message.guild.kick(to_snowflake(member), reason=reason[:512]) if warn_text is not None: await message.channel.send(warn_text) @@ -365,7 +347,7 @@ async def ban_user(message: discord.Message, args: List[str], client: discord.Cl try: if member and dm_sent: await dm_sent # Wait for dm to be sent before banning - await message.guild.ban(user, delete_message_days=delete_days, reason=reason[:512]) + await message.guild.ban(to_snowflake(user), delete_message_days=delete_days, reason=reason[:512]) except discord.errors.Forbidden: raise lib_sonnetcommands.CommandError(f"{BOT_NAME} does not have permission to ban this user.") @@ -405,7 +387,7 @@ async def unban_user(message: discord.Message, args: List[str], client: discord. # Attempt to unban user try: - await message.guild.unban(user, reason=reason[:512]) + await message.guild.unban(to_snowflake(user), reason=reason[:512]) except discord.errors.Forbidden: await message.channel.send(f"{BOT_NAME} does not have permission to unban this user.") return 1 @@ -446,13 +428,13 @@ async def softban_user(message: discord.Message, args: List[str], client: discor try: if member and dm_sent: await dm_sent # Wait for dm to be sent before banning - await message.guild.ban(user, delete_message_days=delete_days, reason=reason[:512]) + await message.guild.ban(to_snowflake(user), delete_message_days=delete_days, reason=reason[:512]) except discord.errors.Forbidden: raise lib_sonnetcommands.CommandError(f"{BOT_NAME} does not have permission to ban this user.") try: - await message.guild.unban(user, reason=reason[:512]) + await message.guild.unban(to_snowflake(user), reason=reason[:513]) except discord.errors.Forbidden: raise lib_sonnetcommands.CommandError(f"{BOT_NAME} does not have permission to unban this user.") except discord.errors.NotFound: @@ -498,20 +480,16 @@ async def sleep_and_unmute(guild: discord.Guild, member: discord.Member, infract db.unmute_user(infractionid=infractionID) try: - await member.remove_roles(mute_role) + await member.remove_roles(to_snowflake(mute_role)) except discord.errors.HTTPException: pass -async def mute_user(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> int: - if not message.guild: - return 1 - - ramfs = ctx.ramfs - automod = ctx.automod - verbose = ctx.verbose - - modifiers = parse_infraction_modifiers(message.guild, args) +def parse_duration_for_mutes(args: List[str], inf_name: str, /) -> Tuple[int, Optional[str]]: + """ + Parses a duration from args to get mutetime/timeouttime and returns string if a duration was not passed in the correct place but is valid + abstracts logic for mutes/timeouts + """ # Grab mute time if len(args) >= 2: @@ -531,6 +509,23 @@ async def mute_user(message: discord.Message, args: List[str], client: discord.C if mutetime is None: mutetime = 0 + duration_str = f"\n(No {inf_name} length was specified, but one of the reason items `{misplaced_duration}` is a valid duration, did you mean to {inf_name} for this length?)" if misplaced_duration is not None else None + + return mutetime, duration_str + + +async def mute_user(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> int: + if not message.guild: + return 1 + + ramfs = ctx.ramfs + automod = ctx.automod + verbose = ctx.verbose + + mutetime, duration_str = parse_duration_for_mutes(args, "mute") + + modifiers = parse_infraction_modifiers(message.guild, args) + # This ones for you, curl if not 0 <= mutetime < 60 * 60 * 256: mutetime = 0 @@ -543,7 +538,7 @@ async def mute_user(message: discord.Message, args: List[str], client: discord.C try: mute_role = await grab_mute_role(message, ramfs) - member, _, reason, infractionID, _, warn_text = await process_infraction(message, args, client, "mute", ramfs, automod=automod, modifiers=modifiers) + member, _, reason, infractionID, _, warn_text = await process_infraction(message, args, client, "mute", ramfs, automod=automod, modifiers=modifiers, require_in_guild=True) except (NoMuteRole, InfractionGenerationError): return 1 @@ -554,16 +549,15 @@ async def mute_user(message: discord.Message, args: List[str], client: discord.C # Attempt to mute user try: - await member.add_roles(mute_role) + await member.add_roles(to_snowflake(mute_role)) except discord.errors.Forbidden: await message.channel.send(f"{BOT_NAME} does not have permission to mute this user.") return 1 mod_str = f" with {','.join(m.title for m in modifiers)}" if modifiers else "" - duration_str = f"\n(No mute length was specified, but one of the reason items `{misplaced_duration}` is a valid duration, did you mean to mute for this length?)" if misplaced_duration is not None else "" if verbose and not mutetime: - await message.channel.send(f"Muted {member.mention} with ID {member.id}{mod_str} for {reason}{duration_str}", allowed_mentions=discord.AllowedMentions.none()) + await message.channel.send(f"Muted {member.mention} with ID {member.id}{mod_str} for {reason}{duration_str or ''}", allowed_mentions=discord.AllowedMentions.none()) if warn_text is not None: await message.channel.send(warn_text) @@ -607,7 +601,7 @@ async def unmute_user(message: discord.Message, args: List[str], client: discord try: mute_role = await grab_mute_role(message, ramfs) - member, _, reason, _, _, _ = await process_infraction(message, args, client, "unmute", ramfs, infraction=False, automod=automod) + member, _, reason, _, _, _ = await process_infraction(message, args, client, "unmute", ramfs, infraction=False, automod=automod, require_in_guild=True) except (InfractionGenerationError, NoMuteRole): return 1 @@ -617,7 +611,7 @@ async def unmute_user(message: discord.Message, args: List[str], client: discord # Attempt to unmute user try: - await member.remove_roles(mute_role) + await member.remove_roles(to_snowflake(mute_role)) except discord.errors.Forbidden: await message.channel.send(f"{BOT_NAME} does not have permission to unmute this user.") return 1 @@ -630,6 +624,66 @@ async def unmute_user(message: discord.Message, args: List[str], client: discord return 0 +async def timeout_user(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> int: + if not message.guild: + return 1 + + mutetime, duration_str = parse_duration_for_mutes(args, "timeout") + + if mutetime >= ((60 * 60) * 24) * 28 or mutetime == 0: + mutetime = ((60 * 60) * 24) * 28 + + modifiers = parse_infraction_modifiers(message.guild, args) + + try: + member, _, reason, _, _, warn_text = await process_infraction(message, args, client, "timeout", ctx.ramfs, automod=ctx.automod, modifiers=modifiers, require_in_guild=True) + except InfractionGenerationError: + return 1 + + if not member: + raise lib_sonnetcommands.CommandError("ERROR: User not in guild") + + try: + await member.timeout(timedelta(seconds=mutetime), reason=reason[:512]) + except discord.errors.Forbidden: + raise lib_sonnetcommands.CommandError(f"{BOT_NAME} does not have permission to timeout this user.") + + mod_str = f" with {','.join(m.title for m in modifiers)}" if modifiers else "" + + if ctx.verbose: + await message.channel.send( + f"Timed out {member.mention} with ID {member.id}{mod_str} for {format_duration(mutetime)} for {reason}{duration_str or ''}", allowed_mentions=discord.AllowedMentions.none() + ) + + if warn_text is not None: + await message.channel.send(warn_text) + + return 0 + + +async def un_timeout_user(message: discord.Message, args: List[str], client: discord.Client, ctx: CommandCtx) -> int: + if not message.guild: + return 1 + + try: + member, _, reason, _, _, _ = await process_infraction(message, args, client, "untimeout", ctx.ramfs, infraction=False, automod=ctx.automod, require_in_guild=True) + except InfractionGenerationError: + return 1 + + if not member: + raise lib_sonnetcommands.CommandError("ERROR: User is not a member") + + # Attempt to untimeout user + try: + await member.timeout(None, reason=reason[:512]) + except discord.errors.Forbidden: + raise lib_sonnetcommands.CommandError(f"{BOT_NAME} does not have permission to untimeout this user.") + + if ctx.verbose: + await message.channel.send(f"Removed timeout for {member.mention} with ID {member.id} for {reason}", allowed_mentions=discord.AllowedMentions.none()) + return 0 + + class purger: __slots__ = "user_id", "message_id" @@ -674,7 +728,7 @@ async def purge_cli(message: discord.Message, args: List[str], client: discord.C raise lib_sonnetcommands.CommandError("User does not exist") try: - purged = await cast(discord.TextChannel, message.channel).purge(limit=limit, check=ucheck) + purged = await cast(GuildMessageable, message.channel).purge(limit=limit, check=ucheck) await message.channel.send(f"Purged {len(purged)} message{'s' * (len(purged)!=1)}, initiated by {message.author.mention}", allowed_mentions=discord.AllowedMentions.none()) except discord.errors.Forbidden: raise lib_sonnetcommands.CommandError("ERROR: Bot lacks perms to purge") @@ -708,7 +762,7 @@ async def purge_cli(message: discord.Message, args: List[str], client: discord.C 'execute': ban_user }, 'unban': { - 'pretty_name': 'unban ', + 'pretty_name': 'unban [reason]', 'description': 'Unban a user, does not dm user', 'permission': 'moderator', 'execute': unban_user @@ -727,11 +781,27 @@ async def purge_cli(message: discord.Message, args: List[str], client: discord.C 'execute': mute_user }, 'unmute': { - 'pretty_name': 'unmute ', + 'pretty_name': 'unmute [reason]', 'description': 'Unmute a user, does not dm user', 'permission': 'moderator', 'execute': unmute_user }, + "timeout": + { + 'pretty_name': 'timeout [+modifiers] [time[h|m|S]] [reason]', + 'description': 'Timeout a member, defaults to longest timeout possible (28 days)', + 'permission': "moderator", + "execute": timeout_user, + }, + "untimeout": { + 'alias': "remove-timeout", + }, + "remove-timeout": { + 'pretty_name': 'remove-timeout [reason]', + 'description': 'Remove a timeout on a member', + 'permission': "moderator", + "execute": un_timeout_user, + }, 'purge': { 'pretty_name': 'purge [user]', @@ -744,4 +814,4 @@ async def purge_cli(message: discord.Message, args: List[str], client: discord.C } } -version_info: str = "1.2.14" +version_info: str = "2.0.0" diff --git a/cmds/cmd_reactionroles.py b/cmds/cmd_reactionroles.py index 3910920..b3d96ca 100644 --- a/cmds/cmd_reactionroles.py +++ b/cmds/cmd_reactionroles.py @@ -15,10 +15,14 @@ import lib_loaders importlib.reload(lib_loaders) +import lib_compatibility + +importlib.reload(lib_compatibility) from lib_db_obfuscator import db_hlapi from lib_parsers import parse_channel_message_noexcept from lib_loaders import load_embed_color, embed_colors +from lib_compatibility import to_snowflake from typing import List, Any, Final, Dict, Union @@ -106,8 +110,11 @@ async def try_add_reaction(message: discord.Message, emoji: Union[discord.Emoji, async def try_remove_reaction(me: discord.Client, message: discord.Message, emoji: Union[discord.Emoji, str]) -> None: + if not me.user: + return + try: - await message.remove_reaction(emoji, me.user) + await message.remove_reaction(emoji, to_snowflake(me.user)) except discord.errors.Forbidden: # raise non permission errors pass @@ -369,4 +376,4 @@ async def rr_purge(message: discord.Message, args: List[str], client: discord.Cl }, } -version_info: str = "1.2.14" +version_info: str = "2.0.0" diff --git a/cmds/cmd_scripting.py b/cmds/cmd_scripting.py index c2d3f42..e719589 100644 --- a/cmds/cmd_scripting.py +++ b/cmds/cmd_scripting.py @@ -110,7 +110,7 @@ async def sonnet_sh(message: discord.Message, args: List[str], client: discord.C try: suc = (await cmd.execute_ctx(message, arguments, client, newctx)) or 0 except lib_sonnetcommands.CommandError as ce: - await message.channel.send(ce) + await message.channel.send(str(ce)) suc = 1 # Stop processing if error @@ -222,7 +222,7 @@ async def sonnet_map(message: discord.Message, args: List[str], client: discord. try: suc = (await cmd.execute_ctx(message, arguments, client, newctx)) or 0 except lib_sonnetcommands.CommandError as ce: - await message.channel.send(ce) + await message.channel.send(str(ce)) suc = 1 if suc != 0: @@ -247,7 +247,7 @@ async def wrapasyncerror(cmd: SonnetCommand, message: discord.Message, args: Lis await cmd.execute_ctx(message, args, client, ctx) except lib_sonnetcommands.CommandError as ce: # catch CommandError to print message try: - await message.channel.send(ce) + await message.channel.send(str(ce)) except discord.errors.Forbidden: pass @@ -430,4 +430,4 @@ async def sleep_for(message: discord.Message, args: List[str], client: discord.C }, } -version_info: str = "1.2.13" +version_info: str = "2.0.0" diff --git a/cmds/cmd_utils.py b/cmds/cmd_utils.py index c9a7f30..fa20ade 100644 --- a/cmds/cmd_utils.py +++ b/cmds/cmd_utils.py @@ -40,11 +40,11 @@ importlib.reload(lib_datetimeplus) -from typing import Any, Final, List, Optional, Tuple, Dict, cast +from typing import Any, Final, List, Optional, Tuple, Dict import lib_constants as constants import lib_lexdpyk_h as lexdpyk -from lib_compatibility import discord_datetime_now, user_avatar_url +from lib_compatibility import discord_datetime_now, user_avatar_url, is_guild_messageable from lib_db_obfuscator import db_hlapi from lib_loaders import embed_colors, load_embed_color from lib_parsers import (parse_boolean_strict, parse_permissions, parse_core_permissions, parse_user_member_noexcept, parse_channel_message_noexcept, generate_reply_field, grab_files) @@ -95,7 +95,7 @@ def parsedate(indata: Optional[datetime]) -> str: def _get_highest_perm(message: discord.Message, member: discord.Member, conf_cache: Dict[str, Any]) -> str: - if not isinstance(message.channel, discord.TextChannel): + if not is_guild_messageable(message.channel): return "everyone" highest = "everyone" @@ -124,8 +124,10 @@ async def profile_function(message: discord.Message, args: List[str], client: di "invisible": "\U000026AB (offline)" } + avatar_asset = user.display_avatar if member is None else member.display_avatar + embed: Final = discord.Embed(title="User Information", description=f"User information for {user.mention}:", color=load_embed_color(message.guild, embed_colors.primary, ctx.ramfs)) - embed.set_thumbnail(url=user_avatar_url(user)) + embed.set_thumbnail(url=avatar_asset.url) embed.add_field(name="Username", value=str(user), inline=True) embed.add_field(name="User ID", value=str(user.id), inline=True) if member: @@ -154,10 +156,24 @@ async def avatar_function(message: discord.Message, args: List[str], client: dis if not message.guild: return 1 - user, _ = await parse_user_member_noexcept(message, args, client, default_self=True) + p = Parser("avatar") + global_flag: lib_tparse.Promise[bool] = p.add_arg(["-g", "--global"], lib_tparse.store_true, flag=True, helpstr="whether or not to grab global avatar") + + try: + p.parse(args, stderr=io.StringIO(), exit_on_fail=False, lazy=True, consume=True) + except lib_tparse.ParseFailureError: + # this is a programming error because lazy is set and no value parsing happens + raise + + user, member = await parse_user_member_noexcept(message, args, client, default_self=True) + + global_avatar = user.avatar if user.avatar is not None else user.default_avatar + guild_avatar = member.display_avatar if member is not None else user.display_avatar + + avatar_asset = global_avatar if global_flag.get(False) else guild_avatar embed = discord.Embed(description=f"{user.mention}'s Avatar", color=load_embed_color(message.guild, embed_colors.primary, kwargs["ramfs"])) - embed.set_image(url=user_avatar_url(user)) + embed.set_image(url=avatar_asset.url) embed.timestamp = Time.now().as_datetime() try: await message.channel.send(embed=embed) @@ -416,7 +432,8 @@ async def grab_guild_info(message: discord.Message, args: List[str], client: dis guild_embed.add_field(name="Creation Date:", value=parsedate(guild.created_at)) guild_embed.set_footer(text=f"gid: {guild.id}") - guild_embed.set_thumbnail(url=cast(str, guild.icon_url)) + if guild.icon: + guild_embed.set_thumbnail(url=guild.icon.url) try: await message.channel.send(embed=guild_embed) @@ -594,10 +611,8 @@ async def coinflip(message: discord.Message, args: List[str], client: discord.Cl 'alias': 'avatar' }, 'avatar': { - 'pretty_name': 'avatar [user]', - 'description': 'Get avatar of a user', - 'permission': 'everyone', - 'cache': 'keep', + 'pretty_name': 'avatar [user] [--global]', + 'description': 'Get avatar of a user, returns guild avatar if it exists unless --global is specified', 'execute': avatar_function }, 'server-info': { @@ -634,4 +649,4 @@ async def coinflip(message: discord.Message, args: List[str], client: discord.Cl } } -version_info: str = "1.2.14" +version_info: str = "2.0.0" diff --git a/dlibs/dlib_messages.py b/dlibs/dlib_messages.py index ec7ea51..79a24cb 100644 --- a/dlibs/dlib_messages.py +++ b/dlibs/dlib_messages.py @@ -1,39 +1,14 @@ # Dynamic libraries (editable at runtime) for message handling # Ultrabear 2020 -import importlib - import time, asyncio, os, hashlib, string, io, gzip import warnings import copy as pycopy import discord, lz4.frame -import lib_db_obfuscator - -importlib.reload(lib_db_obfuscator) -import lib_parsers - -importlib.reload(lib_parsers) -import lib_loaders - -importlib.reload(lib_loaders) -import lib_encryption_wrapper - -importlib.reload(lib_encryption_wrapper) -import lib_lexdpyk_h - -importlib.reload(lib_lexdpyk_h) -import lib_constants - -importlib.reload(lib_constants) -import lib_compatibility - -importlib.reload(lib_compatibility) import lib_sonnetcommands -importlib.reload(lib_sonnetcommands) - from lib_db_obfuscator import db_hlapi from lib_loaders import load_message_config, inc_statistics_better, load_embed_color, embed_colors, datetime_now from lib_parsers import parse_blacklist, parse_skip_message, parse_permissions, grab_files, generate_reply_field, parse_boolean_strict @@ -41,7 +16,7 @@ from lib_compatibility import user_avatar_url from lib_sonnetcommands import SonnetCommand, CommandCtx, CallCtx, ExecutableCtxT -from typing import List, Any, Dict, Optional, Callable, Tuple, Final, Literal, TypedDict, NewType, Union, cast +from typing import List, Any, Dict, Optional, Callable, Tuple, Final, Literal, TypedDict, NewType, Union, Awaitable, cast import lib_lexdpyk_h as lexdpyk import lib_constants as constants @@ -50,7 +25,7 @@ async def catch_logging_error(channel: discord.TextChannel, contents: discord.Embed, files: Optional[List[discord.File]] = None) -> None: try: - await channel.send(embed=contents, files=files) + await channel.send(embed=contents, files=(files or [])) except discord.errors.Forbidden: pass except discord.errors.HTTPException: @@ -273,7 +248,7 @@ async def on_message_edit(old_message: discord.Message, message: discord.Message asyncio.create_task(attempt_message_delete(message)) execargs: Final = [str(message.author.id), "[AUTOMOD]", ", ".join(infraction_type), "Blacklist"] - await warn_missing(kctx.command_modules[1], mconf["blacklist-action"])(message, execargs, client, command_ctx) + await catch_ce(message, warn_missing(kctx.command_modules[1], mconf["blacklist-action"])(message, execargs, client, command_ctx)) if notify: asyncio.create_task(grab_an_adult(message, message.guild, client, mconf, kctx.ramfs)) @@ -435,6 +410,20 @@ async def dummy(message: discord.Message, args: List[str], client: discord.Clien return dummy +async def catch_ce(err_rsp: discord.Message, promise: Awaitable[Any]) -> None: + + try: + await promise + except lib_sonnetcommands.CommandError as ce: + try: + await err_rsp.channel.send(str(ce)) + except discord.errors.HTTPException: + pass + except discord.errors.Forbidden: + # ignore permission errors + pass + + @lexdpyk.ToKernelArgs async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) -> None: @@ -450,6 +439,8 @@ async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) return elif not message.guild: return + elif not client.user: + return inc_statistics_better(message.guild.id, "on-message", kernel_args.kernel_ramfs) @@ -489,7 +480,7 @@ async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) message_deleted = True asyncio.create_task(attempt_message_delete(message)) execargs = [str(message.author.id), "[AUTOMOD]", ", ".join(infraction_type), "Blacklist"] - asyncio.create_task(warn_missing(command_modules_dict, mconf["blacklist-action"])(message, execargs, client, automod_ctx)) + asyncio.create_task(catch_ce(message, warn_missing(command_modules_dict, mconf["blacklist-action"])(message, execargs, client, automod_ctx))) if spammer: message_deleted = True @@ -497,7 +488,7 @@ async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) with db_hlapi(message.guild.id) as db: if not db.is_muted(userid=message.author.id): execargs = [str(message.author.id), mconf["antispam-time"], "[AUTOMOD]", spamstr] - asyncio.create_task(warn_missing(command_modules_dict, "mute")(message, execargs, client, automod_ctx)) + asyncio.create_task(catch_ce(message, warn_missing(command_modules_dict, mconf["antispam-action"])(message, execargs, client, automod_ctx))) if notify: asyncio.create_task(grab_an_adult(message, message.guild, client, mconf, ramfs)) @@ -561,7 +552,7 @@ async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) await cmd.execute_ctx(message, arguments, client, command_ctx) except lib_sonnetcommands.CommandError as ce: try: - await message.channel.send(ce) + await message.channel.send(str(ce)) except discord.errors.Forbidden: pass @@ -594,4 +585,4 @@ async def on_message(message: discord.Message, kernel_args: lexdpyk.KernelArgs) "on-message-delete": on_message_delete, } -version_info: Final = "1.2.14" +version_info: Final = "2.0.0" diff --git a/dlibs/dlib_reactionroles.py b/dlibs/dlib_reactionroles.py index 6444b87..9b1adf9 100644 --- a/dlibs/dlib_reactionroles.py +++ b/dlibs/dlib_reactionroles.py @@ -11,9 +11,13 @@ import lib_lexdpyk_h importlib.reload(lib_lexdpyk_h) +import lib_compatibility + +importlib.reload(lib_compatibility) from lib_loaders import load_message_config, inc_statistics_better from lib_lexdpyk_h import ToKernelArgs, KernelArgs +from lib_compatibility import to_snowflake from typing import Dict, Any, Union, Optional, Tuple @@ -65,16 +69,17 @@ async def on_raw_reaction_add(payload: discord.RawReactionActionEvent, kargs: Ke client: discord.Client = kargs.client rrconf: Optional[Dict[str, Dict[str, int]]] = load_message_config(payload.guild_id, kargs.ramfs, datatypes=reactionrole_types)["reaction-role-data"] - # do not give reactionroles to self - if payload.user_id == client.user.id: - return + if client.user: + # do not give reactionroles to self + if payload.user_id == client.user.id: + return if rrconf: opt = await get_role_from_emojiname(payload, client, rrconf) if opt is not None: try: member, role = opt - await member.add_roles(role) + await member.add_roles(to_snowflake(role)) except discord.errors.Forbidden: pass @@ -89,16 +94,17 @@ async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent, kargs: client: discord.Client = kargs.client rrconf: Optional[Dict[str, Dict[str, int]]] = load_message_config(payload.guild_id, kargs.ramfs, datatypes=reactionrole_types)["reaction-role-data"] - # do not remove reactionroles from self - if payload.user_id == client.user.id: - return + if client.user: + # do not remove reactionroles from self + if payload.user_id == client.user.id: + return if rrconf: opt = await get_role_from_emojiname(payload, client, rrconf) if opt is not None: try: member, role = opt - await member.remove_roles(role) + await member.remove_roles(to_snowflake(role)) except discord.errors.Forbidden: pass @@ -110,4 +116,4 @@ async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent, kargs: "on-raw-reaction-remove": on_raw_reaction_remove, } -version_info = "1.2.14" +version_info = "2.0.0" diff --git a/dlibs/dlib_startup.py b/dlibs/dlib_startup.py index 8206ade..56d8e41 100644 --- a/dlibs/dlib_startup.py +++ b/dlibs/dlib_startup.py @@ -11,9 +11,13 @@ import lib_loaders importlib.reload(lib_loaders) +import lib_compatibility + +importlib.reload(lib_compatibility) from lib_db_obfuscator import db_hlapi from lib_loaders import inc_statistics_better, datetime_now +from lib_compatibility import to_snowflake from typing import Dict, Callable, Any, List, Tuple @@ -27,7 +31,7 @@ async def attempt_unmute(Client: discord.Client, mute_entry: Tuple[str, str, str if (guild := Client.get_guild(int(mute_entry[0]))) and mute_role_id: if (user := guild.get_member(int(mute_entry[2]))) and (mute_role := guild.get_role(int(mute_role_id))): try: - await user.remove_roles(mute_role) + await user.remove_roles(to_snowflake(mute_role)) except discord.errors.Forbidden: pass @@ -40,7 +44,7 @@ async def on_ready(**kargs: Any) -> None: print(f'{Client.user} has connected to Discord!') # Warn if user is not bot - if not Client.user.bot: + if Client.user and not Client.user.bot: print("WARNING: The connected account is not a bot, as it is against ToS we do not condone user botting") # bot start time check to not reparse timers on network disconnect @@ -84,4 +88,4 @@ async def on_guild_join(guild: discord.Guild, **kargs: Any) -> None: commands: Dict[str, Callable[..., Any]] = {"on-ready": on_ready, "on-guild-join": on_guild_join} -version_info: str = "1.2.10" +version_info: str = "2.0.0" diff --git a/dlibs/dlib_userupdate.py b/dlibs/dlib_userupdate.py index 08cb37b..dd9ae91 100644 --- a/dlibs/dlib_userupdate.py +++ b/dlibs/dlib_userupdate.py @@ -25,7 +25,7 @@ from typing import Any, Dict, List, Optional, Union import lib_lexdpyk_h as lexdpyk -from lib_compatibility import (discord_datetime_now, has_default_avatar, user_avatar_url) +from lib_compatibility import (discord_datetime_now, has_default_avatar, user_avatar_url, to_snowflake) from lib_db_obfuscator import db_hlapi from lib_loaders import (datetime_now, embed_colors, inc_statistics_better, load_embed_color, load_message_config) from lib_parsers import parse_boolean_strict @@ -103,7 +103,7 @@ async def try_mute_on_rejoin(member: discord.Member, db: db_hlapi, client: disco success: bool try: - await member.add_roles(mute_role) + await member.add_roles(to_snowflake(mute_role)) success = True except discord.errors.Forbidden: success = False @@ -116,8 +116,11 @@ async def try_mute_on_rejoin(member: discord.Member, db: db_hlapi, client: disco if log and (channel := client.get_channel(int(log))): if isinstance(channel, discord.TextChannel): - muted_embed = discord.Embed(title=f"Notify on muted member join: {member}", description=f"This user has an entry in the mute database and {stringcases[success]}.") - muted_embed.color = load_embed_color(member.guild, embed_colors.primary, ramfs) + muted_embed = discord.Embed( + title=f"Notify on muted member join: {member}", + description=f"This user has an entry in the mute database and {stringcases[success]}.", + color=load_embed_color(member.guild, embed_colors.primary, ramfs) + ) muted_embed.set_footer(text=f"uid: {member.id}") await catch_logging_error(channel, muted_embed) @@ -202,4 +205,4 @@ async def on_member_remove(member: discord.Member, **kargs: Any) -> None: "on-member-remove": on_member_remove, } -version_info: str = "1.2.14" +version_info: str = "2.0.0" diff --git a/libs/lib_compatibility.py b/libs/lib_compatibility.py index 408eefd..0df5417 100644 --- a/libs/lib_compatibility.py +++ b/libs/lib_compatibility.py @@ -5,7 +5,8 @@ import discord import datetime -from typing import Union, Dict, Callable, cast +from typing import Union, Dict, Callable, Protocol, TypeVar, cast +from typing_extensions import TypeGuard _releaselevel: int = discord.version_info[0] @@ -15,6 +16,9 @@ "default_avatar_url", "has_default_avatar", "discord_datetime_now", + "to_snowflake", + "GuildMessageable", + "is_guild_messageable", ] @@ -69,7 +73,7 @@ def user_avatar_url(user: Union[discord.User, discord.Member]) -> str: # 1.7: user.avatar_url -> Asset (supports __str__()) # 2.0: user.display_avatar.url -> str - return _avatar_url_func(user) + return user.display_avatar.url def default_avatar_url(user: Union[discord.User, discord.Member]) -> str: @@ -81,7 +85,7 @@ def default_avatar_url(user: Union[discord.User, discord.Member]) -> str: :raises: AttributeError - Failed to get the avatar url (programming error) """ - return _default_avatar_url_func(user) + return user.default_avatar.url def has_default_avatar(user: Union[discord.User, discord.Member]) -> bool: @@ -92,7 +96,7 @@ def has_default_avatar(user: Union[discord.User, discord.Member]) -> bool: :returns: bool - if the user has a default avatar """ - return _default_avatar_url_func(user) == _avatar_url_func(user) + return user.avatar is None # Returns either an aware or unaware @@ -108,4 +112,34 @@ def discord_datetime_now() -> datetime.datetime: # 1.7: datetime naive # 2.0: datetime aware - return _datetime_now_func() + return datetime.datetime.now(datetime.timezone.utc) + + +class _WeakSnowflake(Protocol): + id: int + + +SF = TypeVar("SF", bound=_WeakSnowflake) + + +def to_snowflake(v: SF, /) -> SF: + """ + ~~Casts any snowflake compatible type into something satisfying the discord.py Showflake interface, bypassing a interface bug with mypy~~ + This is patched as of dpy2.0.1, it now returns the type passed in as long as it satisfies the bound of a snowflake + """ + return v + + +GuildMessageable = Union[discord.TextChannel, discord.Thread, discord.VoiceChannel] + +_concrete_channels = Union[discord.TextChannel, discord.VoiceChannel, discord.CategoryChannel, discord.StageChannel, discord.ForumChannel, discord.Thread, discord.DMChannel, discord.GroupChannel, + discord.PartialMessageable] +_abstract_base_class_channels = Union[discord.abc.PrivateChannel, discord.abc.GuildChannel] + + +def is_guild_messageable(v: Union[_concrete_channels, _abstract_base_class_channels], /) -> TypeGuard[GuildMessageable]: + """ + Returns True if the channel type passed is within a guild and messageable + """ + + return isinstance(v, (discord.TextChannel, discord.Thread, discord.VoiceChannel)) diff --git a/libs/lib_db_obfuscator.py b/libs/lib_db_obfuscator.py index 80eb9ac..002ed3d 100644 --- a/libs/lib_db_obfuscator.py +++ b/libs/lib_db_obfuscator.py @@ -4,9 +4,4 @@ # Explicitly export db_hlapi __all__ = ["db_hlapi"] -import importlib - -import lib_sonnetdb - -importlib.reload(lib_sonnetdb) from lib_sonnetdb import db_hlapi diff --git a/libs/lib_encryption_wrapper.py b/libs/lib_encryption_wrapper.py index 02804a0..a06a219 100644 --- a/libs/lib_encryption_wrapper.py +++ b/libs/lib_encryption_wrapper.py @@ -6,7 +6,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import hashes, hmac -from typing import Generator, Union, Protocol +from typing import Generator, Union, List, Protocol, runtime_checkable class errors: @@ -47,6 +47,12 @@ def close(self) -> None: ... +@runtime_checkable +class _Flushable(Protocol): + def flush(self) -> None: + ... + + class encrypted_writer: __slots__ = "cipher", "encryptor_module", "HMACencrypt", "rawfile", "buf" @@ -115,6 +121,10 @@ def finalize(self) -> None: self.rawfile.close() self.encryptor_module.finalize() + def flush(self) -> None: + if isinstance(self.rawfile, _Flushable): + self.rawfile.flush() + def close(self) -> None: self.finalize() @@ -201,7 +211,7 @@ def read(self, size: int = -1) -> bytes: if size == -1: if self.pointer == 0: # Return entire file if pointer is at 0 - datamap = [] + datamap: List[bytes] = [] while a := self.rawfile.read(2): datamap.append(self._grab_amount(int.from_bytes(a, "little"))) return b"".join(datamap) diff --git a/libs/lib_goparsers.py b/libs/lib_goparsers.py index 15325d3..14233c6 100644 --- a/libs/lib_goparsers.py +++ b/libs/lib_goparsers.py @@ -11,20 +11,11 @@ "GetVersion", ] -import importlib - import ctypes as _ctypes import subprocess as _subprocess from functools import lru_cache -from typing import Optional - -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) -import lib_datetimeplus - -importlib.reload(lib_datetimeplus) +from typing import Optional, List from lib_sonnetconfig import GOLIB_LOAD, GOLIB_VERSION from lib_datetimeplus import Duration as _Duration @@ -127,7 +118,7 @@ def GenerateCacheFile(fin: str, fout: str) -> None: with open(fin, "rb") as words: maxval = 0 - structured_data = [] + structured_data: List[bytes] = [] for byte in words.read().split(b"\n"): if byte and not len(byte) > 85 and not b"\xc3" in byte: diff --git a/libs/lib_lexdpyk_h.py b/libs/lib_lexdpyk_h.py index 2e748b2..49db3d6 100644 --- a/libs/lib_lexdpyk_h.py +++ b/libs/lib_lexdpyk_h.py @@ -100,16 +100,14 @@ def newfunc(*args: Any, **kwargs: Any) -> Coroutine[Any, Any, Any]: class _BotOwners: __slots__ = "owners", - def __init__(self, unknown: object) -> None: + def __init__(self, unknown: Union[List[Union[str, int]], Tuple[Union[str, int], ...], str, int]) -> None: self.owners: Set[int] if isinstance(unknown, (int, str)): self.owners = set([int(unknown)]) if unknown else set() - elif isinstance(unknown, (list, tuple)): - self.owners = set(map(int, unknown)) else: - self.owners = set() + self.owners = set(map(int, unknown)) def is_owner(self, user: Union[discord.User, discord.Member]) -> bool: """ diff --git a/libs/lib_loaders.py b/libs/lib_loaders.py index b048024..dc02388 100644 --- a/libs/lib_loaders.py +++ b/libs/lib_loaders.py @@ -3,27 +3,12 @@ from __future__ import annotations -import importlib - import discord import random, ctypes, time, io, json, pickle, threading, warnings import datetime import subprocess -import lib_db_obfuscator - -importlib.reload(lib_db_obfuscator) -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) -import lib_goparsers - -importlib.reload(lib_goparsers) -import lib_datetimeplus - -importlib.reload(lib_datetimeplus) - from lib_goparsers import GenerateCacheFile from lib_db_obfuscator import db_hlapi from lib_sonnetconfig import CLIB_LOAD, GLOBAL_PREFIX, BLACKLIST_ACTION @@ -82,8 +67,8 @@ def directBinNumber(inData: int, length: int) -> Tuple[int, ...]: "csv": [["word-blacklist", ""], ["filetype-blacklist", ""], ["word-in-word-blacklist", ""], ["url-blacklist", ""], ["antispam", "3,2"], ["char-antispam", "2,2,1000"]], "text": [ - ["prefix", GLOBAL_PREFIX], ["blacklist-action", BLACKLIST_ACTION], ["blacklist-whitelist", ""], ["regex-notifier-log", ""], ["admin-role", ""], ["moderator-role", ""], - ["antispam-time", "20"], ["moderator-protect", "0"] + ["prefix", GLOBAL_PREFIX], ["blacklist-action", BLACKLIST_ACTION], ["antispam-action", "mute"], ["blacklist-whitelist", ""], ["regex-notifier-log", ""], ["admin-role", ""], + ["moderator-role", ""], ["antispam-time", "20"], ["moderator-protect", "0"] ], 0: "sonnet_default" } diff --git a/libs/lib_mdb_handler.py b/libs/lib_mdb_handler.py index d51dd21..fd2eb22 100644 --- a/libs/lib_mdb_handler.py +++ b/libs/lib_mdb_handler.py @@ -199,5 +199,3 @@ def __exit__(self, err_type: Any, err_value: Any, err_traceback: Any) -> None: self.con.commit() self.con.close() self.closed = True - if err_type: - raise err_type(err_value) diff --git a/libs/lib_parsers.py b/libs/lib_parsers.py index 3bc351a..a0e60a7 100644 --- a/libs/lib_parsers.py +++ b/libs/lib_parsers.py @@ -7,26 +7,14 @@ import lz4.frame, discord, os, json, hashlib, io, warnings, math -import lib_db_obfuscator - -importlib.reload(lib_db_obfuscator) -import lib_encryption_wrapper - -importlib.reload(lib_encryption_wrapper) -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) -import lib_constants - -importlib.reload(lib_constants) import lib_sonnetcommands - -importlib.reload(lib_sonnetcommands) +import lib_encryption_wrapper from lib_sonnetconfig import REGEX_VERSION from lib_db_obfuscator import db_hlapi from lib_encryption_wrapper import encrypted_reader import lib_constants as constants +from lib_compatibility import is_guild_messageable, GuildMessageable from typing import Callable, Iterable, Optional, Any, Tuple, Dict, Union, List, TypeVar, Literal, overload, cast import lib_lexdpyk_h as lexdpyk @@ -199,8 +187,9 @@ def parse_skip_message(Client: discord.Client, message: discord.Message, *, allo """ # Make sure we don't start a feedback loop. - if message.author.id == Client.user.id: - return True + if Client.user: + if message.author.id == Client.user.id: + return True # only check if we are not allowing bots if not allow_bots: @@ -262,7 +251,7 @@ async def update_log_channel(message: discord.Message, args: list[str], client: if not message.guild: raise errors.log_channel_update_error("ERROR: No guild") - if not isinstance(message.channel, discord.TextChannel): + if not is_guild_messageable(message.channel): raise errors.log_channel_update_error("ERROR: Wrong channel context") if args: @@ -294,9 +283,10 @@ async def update_log_channel(message: discord.Message, args: list[str], client: await message.channel.send(constants.sonnet.error_channel.invalid) raise errors.log_channel_update_error("Channel is not a valid channel") + # we may only log to textchannels, not threads as they may be deleted automatically if not isinstance(discord_channel, discord.TextChannel): await message.channel.send(constants.sonnet.error_channel.wrongType) - raise errors.log_channel_update_error("Channel is not a valid channel") + raise errors.log_channel_update_error("Channel is not a TextChannel") if discord_channel.guild.id != message.channel.guild.id: await message.channel.send(constants.sonnet.error_channel.scope) @@ -317,21 +307,21 @@ def _parse_role_perms(author: discord.Member, permrole: str) -> bool: @overload -def parse_core_permissions(channel: discord.TextChannel, member: discord.Member, mconf: Dict[str, str], perms: Literal["everyone"]) -> Literal[True]: +def parse_core_permissions(channel: GuildMessageable, member: discord.Member, mconf: Dict[str, str], perms: Literal["everyone"]) -> Literal[True]: ... @overload -def parse_core_permissions(channel: discord.TextChannel, member: discord.Member, mconf: Dict[str, str], perms: Literal["moderator", "administrator", "owner"]) -> bool: +def parse_core_permissions(channel: GuildMessageable, member: discord.Member, mconf: Dict[str, str], perms: Literal["moderator", "administrator", "owner"]) -> bool: ... @overload -def parse_core_permissions(channel: discord.TextChannel, member: discord.Member, mconf: Dict[str, str], perms: str) -> Optional[bool]: +def parse_core_permissions(channel: GuildMessageable, member: discord.Member, mconf: Dict[str, str], perms: str) -> Optional[bool]: ... -def parse_core_permissions(channel: discord.TextChannel, member: discord.Member, mconf: Dict[str, str], perms: str) -> Optional[bool]: +def parse_core_permissions(channel: GuildMessageable, member: discord.Member, mconf: Dict[str, str], perms: str) -> Optional[bool]: """ Parse permissions of a given TextChannel and Member, only parses core permissions (everyone,moderator,administrator,owner) and does not have verbosity This is a lightweight alternative to parse_permissions for parsing simple permissions, while not sufficient for full command permission parsing. @@ -366,7 +356,7 @@ async def parse_permissions(message: discord.Message, mconf: Dict[str, str], per :returns: bool """ - if not isinstance(message.channel, discord.TextChannel): + if not is_guild_messageable(message.channel): # Perm check called outside a guild return False @@ -597,7 +587,7 @@ async def parse_channel_message_noexcept(message: discord.Message, args: list[st if not discord_channel: raise lib_sonnetcommands.CommandError(constants.sonnet.error_channel.invalid) - if not isinstance(discord_channel, discord.TextChannel): + if not is_guild_messageable(discord_channel): raise lib_sonnetcommands.CommandError(constants.sonnet.error_channel.scope) if discord_channel.guild.id != message.guild.id: @@ -624,7 +614,7 @@ async def parse_channel_message(message: discord.Message, args: List[str], clien try: return await parse_channel_message_noexcept(message, args, client) except lib_sonnetcommands.CommandError as ce: - await message.channel.send(ce) + await message.channel.send(str(ce)) raise errors.message_parse_failure(ce) @@ -632,10 +622,10 @@ async def parse_channel_message(message: discord.Message, args: List[str], clien # should return a union of many types but for now only handle discord.Message -async def _guess_id_type(message: discord.Message, mystery_id: int) -> Optional[Union[discord.Message, discord.Role, discord.TextChannel]]: +async def _guess_id_type(message: discord.Message, mystery_id: int) -> Optional[Union[discord.Message, discord.Role, GuildMessageable]]: # hot path current channel id - if message.channel.id == mystery_id and isinstance(message.channel, discord.TextChannel): + if message.channel.id == mystery_id and is_guild_messageable(message.channel): return message.channel # asserts guild @@ -647,11 +637,11 @@ async def _guess_id_type(message: discord.Message, mystery_id: int) -> Optional[ return role if (chan := message.guild.get_channel(mystery_id)) is not None: - if isinstance(chan, discord.TextChannel): + if is_guild_messageable(chan): return chan # asserts channel - if not isinstance(message.channel, discord.TextChannel): + if not is_guild_messageable(message.channel): return None # requires channel @@ -701,12 +691,13 @@ async def parse_user_member_noexcept(message: discord.Message, except (discord.errors.NotFound, discord.errors.HTTPException): if (pot := await _guess_id_type(message, uid)) is not None: errappend = "Note: While this ID is not a valid user ID, it is " - if isinstance(pot, discord.TextChannel): - errappend += f"a valid channel ID: <#{pot.id}>" + if isinstance(pot, discord.Role): + errappend += "a valid role" elif isinstance(pot, discord.Message): errappend += f"a valid message by a user with ID {pot.author.id}\n(did you mean to select this user?)" - elif isinstance(pot, discord.Role): - errappend += "a valid role" + else: + chan_name = "channel" if isinstance(pot, discord.TextChannel) else "thread" + errappend += f"a valid {chan_name} ID: <#{pot.id}>" raise lib_sonnetcommands.CommandError(f"User does not exist\n{errappend}") @@ -727,7 +718,7 @@ async def parse_user_member(message: discord.Message, args: List[str], client: d try: return await parse_user_member_noexcept(message, args, client, argindex=argindex, default_self=default_self) except lib_sonnetcommands.CommandError as ce: - await message.channel.send(ce) + await message.channel.send(str(ce)) raise errors.user_parse_error(ce) diff --git a/libs/lib_sonnetconfig.py b/libs/lib_sonnetconfig.py index 911a995..072dbbe 100644 --- a/libs/lib_sonnetconfig.py +++ b/libs/lib_sonnetconfig.py @@ -40,8 +40,7 @@ def _load_cfg(attr: str, default: Typ, typ: Type[Typ], testfunc: Optional[Callab if testfunc is not None and not testfunc(conf): raise TypeError(f"Sonnet Config {attr}: {errmsg}") - # pyright thinks that it can still be Any despite isinstance check - return conf # pyright: ignore[reportGeneralTypeIssues] + return conf # Prints a warning if not using re2 diff --git a/libs/lib_sonnetdb.py b/libs/lib_sonnetdb.py index c8ab83b..5aacaa9 100644 --- a/libs/lib_sonnetdb.py +++ b/libs/lib_sonnetdb.py @@ -7,13 +7,11 @@ import warnings import io -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) - from lib_sonnetconfig import DB_TYPE, SQLITE3_LOCATION -from typing import Union, Dict, List, Tuple, Optional, Any, Type, cast +from typing import Union, Dict, List, Tuple, Optional, Any, Type, Protocol, cast + +db_handler: Type["_DataBaseHandler"] # Get db handling library if DB_TYPE == "mariadb": @@ -34,6 +32,63 @@ raise RuntimeError("Could not load database backend (non valid specifier)") +class _DataBaseHandler(Protocol): + @property + def TEXT_KEY(self) -> bool: + ... + + def __init__(self, db_connection: Any, /) -> None: + ... + + def __enter__(self, /) -> "_DataBaseHandler": + ... + + def make_new_index(self, tablename: str, indexname: str, columns: List[str], /) -> None: + ... + + def make_new_table(self, tablename: str, data: Union[List[Any], Tuple[Any, ...]], /) -> None: + ... + + def add_to_table(self, table: str, data: Union[List[Any], Tuple[Any, ...]], /) -> None: + ... + + def multicount_rows_from_table(self, table: str, searchparms: List[List[Any]], /) -> int: + ... + + def fetch_rows_from_table(self, table: str, search: List[Any], /) -> Tuple[Any, ...]: + ... + + def multifetch_rows_from_table(self, table: str, searchparms: List[List[Any]], /) -> Tuple[Any, ...]: + ... + + def delete_rows_from_table(self, table: str, column_search: List[Any], /) -> None: + ... + + def delete_table(self, table: str, /) -> None: + ... + + def fetch_table(self, table: str, /) -> Tuple[Any, ...]: + ... + + def list_tables(self, searchterm: str, /) -> Tuple[Tuple[str], ...]: + ... + + def ping(self, /) -> None: + ... + + def commit(self, /) -> None: + ... + + def close(self, /) -> None: + ... + + def __del__(self, /) -> None: + ... + + def __exit__(self, err_type: Any, err_value: Any, err_traceback: Any, /) -> None: + ... + + class DATABASE_FATAL_CONNECTION_LOSS(Exception): __slots__ = () @@ -45,7 +100,7 @@ class DATABASE_FATAL_CONNECTION_LOSS(Exception): raise DATABASE_FATAL_CONNECTION_LOSS("Database failure") -def db_grab_connection() -> db_handler: # pytype: disable=invalid-annotation +def db_grab_connection() -> _DataBaseHandler: # pytype: disable=invalid-annotation global db_connection try: db_connection.ping() @@ -559,8 +614,6 @@ def close(self) -> None: def __exit__(self, err_type: Optional[Type[Exception]], err_value: Optional[str], err_traceback: Any) -> None: self._db.commit() - if err_type: - raise err_type(err_value) class _enum_context: @@ -575,8 +628,7 @@ def __enter__(self) -> "_enum_context": return self def __exit__(self, err_type: Optional[Type[Exception]], err_value: Optional[str], err_traceback: Any) -> None: - if err_type: - raise err_type(err_value) + return def grab(self, name: Union[str, int]) -> Optional[List[Union[str, int]]]: self._hlapi.grab_enum.__doc__ diff --git a/libs/lib_sql_handler.py b/libs/lib_sql_handler.py index 3ee690e..24c776f 100644 --- a/libs/lib_sql_handler.py +++ b/libs/lib_sql_handler.py @@ -225,5 +225,3 @@ def __exit__(self, err_type: Any, err_value: Any, err_traceback: Any) -> None: self.con.commit() self.con.close() self.closed = True - if err_type: - raise err_type(err_value) diff --git a/libs/lib_starboard.py b/libs/lib_starboard.py index e74f207..5cff650 100644 --- a/libs/lib_starboard.py +++ b/libs/lib_starboard.py @@ -7,16 +7,6 @@ import discord -import lib_sonnetconfig - -importlib.reload(lib_sonnetconfig) -import lib_parsers - -importlib.reload(lib_parsers) -import lib_compatibility - -importlib.reload(lib_compatibility) - from lib_parsers import generate_reply_field from lib_sonnetconfig import STARBOARD_EMOJI, STARBOARD_COUNT, REGEX_VERSION from lib_compatibility import user_avatar_url diff --git a/libs/lib_tparse.py b/libs/lib_tparse.py index d44f8e4..84d915d 100644 --- a/libs/lib_tparse.py +++ b/libs/lib_tparse.py @@ -61,6 +61,8 @@ class ParseFailureError(TParseError): _ParserT = TypeVar("_ParserT") # C Iterator Type _CIT = TypeVar("_CIT") +# Generic T +_T = TypeVar("_T") class _IteratorCtx: @@ -194,13 +196,16 @@ def parse(self, args: List[str] = sys.argv[1:], stderr: StringWriter = sys.stder garbage: List[int] = [] + def store_private(promise: Promise[_T], v: _T) -> None: + promise._data = v # pyright: ignore[reportPrivateUsage] + for idx, val in _CIterator(args): if val in self._arghash: garbage.append(idx.i) arg = self._arghash[val] if arg.flag is True: - arg.store._data = arg.func("") + store_private(arg.store, arg.func("")) else: idx.i += 1 garbage.append(idx.i) @@ -208,7 +213,7 @@ def parse(self, args: List[str] = sys.argv[1:], stderr: StringWriter = sys.stder self._error(f"Failure to parse argument {val}, expected parameter, reached EOL", exit_on_fail, stderr) try: - arg.store._data = arg.func(args[idx.i]) + store_private(arg.store, arg.func(args[idx.i])) except ValueError as ve: self._error(f"Failed to parse argument {val}; ValueError: {ve}", exit_on_fail, stderr) @@ -221,7 +226,7 @@ def parse(self, args: List[str] = sys.argv[1:], stderr: StringWriter = sys.stder del args[di] for i in self._arguments: - i.store._parsed = True + i.store._parsed = True # pyright: ignore[reportPrivateUsage] def store_true(s: str) -> Literal[True]: diff --git a/main.py b/main.py index 42db2db..d7c597a 100644 --- a/main.py +++ b/main.py @@ -15,10 +15,10 @@ import os, importlib, sys, io, traceback # Import sub dependencies -import glob, json, hashlib, logging, getpass, datetime, argparse +import glob, json, hashlib, logging, getpass, datetime, argparse, random # Import typing support -from typing import List, Optional, Any, Tuple, Dict, Union, Type, Protocol +from typing import List, Optional, Any, Tuple, Dict, Union, Type, Protocol, TypeVar # Start Discord.py import discord, asyncio @@ -41,6 +41,7 @@ intents.guilds = True intents.members = True intents.reactions = True +intents.message_content = True # Initialize Discord Client. Client = discord.Client(status=discord.Status.online, intents=intents) @@ -289,6 +290,60 @@ def _dump_data(self, dirstr: Optional[str] = None, dirlist: Optional[List[str]] command_modules_dict: Dict[str, Any] = {} dynamiclib_modules: List[Any] = [] dynamiclib_modules_dict: Dict[str, Any] = {} +# LeXdPyK 1.5: undefined exec order feature +# on-message is now Dict[on-message: [[on-message items], [on-message-0 items], [on-message-1 items]] +# this feature means you can have multiple on-message calls that will exec in an undefined order, but on-message-0 will always +# exec after on-message and on-message-1 after that etc +# this allows flexibility with multiple modules that just "must run after command processor init" +dynamiclib_modules_exec_dict: Dict[str, List[List[Any]]] = {} + +# LeXdPyK 2.0: optional lib reloads +# lexdpyk 2.0 ships with the new feature of not needing to reload library modules in sonnet, +# as the kernel handles reloading them at module load and reload time +# this comes with the side effect of all library modules being init by the kernel +loaded_libraries: List[Any] = [] + + +def add_module_to_exec_dict(module_dlibs: Dict[str, Any]) -> None: + + global dynamiclib_modules_exec_dict + + for k, v in module_dlibs.items(): + + if (maybe_num := k.split("-")[-1]).isnumeric(): + true_key = "-".join(k.split("-")[:-1]) + idx = int(maybe_num) + 1 + + else: + true_key = k + idx = 0 + + if idx >= 2048: + raise RuntimeError("Command execution order request exceeds 2048 (oom safety limit reached)") + + try: + data_list = dynamiclib_modules_exec_dict[true_key] + except KeyError: + data_list = dynamiclib_modules_exec_dict[true_key] = [] + + if len(data_list) < (idx + 1): + data_list.extend([] for _ in range((idx + 1) - len(data_list))) + + data_list[idx].append(v) + + +def compress_exec_dict() -> None: + + global dynamiclib_modules_exec_dict + + for k in dynamiclib_modules_exec_dict: + + dynamiclib_modules_exec_dict[k] = [i for i in dynamiclib_modules_exec_dict[k] if i] + + # randomize inner ordering to prevent people relying on it + for unordered in dynamiclib_modules_exec_dict[k]: + random.shuffle(unordered) + # Initialize ramfs, kernel ramfs ramfs = ram_filesystem() @@ -325,20 +380,48 @@ def log_kernel_info(s: object) -> None: logger.info(f"{version_info}: {s}") +def reload_libraries() -> List[Tuple[Exception, str]]: + """ + Reloads all lib_ libraries + """ + global loaded_libraries + loaded_libraries = [] + + err = [] + + # import libraries + for f in filter(lambda f: f.startswith("lib_") and f.endswith(".py"), os.listdir('./libs')): + try: + loaded_libraries.append(importlib.import_module(f[:-3])) + except Exception as e: + err.append((e, f[:-3]), ) + + for i in range(len(loaded_libraries)): + try: + loaded_libraries[i] = importlib.reload(loaded_libraries[i]) + except Exception as e: + err.append((e, loaded_libraries[i].__name__), ) + + return err + + def kernel_load_command_modules(args: List[str] = []) -> Optional[Tuple[str, List[Exception]]]: log_kernel_info("Loading Kernel Modules") start_load_modules = time.monotonic() # Globalize variables - global command_modules, command_modules_dict, dynamiclib_modules, dynamiclib_modules_dict + global command_modules, command_modules_dict, dynamiclib_modules, dynamiclib_modules_dict, dynamiclib_modules_exec_dict command_modules = [] command_modules_dict = {} dynamiclib_modules = [] dynamiclib_modules_dict = {} + dynamiclib_modules_exec_dict = {} importlib.invalidate_caches() # Init return state err: List[Tuple[Exception, str]] = [] + err.extend(reload_libraries()) + # Init imports for f in filter(lambda f: f.startswith("cmd_") and f.endswith(".py"), os.listdir('./cmds')): print(f) @@ -361,10 +444,13 @@ def kernel_load_command_modules(args: List[str] = []) -> Optional[Tuple[str, Lis err.append((KernelSyntaxError("Missing commands"), module.__name__), ) for module in dynamiclib_modules: try: + add_module_to_exec_dict(module.commands) dynamiclib_modules_dict.update(module.commands) except AttributeError: err.append((KernelSyntaxError("Missing commands"), module.__name__), ) + compress_exec_dict() + log_kernel_info(f"Loaded Kernel Modules in {(time.monotonic()-start_load_modules)*1000:.1f}ms") if err: return ("\n".join([f"Error importing {i[1]}: {type(i[0]).__name__}: {i[0]}" for i in err]), [i[0] for i in err]) @@ -388,42 +474,48 @@ def regenerate_kernel_ramfs(args: List[str] = []) -> Optional[Tuple[str, List[Ex def kernel_reload_command_modules(args: List[str] = []) -> Optional[Tuple[str, List[Exception]]]: log_kernel_info("Reloading Kernel Modules") # Init vars - global command_modules, command_modules_dict, dynamiclib_modules, dynamiclib_modules_dict + global command_modules, command_modules_dict, dynamiclib_modules, dynamiclib_modules_dict, dynamiclib_modules_exec_dict command_modules_dict = {} dynamiclib_modules_dict = {} + dynamiclib_modules_exec_dict = {} start_reload_modules = time.monotonic() # Init ret state err = [] + err.extend(reload_libraries()) + # Update set for i in range(len(command_modules)): try: command_modules[i] = (importlib.reload(command_modules[i])) except Exception as e: - err.append([e, command_modules[i].__name__]) + err.append((e, command_modules[i].__name__)) for i in range(len(dynamiclib_modules)): try: dynamiclib_modules[i] = (importlib.reload(dynamiclib_modules[i])) except Exception as e: - err.append([e, dynamiclib_modules[i].__name__]) + err.append((e, dynamiclib_modules[i].__name__)) # Update hashmaps for module in command_modules: try: command_modules_dict.update(module.commands) except AttributeError: - err.append([KernelSyntaxError("Missing commands"), module.__name__]) + err.append((KernelSyntaxError("Missing commands"), module.__name__)) for module in dynamiclib_modules: try: + add_module_to_exec_dict(module.commands) dynamiclib_modules_dict.update(module.commands) except AttributeError: - err.append([KernelSyntaxError("Missing commands"), module.__name__]) + err.append((KernelSyntaxError("Missing commands"), module.__name__)) # Regen tempramfs regenerate_ramfs() + compress_exec_dict() + log_kernel_info(f"Reloaded Kernel Modules in {(time.monotonic()-start_reload_modules)*1000:.1f}ms") if err: return ("\n".join([f"Error reimporting {i[1]}: {type(i[0]).__name__}: {i[0]}" for i in err]), [i[0] for i in err]) @@ -500,9 +592,10 @@ def kernel_logout(args: List[str] = []) -> Optional[Tuple[str, List[Exception]]] def kernel_drop_dlibs(args: List[str] = []) -> Optional[Tuple[str, List[Exception]]]: log_kernel_info("Dropping dynamiclib modules") - global dynamiclib_modules, dynamiclib_modules_dict + global dynamiclib_modules, dynamiclib_modules_dict, dynamiclib_modules_exec_dict dynamiclib_modules = [] dynamiclib_modules_dict = {} + dynamiclib_modules_exec_dict = {} return None @@ -582,21 +675,25 @@ async def on_error(event: str, *args: Any, **kwargs: Any) -> None: raise -async def do_event(event: Any, args: Tuple[Any, ...]) -> None: - await event( - *args, - client=Client, - ramfs=ramfs, - bot_start=bot_start_time, - command_modules=[command_modules, command_modules_dict], - dynamiclib_modules=[dynamiclib_modules, dynamiclib_modules_dict], - kernel_version=version_info, - kernel_ramfs=kernel_ramfs - ) +async def do_event_return_error(event: Any, args: Tuple[Any, ...]) -> Optional[Exception]: + try: + + await event( + *args, + client=Client, + ramfs=ramfs, + bot_start=bot_start_time, + command_modules=[command_modules, command_modules_dict], + dynamiclib_modules=[dynamiclib_modules, dynamiclib_modules_dict], + kernel_version=version_info, + kernel_ramfs=kernel_ramfs + ) + return None + except Exception as e: + return e async def event_call(argtype: str, *args: Any) -> Optional[errtype]: - # TODO(ultrabear): refactor to use undefined dispatch loop (on-message-0 will not call after on-message/may call at same time etc, module dispatches automatically namespaced) # used by dev mode tstartexec = time.monotonic() @@ -604,42 +701,19 @@ async def event_call(argtype: str, *args: Any) -> Optional[errtype]: etypes = [] try: + functions = dynamiclib_modules_exec_dict[argtype] + except KeyError: + functions = [] - # Do hash lookup with KeyError - # Separate from running function so we do not catch a KeyError deeper in the stack - try: - func = dynamiclib_modules_dict[argtype] - except KeyError: - raise KernelKeyError - - await do_event(func, args) - - # Check for KernelKeyError before checking for Exception (inherits from) - except KernelKeyError: - pass - except Exception as e: - etypes.append(errtype(e, argtype)) - - call = 0 - while True: - - exname = f"{argtype}-{call}" - - # If there is no hash then break the loop - try: - func = dynamiclib_modules_dict[exname] - except KeyError: - break - - try: - await do_event(func, args) - except Exception as e: - etypes.append(errtype(e, exname)) + for ftable in functions: + tasks = [asyncio.create_task(do_event_return_error(func, args)) for func in ftable] - call += 1 + for i in tasks: + if e := (await i): + etypes.append(errtype(e, argtype)) if DEVELOPMENT_MODE: - log_kernel_info(f"EVENT {argtype} : {round((time.monotonic()-tstartexec)*100000)/100}ms CC {call+1}") + log_kernel_info(f"EVENT {argtype} : {round((time.monotonic()-tstartexec)*100000)/100}ms CC {len(functions)}") if etypes: return etypes[0] @@ -647,6 +721,13 @@ async def event_call(argtype: str, *args: Any) -> Optional[errtype]: return None +UT = TypeVar("UT", bound=Union[discord.User, discord.Member]) + + +def lexdpyk_to_snowflake(v: UT) -> UT: + return v + + async def safety_check(guild: Optional[discord.Guild] = None, guild_id: Optional[int] = None, user: Optional[Union[discord.User, discord.Member]] = None, user_id: Optional[int] = None) -> bool: if guild: guild_id = guild.id @@ -666,7 +747,7 @@ async def safety_check(guild: Optional[discord.Guild] = None, guild_id: Optional return False try: - await non_null_guild.ban(user, reason="LeXdPyK: SYSTEM LEVEL BLACKLIST", delete_message_days=0) + await non_null_guild.ban(lexdpyk_to_snowflake(user), reason="LeXdPyK: SYSTEM LEVEL BLACKLIST", delete_message_days=0) except discord.errors.Forbidden: # call kernel_blacklist_guild to add to json db, blacklist guild @@ -737,7 +818,7 @@ async def on_message(message: discord.Message) -> None: # If bot owner run a debug command if len(args) >= 2 and args[0] in debug_commands: - if message.author.id in BOT_OWNER and args[1].strip("<@!>") == str(Client.user.id): + if Client.user and message.author.id in BOT_OWNER and args[1].strip("<@!>") == str(Client.user.id): if e := debug_commands[args[0]](args[2:]): await message.channel.send(e[0]) for i in e[1]: @@ -895,6 +976,56 @@ async def on_member_unban(guild: discord.Guild, user: discord.User) -> None: await event_call("on-member-unban", guild, user) +# new in 2.0: + + +@Client.event +async def on_raw_app_command_permissions_update(payload: discord.RawAppCommandPermissionsUpdateEvent) -> None: + if await safety_check(guild=payload.guild): + await event_call("on-raw-app-command-permissions-update", payload) + + +@Client.event +async def on_app_command_completion(interaction: discord.Interaction, command: Union[discord.app_commands.Command[Any, Any, Any], discord.app_commands.ContextMenu]) -> None: + if await safety_check(guild=interaction.guild, user=interaction.user): + await event_call("on-app-command-completion", interaction, command) + + +@Client.event +async def on_automod_rule_create(rule: discord.AutoModRule) -> None: + if await safety_check(guild=rule.guild): + await event_call("on-automod-rule-create", rule) + + +@Client.event +async def on_automod_rule_update(rule: discord.AutoModRule) -> None: + if await safety_check(guild=rule.guild): + await event_call("on-automod-rule-update", rule) + + +@Client.event +async def on_automod_rule_delete(rule: discord.AutoModRule) -> None: + if await safety_check(guild=rule.guild): + await event_call("on-automod-rule-delete", rule) + + +@Client.event +async def on_automod_action(execution: discord.AutoModAction) -> None: + if await safety_check(guild_id=execution.guild_id): + await event_call("on-automod-action", execution) + + +@Client.event +async def on_raw_member_remove(payload: discord.RawMemberRemoveEvent) -> None: + if await safety_check(guild_id=payload.guild_id): + await event_call("on-raw-member-remove", payload) + + +async def on_presence_update(before: discord.Member, after: discord.Member) -> None: + if await safety_check(guild=before.guild, user=before): + await event_call("on-presence-update") + + def gentoken() -> str: TOKEN = getpass.getpass("Enter TOKEN: ") @@ -964,7 +1095,7 @@ def main(args: List[str]) -> int: # Start bot if TOKEN: try: - Client.run(TOKEN, reconnect=True) + Client.run(TOKEN, reconnect=True, log_handler=None) except discord.errors.LoginFailure: print("Invalid token passed") return 1 @@ -988,7 +1119,7 @@ def main(args: List[str]) -> int: # Define version info and start time -version_info: str = "LeXdPyK 1.4.15" +version_info: str = "LeXdPyK 2.0.3" bot_start_time: float = time.time() if __name__ == "__main__": diff --git a/pyrightconfig.json b/pyrightconfig.json index b4ab2ff..bbe787a 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,5 +2,17 @@ "extraPaths": ["libs/", "common/"], "pythonVersion": "3.8", "pythonPlatform": "Linux", - "reportMissingImports": false + "reportMissingImports": false, + "strict": [ + "libs/lib_compatibility.py", + "libs/lib_datetimeplus.py", + "libs/lib_tparse.py", + "libs/lib_constants.py", + "libs/lib_db_obfuscator.py", + "libs/lib_encryption_wrapper.py", + "libs/lib_goparsers.py", + "libs/lib_lexdpyk_h.py", + "libs/lib_sonnetconfig.py", + "libs/lib_starboard.py" + ] } diff --git a/requirements.txt b/requirements.txt index 308a2a9..334e2de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -discord.py >=1.7.3, <2.0 -discord.py-stubs >=1.7.3, <2.0 +discord.py[speed] >=2.0.1, <3 cryptography >=37.0.0, <38.0.0 lz4 >=3.1.10, <5.0.0 -typing-extensions >=3.10.0.0, <5 +typing-extensions >=4.3, <5