diff --git a/README.md b/README.md index e12b400..216165a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ To add the cogs to your instance please do: `[p]repo add Seina-Cogs https://gith | Purge | 0.1.1 |
Advanced Purge/Cleanup.Purge (deleted) messages that meet a criteria.
| | FreeloaderMode | 0.1.0 |
Ban users that leave your server right after an event or something.Ban freeloaders who leave your server right after an event or something.
| | ErrorHandler | 0.1.0 |
Allow custom error message.Adds ability to replace the output of the bots error handler when CommandInvokeError is raised, all other errors get handled by the old handler.
| +| VoiceNoteLog | 0.1.0 |
Log the voice note sent by your server members.Log voice notes sent by your server members.
| Any questions you can find [Melon](https://discord.com/oauth2/authorize?client_id=808706062013825036&scope=bot&permissions=1099511627767%20applications.commands) and myself over on [the support server](https://discord.gg/mXfYuMy92r) diff --git a/voicenotelog/__init__.py b/voicenotelog/__init__.py new file mode 100644 index 0000000..9a3a95f --- /dev/null +++ b/voicenotelog/__init__.py @@ -0,0 +1,32 @@ +""" +MIT License + +Copyright (c) 2023-present japandotorg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from redbot.core.bot import Red + +from .core import VoiceNoteLog + + +async def setup(bot: Red) -> None: + cog = VoiceNoteLog(bot) + await bot.add_cog(cog) diff --git a/voicenotelog/core.py b/voicenotelog/core.py new file mode 100644 index 0000000..f50b093 --- /dev/null +++ b/voicenotelog/core.py @@ -0,0 +1,239 @@ +""" +MIT License + +Copyright (c) 2023-present japandotorg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import io +import pydub +import logging +import speech_recognition as speech +from typing import Final, List, Dict, Union, Optional, Any, Tuple + +import discord +from redbot.core.bot import Red +from redbot.core import Config, commands +from redbot.core.utils.chat_formatting import box, humanize_list + +from .views import JumpToMessageView + +MIC_GIF: Final[str] = "https://cdn.discordapp.com/emojis/1164844325973270599.gif" + +log: logging.Logger = logging.getLogger("red.seina.voicenotelog") + + +class VoiceNoteLog(commands.Cog): + """ + Voice note logging. + """ + + __author__: Final[List[str]] = ["inthedark.org"] + __version__: Final[str] = "0.1.0" + + def __init__(self, bot: Red) -> None: + super().__init__() + self.bot: Red = bot + + self.config: Config = Config.get_conf( + self, + identifier=69_666_420, + force_registration=True, + ) + default_guild: Dict[str, Union[Optional[int], bool]] = { + "channel": None, + "toggle": False, + } + self.config.register_guild(**default_guild) + + def format_help_for_context(self, ctx: commands.Context) -> str: + pre_processed = super().format_help_for_context(ctx) + n = "\n" if "\n\n" not in pre_processed else "" + text = [ + f"{pre_processed}{n}", + f"Author: **{humanize_list(self.__author__)}**", + f"Cog Version: **{self.__version__}**", + ] + return "\n".join(text) + + def _has_voice_note(self, message: discord.Message) -> bool: + if not message.attachments or not message.flags.value >> 13: + return False + return True + + async def _embed(self, text: str, message: discord.Message) -> discord.Embed: + embed: discord.Embed = discord.Embed( + description=( + f"**Channel:** {message.channel.mention}\n" # type: ignore + f"**Transcribed Text:** {box(text)}\n" + ), + color=await self.bot.get_embed_color(message.channel), # type: ignore + timestamp=message.created_at, + ) + embed.set_thumbnail(url=MIC_GIF) + embed.set_author( + name=f"{message.author.display_name} ({message.author.id})", + icon_url=message.author.display_avatar, + ) + return embed + + async def _transcribe_message( + self, message: discord.Message + ) -> Optional[Union[Any, List, Tuple]]: + if not self._has_voice_note(message): + return None + + voice_message_bytes: bytes = await message.attachments[0].read() + voice_message: io.BytesIO = io.BytesIO(voice_message_bytes) + + audio_segment = pydub.AudioSegment.from_file(voice_message) + empty_bytes: io.BytesIO = io.BytesIO() + audio_segment.export(empty_bytes, format="wav") + + recognizer = speech.Recognizer() + with speech.AudioFile(empty_bytes) as source: + audio_data = recognizer.record(source) + + try: + text = recognizer.recognize_google(audio_data) + except speech.UnknownValueError as error: + raise error + except Exception as error: + raise error + + return text + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.guild is None: + return + if message.is_system(): + return + if message.author.bot: + return + if not isinstance( + message.channel, (discord.TextChannel, discord.VoiceChannel, discord.Thread) + ): + return + + if not (await self.config.guild(message.guild).toggle()): + return + + if await self.bot.cog_disabled_in_guild(self, message.guild): + return + + if not self._has_voice_note(message): + return + + log_channel: Optional[discord.TextChannel] = message.guild.get_channel( + await self.config.guild(message.guild).channel() # type: ignore + ) + if not log_channel: + await self.config.guild(message.guild).toggle.clear() + log.debug( + f"Disabled voice note logging in {message.guild.name} because logging channel was not configured." + ) + return + + if not log_channel.permissions_for(message.guild.me).send_messages: + await self.config.guild(message.guild).toggle.clear() + log.debug( + f"Disabled voice note logging in {message.guild.name} because I do not have send message permission in the configured logging channel." + ) + return + + try: + text: Any = await self._transcribe_message(message) + except speech.UnknownValueError: + log.debug( + f"Could not transcribe {message.jump_url} as response was empty.", + ) + return + except Exception as error: + log.exception( + f"Failed to transcribe {message.jump_url} due to an error.", exc_info=error + ) + return + + embed: discord.Embed = await self._embed(text, message) + view: JumpToMessageView = JumpToMessageView("Jump To Message", message.jump_url) + + await log_channel.send(embed=embed, view=view) + + @commands.guild_only() + @commands.mod_or_permissions(manage_guild=True) + @commands.group(name="voicenotelog", aliases=["vnl"]) + async def _voice_note_log(self, _: commands.GuildContext): + """ + Voice note logging settings. + """ + + @_voice_note_log.command(name="channel") # type: ignore + async def _voice_note_log_channel( + self, ctx: commands.GuildContext, channel: Optional[discord.TextChannel] = None + ): + """ + Configure the logging channel. + """ + if not channel: + await self.config.guild(ctx.guild).channel.clear() + await ctx.send( + "Cleared the voice note logging channel.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + return + await self.config.guild(ctx.guild).channel.set(channel) + await ctx.send( + f"Configured the voice note logging channel to {channel.mention}!", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + + @_voice_note_log.command(name="toggle") # type: ignore + async def _voice_note_log_toggle(self, ctx: commands.GuildContext, toggle: bool): + """ + Toggle voice note logging. + """ + await self.config.guild(ctx.guild).toggle.set(toggle) + await ctx.send( + f"Voice note logging is now {'enabled' if toggle else 'disabled'}.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + + @commands.bot_has_permissions(embed_links=True) + @_voice_note_log.command(name="settings", aliases=["showsettings", "show"]) # type: ignore + async def _voice_note_log_settings(self, ctx: commands.GuildContext): + """ + View the voice note logging configuration settings. + """ + data: Dict[str, Union[Optional[int], bool]] = await self.config.guild(ctx.guild).all() + embed: discord.Embed = discord.Embed( + title="Voice Note Logging Settings", + description=(f"Channel: **{data['channel']}**\n" f"Toggle: **{data['toggle']}\n"), + color=await ctx.embed_color(), + ) + embed.set_thumbnail(url=getattr(ctx.guild.icon, "url", None)) + await ctx.send( + embed=embed, + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) diff --git a/voicenotelog/info.json b/voicenotelog/info.json new file mode 100644 index 0000000..4970df6 --- /dev/null +++ b/voicenotelog/info.json @@ -0,0 +1,20 @@ +{ + "author": ["inthedark.org"], + "install_msg": "Thanks for installing the voicenotelog cog.", + "name": "VoiceNoteLog", + "disabled": false, + "short": "Log the voice note sent by your server members.", + "description": "Log the voice note sent by your server members.", + "tags": [ + "voice", + "audio", + "voicenote", + "logging", + "mod" + ], + "required_cogs": {}, + "min_python_version": [], + "requirements": [], + "type": "COG", + "end_user_data_statement": "This cog does not store End User Data." +} diff --git a/voicenotelog/views.py b/voicenotelog/views.py new file mode 100644 index 0000000..fd7799a --- /dev/null +++ b/voicenotelog/views.py @@ -0,0 +1,49 @@ +""" +MIT License + +Copyright (c) 2023-present japandotorg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from typing import Optional + +import discord + + +class JumpToMessageView(discord.ui.View): + def __init__( + self, + label: Optional[str], + jump_url: Optional[str], + ) -> None: + super().__init__(timeout=None) + self.label = label + self.jump_url = jump_url + + button: discord.ui.Button = discord.ui.Button( + label=str(self.label), + style=discord.ButtonStyle.url, + url=str(self.jump_url), + ) + + self.add_item(button) + + async def on_timeout(self) -> None: + pass