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