Skip to content

Commit

Permalink
[VoiceNoteLog] publish new cog
Browse files Browse the repository at this point in the history
  • Loading branch information
japandotorg committed Nov 1, 2023
1 parent fc537d9 commit e7517ba
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ To add the cogs to your instance please do: `[p]repo add Seina-Cogs https://gith
| Purge | 0.1.1 | <details><summary>Advanced Purge/Cleanup.</summary>Purge (deleted) messages that meet a criteria.</details> |
| FreeloaderMode | 0.1.0 | <details><summary>Ban users that leave your server right after an event or something.</summary>Ban freeloaders who leave your server right after an event or something.</details> |
| ErrorHandler | 0.1.0 | <details><summary>Allow custom error message.</summary>Adds ability to replace the output of the bots error handler when CommandInvokeError is raised, all other errors get handled by the old handler.</details> |
| VoiceNoteLog | 0.1.0 | <details><summary>Log the voice note sent by your server members.</summary>Log voice notes sent by your server members.</details> |


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)
Expand Down
32 changes: 32 additions & 0 deletions voicenotelog/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
239 changes: 239 additions & 0 deletions voicenotelog/core.py
Original file line number Diff line number Diff line change
@@ -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),
)
20 changes: 20 additions & 0 deletions voicenotelog/info.json
Original file line number Diff line number Diff line change
@@ -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."
}
49 changes: 49 additions & 0 deletions voicenotelog/views.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e7517ba

Please sign in to comment.