async def get_one(self, opts: Sequence[T_], time: int = 30, title: str = "Select One") -> Optional[T_]: """Ask the user to select ONE of a set of predefined options.""" onum = len(opts) if not 1 <= onum <= len(letters): return None selection = [cancel, *letters[:onum]] self.add_section( "\n".join(f"{letters[i]}: `{opts[i]}`" for i in range(onum)), title) buttons: Task = await self.add_buttons(selection) await self.post() choice = (await Reactions.waitfor( self.client, all_checks(Reactions.by_user(self.master), Reactions.on_message(self.msg)), timeout=time, ))[0] if not choice or choice.emoji == cancel: result = None else: result = opts[letters.index(choice.emoji)] await buttons await self.clear() return result
async def get_bool(self, time: int = 30, prompt: str = "Select Yes or No", title: str = "Boolean Choice") -> Optional[bool]: """Ask the user to click a simple YES or NO.""" selection = (cancel, confirm) # self.em.description = prompt or "Select Yes or No" # await self.post() # adding = create_task(self.add_buttons(selection)) self.add_section(prompt, title) adding: Task = await self.add_buttons(selection) await self.post() choice = (await Reactions.waitfor( self.client, all_checks(Reactions.by_user(self.master), Reactions.on_message(self.msg)), timeout=time, ))[0] await adding await self.clear() if not choice: return None elif choice.emoji == confirm: return True elif choice.emoji == cancel: return False else: return None
async def cmd_repeat(self, src, **_): return "You said:```{}```".format((await Messages.waitfor( self.client, all_checks(Messages.by_user(src.author), Messages.in_channel(src.channel)), channel=src.channel, prompt="say thing", )).content)
async def get_multi( self, opts: Sequence[T_], time: int = 30, prompt: str = "Select One or More and Confirm:", title: str = "Multiple Choice", ) -> Tuple[T_, ...]: """Ask the user to select ONE OR MORE of a set of predefined options.""" onum = len(opts) if not 1 <= onum <= len(letters): return () selection = [cancel, *letters[:onum], confirm] self.add_section( "\n".join([prompt] + [f"{letters[i]}: `{opts[i]}`" for i in range(onum)]), title, ) buttons: Task = await self.add_buttons(selection) await self.post() ok = (str(cancel), str(confirm)) pre = all_checks(Reactions.by_user(self.master), Reactions.on_message(self.msg)) def check(react_: Reaction, user: User) -> bool: return pre(react_, user) and str(react_.emoji) in ok choice = (await Reactions.waitfor(self.client, check, timeout=time))[0] if not choice or choice.emoji == cancel: await self.clear() return () try: vm: Message = await self.channel.fetch_message(self.msg.id) except: await self.clear() return () results: Tuple[T_, ...] = tuple([ opts[letters.index(react.emoji)] for react in vm.reactions if (react.emoji in selection[1:-1] and self.master in await react.users().flatten()) ]) await buttons await self.clear() return results
async def cmd_kick( self, args, src: discord.Message, _reason: str = None, _noconfirm: bool = False, **_, ): """Kick a user from a guild. Syntax: `{p}kick [OPTIONS] <user tag/id>` Options: `--reason <str>` :: Provide a reason immediately, rather than typing a reason in a subsequent message. `--noconfirm` :: Perform the action immediately, without asking to make sure. ***This can get you in trouble if you mess up with it.*** """ if not args: raise CommandArgsError("No User specified.") if not lambdall(args, lambda x: x.isdigit()): raise CommandInputError("All IDs must be positive Integers.") guild: discord.Guild = self.client.main_guild target: discord.Member = guild.get_member(int(args[0])) if target is None: raise CommandInputError("Could not get user with that ID.") elif target.id == self.client.user.id: raise CommandOperationError( f"I'm sorry, {src.author.mention}. I'm afraid I can't let you do that." ) if _reason is None: await self.client.send_message( src.author, src.channel, "Please give a reason (just reply below): ") try: reason = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: raise CommandOperationError( "Timed out while waiting for reason.") _reason = reason.content targline: str = mono(userline(target)) if not _noconfirm: confirm = await confirm_action( self.client, src, "Member Kick", f"Confirm kicking {targline} from {target.guild.name}?", ) if confirm is not True: if confirm is False: raise CommandExit("Kick was cancelled.") else: raise CommandExit("Confirmation timed out.") try: await target.kick(reason=_reason) except discord.errors.Forbidden: raise CommandOperationError( "It seems I don't have perms to kick this user.") else: logEmbed = (discord.Embed( title="User Kick", description=_reason, colour=0xFF7900).set_author( name=src.author.display_name, icon_url=src.author.avatar_url).add_field( name="Issuer", value=mono(userline(src.author))).add_field( name="Recipient", value=targline).add_field( name="Guild", value=target.guild.name).add_field( name="Timestamp", value=str(dt.utcnow())[:-7]).set_thumbnail( url=target.avatar_url)) await self.client.log_moderation(embed=logEmbed) return f"Successfully Kicked: {targline}"
async def cmd_mute(self, args, src: discord.Message, **_): """Toggle the mute tag on a user if your guild supports that role. Syntax: `{p}mute <user tag/id>` """ if not args: raise CommandArgsError("Must provide User ID.") if not lambdall(args, lambda x: x.isdigit()): raise CommandArgsError("All IDs must be positive Integers.") target: discord.Member = self.client.main_guild.get_member(int( args[0])) if target is None: raise CommandInputError( f"Could not get user with ID `{int(args[0])}`.") elif target.id == self.client.user.id: raise CommandInputError( f"I'm sorry, {src.author.mention}. I'm afraid I can't let you do that." ) await self.client.send_message( src.author, src.channel, "Please give a reason for the mute (just reply below): ", ) try: reason = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: raise CommandOperationError("Timed out while waiting for reason.") role_mute: discord.Role = discord.utils.get(src.guild.roles, name="mute") if role_mute is None: raise CommandOperationError( "This guild does not have a `mute` role. To enable the mute " "function, set up the roles and name one `mute`.") else: try: if role_mute in target.roles: await target.remove_roles(role_mute, reason) # await self.client.guild_voice_state(target, mute=False) warnEmbed = discord.Embed( title="User Unmute", description= f"You have been unmuted by {src.author.name}.", colour=0x00FF11, ) warnEmbed.set_author( name=self.client.user.name, icon_url="https://puu.sh/tB2KH/cea152d8f5.png", ) # warnEmbed.add_field(name="Reason", value=reason.content) warnEmbed.add_field(name="Issuing Server", value=src.guild.name, inline=False) muteswitch = "Unmute" else: await target.add_roles(role_mute, reason) # await self.client.guild_voice_state(target, mute=True) warnEmbed = discord.Embed( title="User Mute", description= f"You have been muted by {src.author.name}.", colour=0xFF0000, ) warnEmbed.set_author( name=self.client.user.name, icon_url="https://puu.sh/tB2KH/cea152d8f5.png", ) warnEmbed.add_field(name="Reason", value=reason.content) warnEmbed.add_field(name="Issuing Server", value=src.guild.name, inline=False) muteswitch = "Mute" except discord.errors.Forbidden: raise CommandOperationError( "It seems I don't have permission to mute this user.") else: yield f"{target.name} (ID: {target.id}) was successfully {muteswitch}d" try: await target.send(embed=warnEmbed) except discord.errors.Forbidden: yield ( f" (FAILED to send a DM notification to user `{target.id}`.)", True, ) else: yield ( f" (A notification was sent in DM to user `{target.id}`.)", True, ) logEmbed = discord.Embed( title=f"User {muteswitch}", description=reason.content, colour=0x1200FF, ) logEmbed.add_field(name="Issuer", value=src.author.name + "\n" + src.author.id) logEmbed.add_field(name="Recipient", value=target.name + "\n" + target.id) logEmbed.add_field(name="Server", value=target.guild.name) logEmbed.add_field(name="Timestamp", value=str(dt.utcnow())[:-7]) logEmbed.set_thumbnail(url=target.avatar_url) await self.client.log_moderation(embed=logEmbed)
async def cmd_warn(self, args, src: discord.Message, **_): """Send an official and logged warning to a user. Syntax: `{p}warn <user tag/id>` """ if not args: return if not lambdall(args, lambda x: x.isdigit()): return "All IDs must be positive Integers." guild = self.client.main_guild userToWarn = guild.get_member(int(args[0])) if userToWarn is None: return "Could not get user with that ID." elif userToWarn.id == self.client.user.id: return ( f"I'm sorry, {src.author.mention}. I'm afraid I can't let you do that." ) await self.client.send_message( src.author, src.channel, "Please give a message to send (just reply below):") try: msg = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: return "Timed out while waiting for message." else: try: warnEmbed = discord.Embed( title="Official Warning", description="The guild has sent you an official warning", colour=0xFFF600, ) warnEmbed.add_field(name="Reason", value=msg.content) warnEmbed.add_field(name="Issuing Server", value=src.guild.name, inline=False) await self.client.embed(userToWarn, warnEmbed) except discord.errors.Forbidden: return "It seems I don't have perms to warn this user" else: logEmbed = discord.Embed(title="User Warn", description=msg.content, colour=0xFF600) logEmbed.set_author( name=self.client.user.name, icon_url="https://puu.sh/tADFM/dc80dc3a5d.png", ) logEmbed.add_field(name="Issuer", value=src.author.name + "\n" + str(src.author.id)) logEmbed.add_field(name="Recipient", value=userToWarn.name + "\n" + str(userToWarn.id)) logEmbed.add_field(name="Server", value=userToWarn.guild.name) logEmbed.add_field(name="Timestamp", value=str(dt.utcnow())[:-7]) logEmbed.set_thumbnail(url=userToWarn.avatar_url) await self.client.embed( self.client.get_channel(self.config.modChannel), logEmbed) return (userToWarn.name + " (ID: " + str(userToWarn.id) + ") was successfully warned")
async def cmd_tempban( self, args, src: discord.Message, _reason: str = None, _purge: int = 1, _days: int = None, **_, ): """Temporarily ban a user. Syntax: `{p}tempban [OPTIONS] <user tag/id>` Options: `--reason <str>` :: Provide a reason immediately, rather than typing a reason in a subsequent message. `--purge <int>` :: Determine how many days of messages by the banned user to delete. Default is 1. Can be between 0 and 7, inclusive. `--days <int>` :: Provide a ban duration immediately, rather than typing a number of days in a subsequent message. """ if not args: return if not lambdall(args, lambda x: x.isdigit()): return "All IDs must be positive Integers." guild = self.client.main_guild userToBan = guild.get_member(int(args[0])) if userToBan is None: return "Could not get user with that ID." elif userToBan.id == self.client.user.id: return ( f"I'm sorry, {src.author.mention}. I'm afraid I can't let you do that." ) if not 0 <= _purge <= 7: return "Can only purge between 0 and 7 days of messages, inclusive." if _reason is None: await self.client.send_message( src.author, src.channel, "Please give a reason (just reply below): ") try: reason = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: return "Timed out while waiting for reason." _reason = reason.content if not _days: await self.client.send_message(src.author, src.channel, "How long? (days) ") try: msg2 = await self.client.wait_for( "message", check=(lambda x: checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), )(x) and x.content.isdigit()), timeout=30, ) except asyncio.TimeoutError: return "Timed out while waiting for input" _days = int(msg2.content) try: # petal.logLock = True timex = time.time() + timedelta(days=int(_days)).total_seconds() self.client.db.update_member( userToBan, { "banned": True, "bannedFrom": userToBan.guild.id, "banExpires": str(timex).split(".")[0], "tempBanned": True, }, ) await userToBan.ban(reason=_reason, delete_message_days=_purge) except discord.errors.Forbidden: return "It seems I don't have perms to ban this user" else: logEmbed = discord.Embed(title="User Ban", description=_reason, colour=0xFF0000) logEmbed.add_field(name="Issuer", value=src.author.name + "\n" + str(src.author.id)) logEmbed.add_field(name="Recipient", value=userToBan.name + "\n" + str(userToBan.id)) logEmbed.add_field(name="Server", value=userToBan.guild.name) logEmbed.add_field(name="Timestamp", value=str(dt.utcnow())[:-7]) logEmbed.set_thumbnail(url=userToBan.avatar_url) await self.client.embed( self.client.get_channel(self.config.modChannel), logEmbed) return ( userToBan.name + " (ID: " + str(userToBan.id) + ") was successfully temp-banned.\n\nThey will be unbanned on " + str(dt.utcnow() + timedelta(days=_days))[:-7])
async def cmd_ban( self, args, src: discord.Message, _reason: str = None, _purge: int = 1, _noconfirm: bool = False, **_, ): """Ban a user permanently. Syntax: `{p}ban [OPTIONS] <user tag/id>` Options: `--reason <str>` :: Provide a reason immediately, rather than typing a reason in a subsequent message. `--purge <int>` :: Determine how many days of messages by the banned user to delete. Default is 1. Can be between 0 and 7, inclusive. `--noconfirm` :: Perform the action immediately, without asking to make sure. ***This can get you in trouble if you mess up with it.*** """ if not args: return if not lambdall(args, lambda x: x.isdigit()): return "All IDs must be positive Integers." guild = self.client.main_guild userToBan = guild.get_member(int(args[0])) if userToBan is None: return "Could not get user with that ID." elif userToBan.id == self.client.user.id: return ( f"I'm sorry, {src.author.mention}. I'm afraid I can't let you do that." ) if not 0 <= _purge <= 7: return "Can only purge between 0 and 7 days of messages, inclusive." if _reason is None: await self.client.send_message( src.author, src.channel, "Please give a reason (just reply below): ") try: reason = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: return "Timed out while waiting for reason." _reason = reason.content if not _noconfirm: await self.client.send_message( src.author, src.channel, "You are about to ban: " + userToBan.name + ". If this is correct, type `yes`.", ) try: msg = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: return "Timed out... user was not banned." if msg.content.lower() != "yes": return userToBan.name + " was not banned." try: # petal.logLock = True self.client.db.update_member(userToBan, { "banned": True, "tempBanned": False, "banExpires": None }) await userToBan.ban(reason=_reason, delete_message_days=_purge) except discord.errors.Forbidden: return "It seems I don't have perms to ban this user." else: logEmbed = (discord.Embed( title="User Ban", description=_reason, colour=0xFF0000).set_author( name=self.client.user.name, icon_url="https://" + "puu.sh/tACjX/fc14b56458.png", ).add_field(name="Issuer", value=src.author.name + "\n" + str(src.author.id)).add_field( name="Recipient", value=userToBan.name + "\n" + str(userToBan.id)).add_field( name="Server", value=userToBan.guild.name).add_field( name="Timestamp", value=str( dt.utcnow())[:-7]).set_thumbnail( url=userToBan.avatar_url)) await self.client.embed( self.client.get_channel(self.config.modChannel), logEmbed) await asyncio.sleep(4) # petal.logLock = False response = await self.client.send_message( src.author, src.channel, userToBan.name + " (ID: " + str(userToBan.id) + ") was successfully banned.", ) try: # Post-processing webhook for ban command return self.generate_post_process_URI( src.author.name + src.author.discriminator, _reason, response.content, userToBan.name + userToBan.discriminator, ) except Exception as e: self.log.err( "Could not generate post_process_message for ban: " + str(e)) return "Error occurred trying to generate webhook URI"
async def cmd_event(self, src, _image: str = None, _message: str = None, _nomenu: bool = False, **_): """Post a message announcing the start of an event. Define a message which will be sent to one or more predefined channels. The message may include mass pings by way of including tags `{{e}}` and `{{h}}` for substitution. Destination channels may be selected conversationally or by way of a reaction-based menu. Options: `--message=<msg>` :: Define the message to send ahead of time. This will skip the step where Petal asks you what you want the message to say. `--nomenu` :: Forsake the Reaction UI and determine destination channels conversationally. """ channels_list = [] channels_dict = {} msg = "" for chan in self.config.get("xPostList"): channel = self.client.get_channel(chan) if channel is not None: msg += (str(len(channels_list)) + ". (" + channel.name + " [{}]".format(channel.guild.name) + ")\n") channels_list.append(channel) channels_dict[channel.guild.name + "/#" + channel.name] = channel else: self.log.warn( chan + " is not a valid channel. I'd remove it if I were you.") # Get channels to send to. if _nomenu: # Do it only conversationally. menu = None while True: await self.client.send_message( src.author, src.channel, "Hi there, " + src.author.name + "! Please select the number of " + "each guild you want to post " + "to. (dont separate the numbers)", ) await self.client.send_message(src.author, src.channel, msg) chans = await Messages.waitfor( self.client, all_checks(Messages.by_user(src.author), Messages.in_channel(src.channel)), timeout=20, ) if chans is None: return ( "Sorry, the request timed out. Please make sure you" " type a valid sequence of numbers.") if self.validate_channel(channels_list, chans.content): break else: await self.client.send_message( src.author, src.channel, "Invalid channel choices. You may try again immediately.", ) post_to = [] for i in chans.content: print(channels_list[int(i)]) post_to.append(channels_list[int(i)]) else: # Use the Reaction-based GUI. menu = Menu( self.client, src.channel, "Event Announcement Post (by {})".format( src.author.display_name), "Use the Reaction Buttons to fill out the Announcement.", user=src.author, ) if _image: menu.em.set_thumbnail(url=_image) selection = await menu.get_multi(list(channels_dict), title="Target Channels") post_to = [ channels_dict[c] for c in selection if c in channels_dict ] if not post_to: # menu.add_section( # "No valid target channels selected; Post canceled.", "Verdict" # ) # await menu.close("No valid target channels selected; Post canceled.") menu.add_section("No Channels selected; Cancelled.", "Target Channels", overwrite=-1) await menu.post() return menu.add_section("\n".join([c.mention for c in post_to]), "Target Channels", overwrite=-1) await menu.post() try: msgstr = (_message or (await Messages.waitfor( self.client, all_checks( Messages.by_user(src.author), Messages.in_channel(src.channel), ), timeout=120, channel=src.channel, prompt="What do you want to send?" " (remember: {e} = `@ev` and {h} = `@here`)", )).content).format(e="@everyone", h="@here") except AttributeError: # Likely tried to get `None.content`. raise CommandOperationError("Text Input timed out.") if _nomenu: embed = discord.Embed(title="Message to post", description=msgstr, colour=0x0ACDFF) embed.add_field(name="Channels", value="\n".join([c.mention for c in post_to])) await self.client.embed(src.channel, embed) msg2 = await Messages.waitfor( self.client, all_checks(Messages.by_user(src.author), Messages.in_channel(src.channel)), timeout=20, channel=src.channel, prompt="If this is correct, type `confirm`.", ) if msg2 is None: return "Event post timed out." elif msg2.content.lower() != "confirm": return "Event post cancelled." else: # confirmer = Menu(self.client, src.channel, "Confirm Post", user=src.author) menu.add_section(msgstr, "Message Preview") proceed = await menu.get_bool( prompt="Send this Event Announcement?", title="Confirmation") if proceed is None: # menu.em.description = "[ Closed ]" menu.add_section("Posting timed out.", "Confirmation", overwrite=-1) return elif proceed is not True: # menu.em.description = "[ Closed ]" menu.add_section("Posting cancelled.", "Confirmation", overwrite=-1) return posted: List[discord.Message] = [] # TODO # em = discord.Embed() # if _image: # em.set_image(url=_image) for i in post_to: posted.append(await i.send(msgstr)) await asyncio.sleep(1) if menu: menu.add_section("Messages have been posted.", "Confirmation", overwrite=-1) await menu.post() else: # menu.em.description = "[ Closed ]" await self.client.send_message(src.author, src.channel, "Messages have been posted.") try: subkey, subname = self.get_event_subscription(msgstr) except AttributeError: pass else: if subkey is None: return ( "I was unable to auto-detect any game titles in your post." " No subscribers will be notified for this event.") else: n = await Messages.waitfor( self.client, all_checks(Messages.by_user(src.author), Messages.in_channel(src.channel)), timeout=20, channel=src.channel, prompt= f"I auto-detected a possible game in your announcement:" f" **{subname}**. Would you like to notify subscribers? [y/N]", ) if not n: return "Timed out." elif n.content.lower() not in ("y", "yes"): return "Subscribers will not be notified." else: response = await self.notify_subscribers( src.channel, posted[0], subkey) todelete = "[{}]".format(subkey) for post in posted: content = post.content # print(content) # print(todelete) if todelete in content: # print("replacing") content = content.replace(todelete, "") # print("replaced: " + content) await post.edit(content=content) return response
async def cmd_send(self, args, src: discord.Message, _everyone: bool = False, _here: bool = False, _identity: str = None, _i: str = None, _image: str = None, _I: str = None, **_): """Broadcast an official-looking message into another channel. By using the Identity Option, you can specify who the message is from. Valid Identities:```\n{}``` Syntax: `{{p}}send [OPTIONS] <channel-id> ["<message>"]` Parameters ---------- _ : dict Dict of additional Keyword Args. self self args : List[str] List of Positional Arguments supplied after Command. src : discord.Message The Discord Message that invoked this Command. _everyone : bool Include an `@everyone` ping in the message. Overrides `--here`. _here : bool Include a `@here` ping in the message. _identity, _i : str Select the group/team on whose behalf this message is being sent. _image, _I : str Provide the URL of an image to be included in the embed. """ if 2 < len(args) < 1: raise CommandArgsError( "Must provide a Channel ID and, optionally, a quoted message.") elif not args[0].isdigit(): raise CommandArgsError("Channel ID must be integer.") destination: discord.TextChannel = self.client.get_channel( int(args.pop(0))) if not destination: raise CommandArgsError("Invalid Channel.") if not args: await self.client.send_message( src.author, src.channel, "Please give a message to send (just reply below):", ) try: msg = await self.client.wait_for( "message", check=checks.all_checks( checks.Messages.by_user(src.author), checks.Messages.in_channel(src.channel), ), timeout=30, ) except asyncio.TimeoutError: raise CommandInputError("Timed out while waiting for message.") else: text = msg.content else: text = args[0] identity = (_identity or _i or default).lower() ident = idents.get(identity, idents[list(sorted(idents.keys()))[0]]) ident["description"] = text img = _image or _I try: preview = discord.Embed(**ident) if img: preview.set_image(url=img) menu = Menu(self.client, src.channel, "", "", user=src.author) menu.em = preview confirm = await menu.get_bool( prompt="Send this message to {} on behalf of {}?\n" "(This section will not be sent.)".format( destination.mention, identity) + ("\n***NOTE: THIS MESSAGE WILL SEND A MASS PING!***" if _everyone or _here else ""), title="Confirm", ) if confirm is True: em = discord.Embed(**ident) if img: em.set_image(url=img) if _everyone: await destination.send("(@everyone)", embed=em) elif _here: await destination.send("(@here)", embed=em) else: await destination.send(embed=em) # await self.client.embed(destination, em) elif confirm is False: raise CommandExit("Message cancelled.") else: raise CommandExit("Confirmation timed out.") except discord.errors.Forbidden: raise CommandOperationError( "Failed to send message: Access Denied") else: return ("{} (ID: `{}`) sent the following message to {}" " on behalf of `{}`:\n{}".format( src.author.name, str(src.author.id), destination.mention, identity, text, ))