From: Muhammad Rizki <[email protected]>
To: Ammar Faizi <[email protected]>
Cc: Muhammad Rizki <[email protected]>,
GNU/Weeb Mailing List <[email protected]>,
Alviro Iskandar Setiawan <[email protected]>
Subject: [PATCH v1 2/3] First release Discord bot
Date: Thu, 25 Aug 2022 23:09:52 +0700 [thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
The Discord bot for lore email message has been released.
Signed-off-by: Muhammad Rizki <[email protected]>
---
daemon/discord/.env.example | 8 +
daemon/discord/config.py.example | 9 +
daemon/discord/execute_me.sql | 64 +++++
daemon/discord/gnuweeb/__init__.py | 6 +
daemon/discord/gnuweeb/client.py | 110 ++++++++
daemon/discord/gnuweeb/filters.py | 67 +++++
daemon/discord/gnuweeb/models/__init__.py | 6 +
daemon/discord/gnuweeb/models/ui/__init__.py | 6 +
.../gnuweeb/models/ui/buttons/__init__.py | 6 +
.../models/ui/buttons/full_message_btn.py | 21 ++
daemon/discord/gnuweeb/plugins/__init__.py | 20 ++
.../plugins/basic_commands/__init__.py | 13 +
.../plugins/basic_commands/debugger.py | 20 ++
.../gnuweeb/plugins/basic_commands/sync_it.py | 20 ++
.../gnuweeb/plugins/events/__init__.py | 13 +
.../gnuweeb/plugins/events/on_error.py | 17 ++
.../gnuweeb/plugins/events/on_ready.py | 24 ++
.../plugins/slash_commands/__init__.py | 15 ++
.../plugins/slash_commands/get_lore_mail.py | 47 ++++
.../plugins/slash_commands/manage_atom.py | 81 ++++++
.../slash_commands/manage_broadcast.py | 83 ++++++
daemon/discord/gnuweeb/utils.py | 52 ++++
daemon/discord/mailer/__init__.py | 7 +
daemon/discord/mailer/database.py | 203 +++++++++++++++
daemon/discord/mailer/listener.py | 152 +++++++++++
daemon/discord/mailer/scraper.py | 63 +++++
daemon/discord/mailer/utils.py | 241 ++++++++++++++++++
daemon/discord/requirements.txt | 8 +
daemon/discord/run.py | 48 ++++
daemon/discord/storage/.gitignore | 2 +
daemon/telegram/packages/plugins/admin.py | 42 ---
daemon/telegram/packages/plugins/scrape.py | 86 -------
32 files changed, 1432 insertions(+), 128 deletions(-)
create mode 100644 daemon/discord/.env.example
create mode 100644 daemon/discord/config.py.example
create mode 100644 daemon/discord/execute_me.sql
create mode 100644 daemon/discord/gnuweeb/__init__.py
create mode 100644 daemon/discord/gnuweeb/client.py
create mode 100644 daemon/discord/gnuweeb/filters.py
create mode 100644 daemon/discord/gnuweeb/models/__init__.py
create mode 100644 daemon/discord/gnuweeb/models/ui/__init__.py
create mode 100644 daemon/discord/gnuweeb/models/ui/buttons/__init__.py
create mode 100644 daemon/discord/gnuweeb/models/ui/buttons/full_message_btn.py
create mode 100644 daemon/discord/gnuweeb/plugins/__init__.py
create mode 100644 daemon/discord/gnuweeb/plugins/basic_commands/__init__.py
create mode 100644 daemon/discord/gnuweeb/plugins/basic_commands/debugger.py
create mode 100644 daemon/discord/gnuweeb/plugins/basic_commands/sync_it.py
create mode 100644 daemon/discord/gnuweeb/plugins/events/__init__.py
create mode 100644 daemon/discord/gnuweeb/plugins/events/on_error.py
create mode 100644 daemon/discord/gnuweeb/plugins/events/on_ready.py
create mode 100644 daemon/discord/gnuweeb/plugins/slash_commands/__init__.py
create mode 100644 daemon/discord/gnuweeb/plugins/slash_commands/get_lore_mail.py
create mode 100644 daemon/discord/gnuweeb/plugins/slash_commands/manage_atom.py
create mode 100644 daemon/discord/gnuweeb/plugins/slash_commands/manage_broadcast.py
create mode 100644 daemon/discord/gnuweeb/utils.py
create mode 100644 daemon/discord/mailer/__init__.py
create mode 100644 daemon/discord/mailer/database.py
create mode 100644 daemon/discord/mailer/listener.py
create mode 100644 daemon/discord/mailer/scraper.py
create mode 100644 daemon/discord/mailer/utils.py
create mode 100644 daemon/discord/requirements.txt
create mode 100644 daemon/discord/run.py
create mode 100644 daemon/discord/storage/.gitignore
delete mode 100644 daemon/telegram/packages/plugins/admin.py
delete mode 100644 daemon/telegram/packages/plugins/scrape.py
diff --git a/daemon/discord/.env.example b/daemon/discord/.env.example
new file mode 100644
index 0000000..c2a9de8
--- /dev/null
+++ b/daemon/discord/.env.example
@@ -0,0 +1,8 @@
+# Input your Discord bot token below
+DISCORD_TOKEN=
+
+# Input your MySQL connection below
+DB_HOST=
+DB_USER=
+DB_PASS=
+DB_NAME=
diff --git a/daemon/discord/config.py.example b/daemon/discord/config.py.example
new file mode 100644
index 0000000..0e2f5f5
--- /dev/null
+++ b/daemon/discord/config.py.example
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+# Paste the admin role ID below
+# to filter only admin who can
+# access add/delete lore commands.
+ADMIN_ROLE_ID = 0
diff --git a/daemon/discord/execute_me.sql b/daemon/discord/execute_me.sql
new file mode 100644
index 0000000..80f2a32
--- /dev/null
+++ b/daemon/discord/execute_me.sql
@@ -0,0 +1,64 @@
+-- Adminer 4.7.6 MySQL dump
+
+SET NAMES utf8;
+SET time_zone = '+00:00';
+SET foreign_key_checks = 0;
+SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
+
+SET NAMES utf8mb4;
+
+DROP TABLE IF EXISTS `emails`;
+CREATE TABLE `emails` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `message_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `created_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `message_id` (`message_id`),
+ KEY `created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `discord_emails`;
+CREATE TABLE `discord_emails` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `email_id` bigint unsigned NOT NULL,
+ `channel_id` bigint NOT NULL,
+ `dc_msg_id` bigint unsigned NOT NULL,
+ `created_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `email_id` (`email_id`),
+ KEY `channel_id` (`channel_id`),
+ KEY `dc_msg_id` (`dc_msg_id`),
+ KEY `created_at` (`created_at`),
+ CONSTRAINT `discord_emails_ibfk_2` FOREIGN KEY (`email_id`) REFERENCES `emails` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `atom_urls`;
+CREATE TABLE `atom_urls` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `created_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `url` (`url`),
+ KEY `created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `broadcast_chats`;
+CREATE TABLE `broadcast_chats` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `guild_id` BIGINT UNSIGNED NOT NULL,
+ `channel_id` BIGINT UNSIGNED NOT NULL,
+ `channel_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `channel_link` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `created_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `guild_id` (`guild_id`),
+ UNIQUE KEY `channel_id` (`channel_id`),
+ KEY `channel_name` (`channel_name`),
+ KEY `channel_link` (`channel_link`),
+ KEY `created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+-- 2022-07-07 14:25:28
diff --git a/daemon/discord/gnuweeb/__init__.py b/daemon/discord/gnuweeb/__init__.py
new file mode 100644
index 0000000..f3d814a
--- /dev/null
+++ b/daemon/discord/gnuweeb/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .client import GWClient
diff --git a/daemon/discord/gnuweeb/client.py b/daemon/discord/gnuweeb/client.py
new file mode 100644
index 0000000..cf88d36
--- /dev/null
+++ b/daemon/discord/gnuweeb/client.py
@@ -0,0 +1,110 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+import discord
+from discord.ext import commands
+from discord import Intents
+from discord import Interaction
+from typing import Union
+
+from .models.ui import buttons
+from . import filters
+from mailer import utils
+from mailer import Database
+
+
+class GWClient(commands.Bot):
+ def __init__(self, db_conn) -> None:
+ self.db = Database(db_conn)
+ self.mailer = None
+ intents = Intents.default()
+ intents.message_content = True
+ super().__init__(
+ command_prefix=["$", "."],
+ description="Just a bot for receiving lore emails.",
+ intents=intents,
+ activity=discord.Game(
+ name="with Columbina Damselette",
+ type=0
+ )
+ )
+
+
+ async def setup_hook(self):
+ await self.load_extension("gnuweeb.plugins")
+
+ # WARNING! NOT RECOMMENDED SYNCING WHEN THE BOT IS START!!
+ # guild = discord.Object(id=845302963739033611)
+ # self.tree.copy_global_to(guild=guild)
+ # await self.tree.sync(guild=guild)
+
+
+ @filters.wait_on_limit
+ async def send_text_email(self, guild_id: int, chat_id: int, text: str,
+ reply_to: Union[int, None] = None, url: str = None):
+ print("[send_text_email]")
+ text = utils.bottom_border(text)
+ channel = self.get_channel(chat_id)
+
+ m = await channel.send(
+ content=text,
+ reference=discord.MessageReference(
+ guild_id=guild_id,
+ channel_id=chat_id,
+ message_id=reply_to
+ ) if reply_to else None,
+
+ view=buttons.FullMessageBtn(url)
+ )
+ return m
+
+
+ @filters.wait_on_limit
+ async def send_patch_email(self, mail, guild_id: int, chat_id: int, text: str,
+ reply_to: Union[int, None] = None, url: str = None):
+ print("[send_patch_email]")
+ tmp, doc, caption, url = utils.prepare_patch(mail, text, url)
+ channel = self.get_channel(chat_id)
+
+ m = await channel.send(
+ content=caption,
+ file=discord.File(doc),
+ reference=discord.MessageReference(
+ guild_id=guild_id,
+ channel_id=chat_id,
+ message_id=reply_to
+ ) if reply_to else None,
+
+ view=buttons.FullMessageBtn(url)
+ )
+
+ utils.remove_patch(tmp)
+ return m
+
+
+ async def send_text_mail_interaction(self, i: "Interaction",
+ text: str, url: str = None):
+ text = utils.border_and_trim(text)
+
+ m = await i.response.send_message(
+ content=text,
+ view=buttons.FullMessageBtn(url)
+ )
+
+ return m
+
+
+ async def send_patch_mail_interaction(self, mail, i: "Interaction",
+ text: str, url: str = None):
+ tmp, doc, caption, url = utils.prepare_patch(mail, text, url)
+
+ m = await i.response.send_message(
+ content=caption,
+ file=discord.File(doc),
+ view=buttons.FullMessageBtn(url)
+ )
+
+ utils.remove_patch(tmp)
+ return m
diff --git a/daemon/discord/gnuweeb/filters.py b/daemon/discord/gnuweeb/filters.py
new file mode 100644
index 0000000..735c77e
--- /dev/null
+++ b/daemon/discord/gnuweeb/filters.py
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+import config
+import discord
+import asyncio
+from discord import Interaction
+from discord.ext import commands
+from typing import Any, Callable, TypeVar
+from functools import wraps
+
+
+T = TypeVar("T")
+
+def channel(channel_id: int, admin_role_only: bool) -> Callable[[T], T]:
+ def _(func):
+ async def callback(*args: Any, **kwargs: Any) -> Any:
+ ctx: "commands.Context" = args[1]
+
+ if not channel_id == ctx.channel.id: return
+ if not admin_role_only: return
+
+ user_roles = [role.id for role in ctx.author.roles]
+ if config.ADMIN_ROLE_ID in user_roles:
+ return await func(*args, **kwargs,
+ content=ctx.message.content)
+
+ return callback
+ return _
+
+
+def lore_admin(func: Callable[[T], T]) -> Callable[[T], T]:
+ @wraps(func)
+ async def callback(*args: Any, **kwargs: Any) -> Any:
+ i: "Interaction" = args[1]
+ user_roles = [role.id for role in i.user.roles]
+
+ if config.ADMIN_ROLE_ID not in user_roles:
+ return await i.response.send_message(
+ "Sorry, you don't have this permission\n"\
+ "Tell the server admin to add you lore admin role.",
+ ephemeral=True
+ )
+ if config.ADMIN_ROLE_ID in user_roles:
+ return await func(*args, **kwargs)
+
+ return callback
+
+
+def wait_on_limit(func: Callable[[T], T]) -> Callable[[T], T]:
+ @wraps(func)
+ async def callback(*args: Any) -> Any:
+ while True:
+ try:
+ return await func(*args)
+ except discord.errors.RateLimited as e:
+ _flood_exceptions(e)
+ print("[wait_on_limit]: Woken up from flood wait...")
+ return callback
+
+
+async def _flood_exceptions(e: "discord.errors.RateLimited"):
+ wait = e.retry_after
+ print(f"[wait_on_limit]: Sleeping for {wait} seconds due to Telegram limit")
+ await asyncio.sleep(wait)
diff --git a/daemon/discord/gnuweeb/models/__init__.py b/daemon/discord/gnuweeb/models/__init__.py
new file mode 100644
index 0000000..7199f33
--- /dev/null
+++ b/daemon/discord/gnuweeb/models/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .ui import *
diff --git a/daemon/discord/gnuweeb/models/ui/__init__.py b/daemon/discord/gnuweeb/models/ui/__init__.py
new file mode 100644
index 0000000..9625317
--- /dev/null
+++ b/daemon/discord/gnuweeb/models/ui/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .buttons import FullMessageBtn
diff --git a/daemon/discord/gnuweeb/models/ui/buttons/__init__.py b/daemon/discord/gnuweeb/models/ui/buttons/__init__.py
new file mode 100644
index 0000000..c1d2a56
--- /dev/null
+++ b/daemon/discord/gnuweeb/models/ui/buttons/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .full_message_btn import FullMessageBtn
diff --git a/daemon/discord/gnuweeb/models/ui/buttons/full_message_btn.py b/daemon/discord/gnuweeb/models/ui/buttons/full_message_btn.py
new file mode 100644
index 0000000..310d761
--- /dev/null
+++ b/daemon/discord/gnuweeb/models/ui/buttons/full_message_btn.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord import ui
+from discord import ButtonStyle
+
+
+class FullMessageBtn(ui.View):
+ def __init__(self, url: str):
+ super().__init__()
+ self.__add_button(
+ label='See the full message',
+ style=ButtonStyle.gray,
+ url=url
+ )
+
+
+ def __add_button(self, **kwargs):
+ self.add_item(ui.Button(**kwargs))
diff --git a/daemon/discord/gnuweeb/plugins/__init__.py b/daemon/discord/gnuweeb/plugins/__init__.py
new file mode 100644
index 0000000..2902e99
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/__init__.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+from .basic_commands import BasicCommands
+from .events import Events
+from .slash_commands import SlashCommands
+
+
+class Plugins(
+ BasicCommands,
+ Events,
+ SlashCommands
+): pass
+
+
+async def setup(bot: "commands.Bot"):
+ await bot.add_cog(Plugins(bot))
diff --git a/daemon/discord/gnuweeb/plugins/basic_commands/__init__.py b/daemon/discord/gnuweeb/plugins/basic_commands/__init__.py
new file mode 100644
index 0000000..06a735b
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/basic_commands/__init__.py
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .debugger import DebuggerCommand
+from .sync_it import SyncCommand
+
+
+class BasicCommands(
+ DebuggerCommand,
+ SyncCommand
+): pass
diff --git a/daemon/discord/gnuweeb/plugins/basic_commands/debugger.py b/daemon/discord/gnuweeb/plugins/basic_commands/debugger.py
new file mode 100644
index 0000000..be5b9f1
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/basic_commands/debugger.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+from gnuweeb import utils
+# from gnuweeb import filters
+
+
+class DebuggerCommand(commands.Cog):
+ def __init__(self, bot) -> None:
+ self.bot = bot
+
+
+ @commands.command("exec", aliases=["exc", "d"])
+ @commands.is_owner() # only the bot owner who have this access
+ # @filters.channel(channel_id=865963658726211604, admin_role_only=True)
+ async def exec(self, ctx: "commands.Context", *, content: str):
+ await utils.execute_python_code(ctx, content)
diff --git a/daemon/discord/gnuweeb/plugins/basic_commands/sync_it.py b/daemon/discord/gnuweeb/plugins/basic_commands/sync_it.py
new file mode 100644
index 0000000..2ed35cc
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/basic_commands/sync_it.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+
+
+class SyncCommand(commands.Cog):
+ def __init__(self, bot) -> None:
+ self.bot = bot
+
+
+ @commands.command("sync", aliases=["s"])
+ @commands.is_owner()
+ async def sync_it(self, ctx: "commands.Context"):
+ ctx.bot.tree.copy_global_to(guild=ctx.guild)
+ s = await ctx.bot.tree.sync(guild=ctx.guild)
+
+ await ctx.send(f"Synced {len(s)} commands.")
diff --git a/daemon/discord/gnuweeb/plugins/events/__init__.py b/daemon/discord/gnuweeb/plugins/events/__init__.py
new file mode 100644
index 0000000..7074e4d
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/events/__init__.py
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .on_ready import OnReady
+from .on_error import OnError
+
+
+class Events(
+ OnReady,
+ OnError
+): pass
diff --git a/daemon/discord/gnuweeb/plugins/events/on_error.py b/daemon/discord/gnuweeb/plugins/events/on_error.py
new file mode 100644
index 0000000..e1e4e28
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/events/on_error.py
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+
+
+class OnError(commands.Cog):
+ def __init__(self, bot: "commands.Bot") -> None:
+ self.bot = bot
+
+
+ @commands.Cog.listener()
+ async def on_command_error(self, _, err):
+ if isinstance(err, commands.CommandNotFound):
+ pass
diff --git a/daemon/discord/gnuweeb/plugins/events/on_ready.py b/daemon/discord/gnuweeb/plugins/events/on_ready.py
new file mode 100644
index 0000000..1554050
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/events/on_ready.py
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+
+
+class OnReady(commands.Cog):
+ def __init__(self, bot: "commands.Bot") -> None:
+ self.bot = bot
+
+
+ @commands.Cog.listener()
+ async def on_ready(self):
+ t = "[ GNU/Weeb Bot is connected ]\n\n"
+ t += f"ID : {self.bot.user.id}\n"
+ t += f"Name : {self.bot.user.display_name}\n"
+ t += f"Tags : {self.bot.user}\n\n"
+ t += "Ready to get the latest of lore kernel emails."
+ # starting to listen new emails when the bot
+ # is ready to sending the messages.
+ self.bot.mailer.run()
+ print(t)
diff --git a/daemon/discord/gnuweeb/plugins/slash_commands/__init__.py b/daemon/discord/gnuweeb/plugins/slash_commands/__init__.py
new file mode 100644
index 0000000..6e5929a
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/slash_commands/__init__.py
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .manage_atom import ManageAtomSC
+from .manage_broadcast import ManageBroadcastSC
+from .get_lore_mail import GetLoreSC
+
+
+class SlashCommands(
+ ManageAtomSC,
+ ManageBroadcastSC,
+ GetLoreSC
+): pass
diff --git a/daemon/discord/gnuweeb/plugins/slash_commands/get_lore_mail.py b/daemon/discord/gnuweeb/plugins/slash_commands/get_lore_mail.py
new file mode 100644
index 0000000..a2671f4
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/slash_commands/get_lore_mail.py
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+import shutil
+import asyncio
+import discord
+from discord.ext import commands
+from discord import Interaction
+from discord import app_commands
+
+from mailer import utils
+from mailer import Scraper
+
+
+class GetLoreSC(commands.Cog):
+ def __init__(self, bot) -> None:
+ self.bot = bot
+
+
+ @app_commands.command(
+ name="lore",
+ description="Get lore email from raw email URL."
+ )
+ @app_commands.describe(url="Raw lore email URL")
+ async def get_lore(self, i: "Interaction", url: str):
+ s = Scraper()
+ mail = await s.get_email_from_url(url)
+ text, files, is_patch = utils.create_template(mail)
+
+ if is_patch:
+ m = await self.bot.send_patch_mail_interaction(
+ mail=mail, i=i, text=text, url=url
+ )
+ else:
+ text = "#ml\n" + text
+ m = await self.bot.send_text_mail_interaction(
+ i=i, text=text, url=url
+ )
+
+ for d, f in files:
+ await m.reply(f"{d}/{f}", file=discord.File(f))
+ await asyncio.sleep(1)
+
+ if files:
+ shutil.rmtree(str(files[0][0]))
diff --git a/daemon/discord/gnuweeb/plugins/slash_commands/manage_atom.py b/daemon/discord/gnuweeb/plugins/slash_commands/manage_atom.py
new file mode 100644
index 0000000..2d9da80
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/slash_commands/manage_atom.py
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+from discord import Interaction
+from discord import app_commands
+
+from gnuweeb import filters
+from mailer import utils
+
+
+class ManageAtomSC(commands.Cog):
+ atom = app_commands.Group(
+ name="atom",
+ description="Manage lore atom URL."
+ )
+
+ def __init__(self, bot) -> None:
+ self.bot = bot
+
+
+ @atom.command(
+ name="list",
+ description="List of lore atom URL."
+ )
+ @filters.lore_admin
+ async def list_atom(self, i: "Interaction"):
+ atoms = self.bot.db.get_atom_urls()
+ if len(atoms) == 0:
+ t = "Currently empty."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ text = "List of atom URL that currently listened:\n"
+ for u,n in zip(atoms, range(1, len(atoms)+1)):
+ text += f"{n}. {u}\n"
+
+ await i.response.send_message(text, ephemeral=True)
+
+
+ @atom.command(
+ name="add",
+ description="Add lore atom URL for receiving lore emails."
+ )
+ @app_commands.describe(url='Lore atom URL.')
+ @filters.lore_admin
+ async def add_atom(self, i: "Interaction", url: str):
+ is_atom = await utils.is_atom_url(url)
+ if not is_atom:
+ t = "Invalid Atom URL."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ inserted = self.bot.db.insert_atom(url)
+ if inserted is None:
+ t = f"This URL already listened for new email."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ t = f"Success add **{url}** for listening new email."
+ await i.response.send_message(t, ephemeral=True)
+
+
+ @atom.command(
+ name="delete",
+ description="Delete lore atom URL from receiving lore emails."
+ )
+ @app_commands.describe(url='Lore atom URL.')
+ @filters.lore_admin
+ async def del_atom(self, i: "Interaction", url: str):
+ success = self.bot.db.delete_atom(url)
+ if not success:
+ t = "Failed to delete atom URL\n"
+ t += "Maybe because already deleted or not exists."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ t = f"Success delete **{url}** from receiving lore emails."
+ await i.response.send_message(t, ephemeral=True)
diff --git a/daemon/discord/gnuweeb/plugins/slash_commands/manage_broadcast.py b/daemon/discord/gnuweeb/plugins/slash_commands/manage_broadcast.py
new file mode 100644
index 0000000..9eb6b98
--- /dev/null
+++ b/daemon/discord/gnuweeb/plugins/slash_commands/manage_broadcast.py
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from discord.ext import commands
+from discord import Interaction
+from discord import app_commands
+
+from gnuweeb import utils
+from gnuweeb import filters
+
+
+class ManageBroadcastSC(commands.Cog):
+ broadcast = app_commands.Group(
+ name="broadcast",
+ description="Manage broadcast channel."
+ )
+
+ def __init__(self, bot) -> None:
+ self.bot = bot
+
+
+ @broadcast.command(
+ name="list",
+ description="List of broadcast channel."
+ )
+ @filters.lore_admin
+ async def list_channel(self, i: "Interaction"):
+ chats = self.bot.db.get_broadcast_chats()
+ if len(chats) == 0:
+ t = "Currently empty."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ text = "List of channels that will send email messages:\n"
+ for u,n in zip(chats, range(1, len(chats)+1)):
+ text += f"{n}. **{u[3]}**\n"
+ text += f"Link: {u[4]}\n\n"
+
+ await i.response.send_message(text, ephemeral=True)
+
+
+ @broadcast.command(
+ name="add",
+ description="Add broadcast channel for sending lore emails."
+ )
+ @filters.lore_admin
+ async def add_channel(self, i: "Interaction"):
+ inserted = self.bot.db.insert_broadcast(
+ guild_id=i.guild_id,
+ channel_id=i.channel_id,
+ channel_name=i.channel.name,
+ channel_link=utils.channel_link(
+ guild_id=i.guild_id,
+ channel_id=i.channel_id
+ )
+ )
+
+ if inserted is None:
+ t = f"This channel already added for send email messages."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ t = f"Success add this channel for send email messages."
+ await i.response.send_message(t, ephemeral=True)
+
+
+ @broadcast.command(
+ name="delete",
+ description="Delete broadcast channel from sending email messages."
+ )
+ @filters.lore_admin
+ async def del_channel(self, i: "Interaction"):
+ success = self.bot.db.delete_broadcast(i.channel_id)
+ if not success:
+ t = "Failed to delete this channel\n"
+ t += "Maybe because already deleted or not exists."
+ await i.response.send_message(t, ephemeral=True)
+ return
+
+ t = f"Success delete this channel from sending email messages."
+ await i.response.send_message(t, ephemeral=True)
diff --git a/daemon/discord/gnuweeb/utils.py b/daemon/discord/gnuweeb/utils.py
new file mode 100644
index 0000000..d60e932
--- /dev/null
+++ b/daemon/discord/gnuweeb/utils.py
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+import re
+import io
+import import_expression
+import contextlib
+import traceback
+from discord.ext import commands
+from textwrap import indent
+
+
+def channel_link(guild_id: int, channel_id: int):
+ return f"https://discord.com/channels/{guild_id}/{channel_id}"
+
+
+async def execute_python_code(ctx: "commands.Context", content: str):
+ reg = re.compile(r"`{3}py\n([\w\W]*?)`{3}", flags=re.DOTALL)
+ code = reg.search(content).group(1)
+
+ env = {"ctx": ctx}
+ env.update(globals())
+
+ stdout = io.StringIO()
+ to_compile = f'async def func(ctx):\n{indent(code, " ")}'
+
+ try:
+ import_expression.exec(to_compile, env)
+ except Exception as e:
+ o = f"```py\n{e.__class__.__name__}: {e}"[0:1904]+"\n```"
+ return await ctx.send(o)
+
+ func = env["func"]
+
+ try:
+ with contextlib.redirect_stdout(stdout):
+ ret = await func(ctx)
+ except Exception:
+ value = stdout.getvalue()
+ o = f"```{value}{traceback.format_exc()}"[0:1904]+"```"
+ return await ctx.send(o)
+ else:
+ value = stdout.getvalue()
+ if ret is None:
+ if value:
+ o = f"```py\n{value}"[0:1904]+"\n```"
+ return await ctx.send(o)
+ else:
+ o = f"```py\n{value}{ret}"[0:1904]+"\n```"
+ return await ctx.send(o)
diff --git a/daemon/discord/mailer/__init__.py b/daemon/discord/mailer/__init__.py
new file mode 100644
index 0000000..0da4329
--- /dev/null
+++ b/daemon/discord/mailer/__init__.py
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+from .database import Database
+from .scraper import Scraper
diff --git a/daemon/discord/mailer/database.py b/daemon/discord/mailer/database.py
new file mode 100644
index 0000000..0b91558
--- /dev/null
+++ b/daemon/discord/mailer/database.py
@@ -0,0 +1,203 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+# Copyright (C) 2022 Ammar Faizi <[email protected]>
+#
+
+from datetime import datetime
+import mysql
+
+
+class Database:
+ def __init__(self, conn):
+ self.conn = conn
+ self.conn.autocommit = True
+ self.cur = self.conn.cursor(buffered=True)
+
+
+ def __del__(self):
+ self.cur.close()
+ self.conn.close()
+
+
+ def save_email_msg_id(self, email_msg_id):
+ try:
+ return self.__save_email_msg_id(email_msg_id)
+ except mysql.connector.errors.IntegrityError:
+ #
+ # Duplicate data, skip!
+ #
+ return None
+
+
+ def __save_email_msg_id(self, email_msg_id):
+ q = "INSERT INTO emails (message_id, created_at) VALUES (%s, %s)"
+ self.cur.execute(q, (email_msg_id, datetime.utcnow()))
+ return self.cur.lastrowid
+
+
+ def insert_discord(self, email_id, channel_id, dc_msg_id):
+ q = """
+ INSERT INTO discord_emails
+ (email_id, channel_id, dc_msg_id, created_at)
+ VALUES (%s, %s, %s, %s);
+ """
+ self.cur.execute(q, (email_id, channel_id, dc_msg_id,
+ datetime.utcnow()))
+ return self.cur.lastrowid
+
+
+ #
+ # Determine whether the email needs to be sent to @dc_chat_id.
+ #
+ # - Return an email id (PK) if it needs to be sent.
+ # - Return None if it doesn't need to be sent.
+ #
+ def get_email_id_sent(self, email_msg_id, channel_id):
+ q = """
+ SELECT emails.id, discord_emails.id FROM emails
+ LEFT JOIN discord_emails
+ ON emails.id = discord_emails.email_id
+ WHERE emails.message_id = %(email_msg_id)s
+ AND discord_emails.channel_id = %(channel_id)s
+ LIMIT 1
+ """
+
+ self.cur.execute(
+ q,
+ {
+ "email_msg_id": email_msg_id,
+ "channel_id": channel_id
+ }
+ )
+ res = self.cur.fetchone()
+ if bool(res):
+ #
+ # This email has already been sent to
+ # @dc_chat_id.
+ #
+ return None
+
+ q = """
+ SELECT id FROM emails WHERE message_id = %(email_msg_id)s
+ """
+ self.cur.execute(q, {"email_msg_id": email_msg_id})
+ res = self.cur.fetchone()
+ if not bool(res):
+ #
+ # Something goes wrong, skip!
+ #
+ return None
+
+ return int(res[0])
+
+
+ def get_discord_reply(self, email_msg_id, channel_id):
+ q = """
+ SELECT discord_emails.dc_msg_id
+ FROM emails INNER JOIN discord_emails
+ ON emails.id = discord_emails.email_id
+ WHERE emails.message_id = %(email_msg_id)s
+ AND discord_emails.channel_id = %(channel_id)s
+ """
+
+ self.cur.execute(
+ q,
+ {
+ "email_msg_id": email_msg_id,
+ "channel_id": channel_id
+ }
+ )
+ res = self.cur.fetchone()
+ if not bool(res):
+ return None
+
+ return res[0]
+
+
+ def insert_atom(self, atom: str):
+ try:
+ return self.__save_atom(atom)
+ except mysql.connector.errors.IntegrityError:
+ #
+ # Duplicate data, skip!
+ #
+ return None
+
+
+ def __save_atom(self, atom: str):
+ q = "INSERT INTO atom_urls (url, created_at) VALUES (%s, %s)"
+ self.cur.execute(q, (atom, datetime.utcnow()))
+ return self.cur.lastrowid
+
+
+ def delete_atom(self, atom: str):
+ q = """
+ DELETE FROM atom_urls
+ WHERE url = %(atom)s
+ """
+ self.cur.execute(q, {"atom": atom})
+ return self.cur.rowcount > 0
+
+
+ def get_atom_urls(self):
+ q = """
+ SELECT atom_urls.url
+ FROM atom_urls
+ """
+ self.cur.execute(q)
+ urls = self.cur.fetchall()
+
+ return [u[0] for u in urls]
+
+
+ def insert_broadcast(
+ self,
+ guild_id: int,
+ channel_id: int,
+ channel_name: str,
+ channel_link: str = None,
+ ):
+ try:
+ return self.__save_broadcast(
+ guild_id=guild_id,
+ channel_id=channel_id,
+ channel_name=channel_name,
+ channel_link=channel_link
+ )
+ except mysql.connector.errors.IntegrityError:
+ #
+ # Duplicate data, skip!
+ #
+ return None
+
+
+ def __save_broadcast(
+ self,
+ guild_id: int,
+ channel_id: int,
+ channel_name: str,
+ channel_link: str = None,
+ ):
+ q = """
+ INSERT INTO broadcast_chats
+ (guild_id, channel_id, channel_name, channel_link, created_at)
+ VALUES (%s, %s, %s, %s, %s)
+ """
+ values = (guild_id, channel_id, channel_name, channel_link, datetime.utcnow())
+ self.cur.execute(q, values)
+ return self.cur.lastrowid
+
+
+ def delete_broadcast(self, channel_id: int):
+ q = """
+ DELETE FROM broadcast_chats
+ WHERE channel_id = %(channel_id)s
+ """
+ self.cur.execute(q, {"channel_id": channel_id})
+ return self.cur.rowcount > 0
+
+
+ def get_broadcast_chats(self):
+ self.cur.execute("SELECT * FROM broadcast_chats")
+ return self.cur.fetchall()
diff --git a/daemon/discord/mailer/listener.py b/daemon/discord/mailer/listener.py
new file mode 100644
index 0000000..523a87d
--- /dev/null
+++ b/daemon/discord/mailer/listener.py
@@ -0,0 +1,152 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+# Copyright (C) 2022 Ammar Faizi <[email protected]>
+#
+
+import asyncio
+import traceback
+import shutil
+import re
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+from discord import File
+
+from gnuweeb import GWClient
+from .scraper import Scraper
+from . import utils
+
+
+class BotMutexes():
+ def __init__(self):
+ self.lock = asyncio.Lock()
+
+
+class Listener():
+ def __init__(
+ self,
+ client: "GWClient",
+ sched: "AsyncIOScheduler",
+ scraper: Scraper,
+ mutexes: "BotMutexes"
+ ):
+ self.client = client
+ self.sched = sched
+ self.scraper = scraper
+ self.mutexes = mutexes
+ self.db = client.db
+ self.isRunnerFixed = False
+ self.runner = None
+
+
+ def run(self):
+ #
+ # Execute __run() once to avoid high latency at
+ # initilization.
+ #
+ self.sched.start()
+ self.runner = self.sched.add_job(func=self.__run)
+
+
+ async def __run(self):
+ print("[__run]: Running...")
+ for url in self.db.get_atom_urls():
+ try:
+ await self.__handle_atom_url(url)
+ except:
+ print(traceback.format_exc())
+
+ if not self.isRunnerFixed:
+ self.isRunnerFixed = True
+ self.runner = self.sched.add_job(
+ func=self.__run,
+ trigger="interval",
+ seconds=30,
+ misfire_grace_time=None,
+ max_instances=1
+ )
+
+
+ async def __handle_atom_url(self, url):
+ urls = await self.scraper.get_new_threads_urls(url)
+ for url in urls:
+ mail = await self.scraper.get_email_from_url(url)
+ await self.__handle_mail(url, mail)
+
+
+ async def __handle_mail(self, url, mail):
+ chats = self.db.get_broadcast_chats()
+ for chat in chats:
+ async with self.mutexes.lock:
+ should_wait = \
+ await self.__send_to_discord(url, mail,
+ chat[1], chat[2])
+
+ if should_wait:
+ await asyncio.sleep(1)
+
+
+ # @__must_hold(self.mutexes.lock)
+ async def __send_to_discord(self, url, mail, dc_guild_id, dc_chat_id):
+ email_msg_id = utils.get_email_msg_id(mail)
+ if not email_msg_id:
+ #
+ # It doesn't have a Message-Id.
+ # A malformed email. Skip!
+ #
+ return False
+
+ email_id = self.__get_email_id_sent(
+ email_msg_id=email_msg_id,
+ dc_chat_id=dc_chat_id
+ )
+ if not email_id:
+ #
+ # Email has already been sent to Discord.
+ # Skip!
+ #
+ return False
+
+ text, files, is_patch = utils.create_template(mail)
+ reply_to = self.get_discord_reply(mail, dc_chat_id)
+ url = str(re.sub(r"/raw$", "", url))
+
+ if is_patch:
+ m = await self.client.send_patch_email(
+ mail, dc_guild_id, dc_chat_id, text, reply_to, url
+ )
+ else:
+ text = "#ml\n" + text
+ m = await self.client.send_text_email(
+ dc_guild_id, dc_chat_id, text, reply_to, url
+ )
+
+ self.db.insert_discord(email_id, m.channel.id, m.id)
+ for d, f in files:
+ await m.reply(f"{d}/{f}", file=File(f))
+ await asyncio.sleep(1)
+
+ if files:
+ shutil.rmtree(str(files[0][0]))
+
+ return True
+
+
+ def __get_email_id_sent(self, email_msg_id, dc_chat_id):
+ email_id = self.db.save_email_msg_id(email_msg_id)
+ if email_id:
+ return email_id
+
+ email_id = self.db.get_email_id_sent(email_msg_id, dc_chat_id)
+ return email_id
+
+
+ def get_discord_reply(self, mail, dc_chat_id):
+ reply_to = mail.get("in-reply-to")
+ if not reply_to:
+ return None
+
+ reply_to = utils.extract_email_msg_id(reply_to)
+ if not reply_to:
+ return None
+
+ return self.db.get_discord_reply(reply_to, dc_chat_id)
diff --git a/daemon/discord/mailer/scraper.py b/daemon/discord/mailer/scraper.py
new file mode 100644
index 0000000..3873161
--- /dev/null
+++ b/daemon/discord/mailer/scraper.py
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+# Copyright (C) 2022 Ammar Faizi <[email protected]>
+#
+
+from typing import Dict, List
+import email.policy
+import xmltodict
+import httpx
+import email
+
+
+class Scraper:
+ async def get_new_threads_urls(self, atom_url):
+ ret = await self.__get_atom_content(atom_url)
+ return await self.__get_new_threads_from_atom(ret)
+
+
+ async def __get_atom_content(self, atom_url):
+ async with httpx.AsyncClient() as client:
+ res = await client.get(atom_url)
+ if res.status_code == 200:
+ return res.text
+ raise Exception(f"[get_atom_content]: Returned {res.status_code} HTTP code")
+
+
+ async def __get_new_threads_from_atom(self, atom):
+ j: Dict[str, List[
+ Dict[str, str]
+ ]] = xmltodict.parse(atom)["feed"]
+
+ entry = []
+ e = j["entry"]
+ for i in e:
+ entry.append({
+ "link": i["link"]["@href"],
+ "title": i["title"],
+ "updated": i["updated"],
+ })
+ #
+ # TODO(ammarfaizi2): Sort by title as well if the @updated is
+ # identic.
+ #
+ entry.sort(key=lambda x: x["updated"])
+
+ ret = []
+ for i in entry:
+ link = i["link"].replace("http://", "https://")
+ ret.append(link + "raw")
+
+ return ret
+
+
+ async def get_email_from_url(self, url):
+ async with httpx.AsyncClient() as client:
+ res = await client.get(url)
+ if res.status_code == 200:
+ return email.message_from_string(
+ res.text,
+ policy=email.policy.default
+ )
+ raise Exception(f"[get_atom_content]: Returned {res.status_code} HTTP code")
diff --git a/daemon/discord/mailer/utils.py b/daemon/discord/mailer/utils.py
new file mode 100644
index 0000000..0036f24
--- /dev/null
+++ b/daemon/discord/mailer/utils.py
@@ -0,0 +1,241 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+# Copyright (C) 2022 Ammar Faizi <[email protected]>
+#
+
+from email.message import Message
+from typing import Dict
+from slugify import slugify
+import hashlib
+import uuid
+import os
+import re
+import shutil
+import httpx
+
+
+def get_email_msg_id(mail):
+ ret = mail.get("message-id")
+ if not ret:
+ return None
+
+ ret = re.search(r"<([^\<\>]+)>", ret)
+ if not ret:
+ return None
+
+ return ret.group(1)
+
+
+#
+# This increments the @i while we are seeing a whitespace.
+#
+def __skip_whitespace(i, ss_len, ss):
+ while i < ss_len:
+ c = ss[i]
+ if c != ' ' and c != '\t' and c != '\n':
+ break
+ i += 1
+
+ return i
+
+
+#
+# Pick a single element in the list. The delimiter here is
+# a comma char ','. But note that when are inside a double
+# quotes, we must not take the comma as a delimiter.
+#
+def __pick_element(i, ss_len, ss, ret):
+ acc = ""
+ in_quotes = False
+
+ while i < ss_len:
+ c = ss[i]
+ i += 1
+
+ if c == '"':
+ in_quotes = (not in_quotes)
+
+ if not in_quotes and c == ',':
+ break
+
+ acc += c
+
+ if acc != "":
+ ret.append(acc)
+
+ return i
+
+
+def __extract_list(ss):
+ ss = ss.strip()
+ ss_len = len(ss)
+ ret = []
+ i = 0
+
+ while i < ss_len:
+ i = __skip_whitespace(i, ss_len, ss)
+ i = __pick_element(i, ss_len, ss, ret)
+
+ return ret
+
+
+def extract_list(key: str, content: Dict[str, str]):
+ people = content.get(key.lower())
+ if not people:
+ return []
+ return __extract_list(people)
+
+
+def consruct_to_n_cc(to: list, cc: list):
+ NR_MAX_LIST = 20
+
+ n = 0
+ ret = ""
+ for i in to:
+ if n >= NR_MAX_LIST:
+ ret += "To: ...\n"
+ break
+
+ n += 1
+ ret += f"To: {i}\n"
+
+ for i in cc:
+ if n >= NR_MAX_LIST:
+ ret += "Cc: ...\n"
+ break
+
+ n += 1
+ ret += f"Cc: {i}\n"
+
+ return ret
+
+
+def gen_temp(name: str):
+ md5 = hashlib.md5(name.encode()).hexdigest()
+ ret = os.getenv("STORAGE_DIR", "storage") + "/" + md5
+ try:
+ os.mkdir(ret)
+ except FileExistsError:
+ pass
+
+ return ret
+
+
+def extract_body(thread: Message):
+ if not thread.is_multipart():
+ p = thread.get_payload(decode=True)
+ return f"{p.decode(errors='replace')}\n".lstrip(), []
+
+ ret = ""
+ files = []
+ temp = gen_temp(str(uuid.uuid4()))
+ for p in thread.get_payload():
+ fname = p.get_filename()
+ payload = p.get_payload(decode=True)
+
+ if not payload:
+ continue
+
+ if 'inline' in [p.get('content-disposition')] or not bool(fname):
+ ret += f"{payload.decode(errors='replace')}\n".lstrip()
+ continue
+
+ with open(f"{temp}/{fname}", "wb") as f:
+ f.write(payload)
+ files.append((temp, fname))
+
+ ret = re.sub("^(>)", ">>> \\1", ret, 1, re.MULTILINE)
+ return ret, files
+
+
+
+PATCH_PATTERN = r"^\[.*(?:patch|rfc).*?(?:(\d+)\/(\d+))?\](.+)"
+def __is_patch(subject, content):
+ x = re.search(PATCH_PATTERN, subject, re.IGNORECASE)
+ if not x or x.group(1) == "0":
+ return False
+
+ x = re.search(r"diff --git", content)
+ if not x:
+ return False
+
+ return True
+
+
+def create_template(thread: Message, to=None, cc=None):
+ if not to:
+ to = extract_list("to", thread)
+ if not cc:
+ cc = extract_list("cc", thread)
+
+ subject = thread.get('subject')
+ ret = f"From: {thread.get('from')}\n"
+ ret += consruct_to_n_cc(to, cc)
+ ret += f"Date: {thread.get('date')}\n"
+ ret += f"Subject: {subject}\n\n"
+ content, files = extract_body(thread)
+ is_patch = __is_patch(subject, content)
+
+ if is_patch:
+ ret += content
+ else:
+ ret += content.strip().replace("\t", " ")
+ if len(ret) >= 1900:
+ ret = ret[:1900] + "..."
+
+ ret = fix_utf8_char(ret)
+
+ return ret, files, is_patch
+
+
+def prepare_patch(mail, text, url):
+ tmp = gen_temp(url)
+ fnm = str(mail.get("subject"))
+ sch = re.search(PATCH_PATTERN, fnm, re.IGNORECASE)
+
+ nr_patch = sch.group(1)
+ if not nr_patch:
+ nr_patch = 1
+ else:
+ nr_patch = int(nr_patch)
+
+ num = "%04d" % nr_patch
+ fnm = slugify(sch.group(3)).replace("_", "-")
+ file = f"{tmp}/{num}-{fnm}.patch"
+
+ with open(file, "wb") as f:
+ f.write(bytes(text, encoding="utf8"))
+
+ caption = "#patch #ml"
+ return tmp, file, caption, url
+
+
+def remove_patch(tmp):
+ shutil.rmtree(tmp)
+
+
+def fix_utf8_char(text: str):
+ return text.rstrip().replace("�"," ")
+
+
+def bottom_border(text: str):
+ return text + "\n" + "-"*72
+
+
+EMAIL_MSG_ID_PATTERN = r"<([^\<\>]+)>"
+def extract_email_msg_id(msg_id):
+ ret = re.search(EMAIL_MSG_ID_PATTERN, msg_id)
+ if not ret:
+ return None
+ return ret.group(1)
+
+
+async def is_atom_url(text: str):
+ try:
+ async with httpx.AsyncClient() as ses:
+ res = await ses.get(text)
+ mime = res.headers.get("Content-Type")
+
+ return mime == "application/atom+xml"
+ except: return False
diff --git a/daemon/discord/requirements.txt b/daemon/discord/requirements.txt
new file mode 100644
index 0000000..adc5a91
--- /dev/null
+++ b/daemon/discord/requirements.txt
@@ -0,0 +1,8 @@
+discord.py
+apscheduler
+python-slugify
+httpx
+import-expression
+mysql-connector-python
+python-dotenv
+xmltodict
diff --git a/daemon/discord/run.py b/daemon/discord/run.py
new file mode 100644
index 0000000..e3eb7a6
--- /dev/null
+++ b/daemon/discord/run.py
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (C) 2022 Muhammad Rizki <[email protected]>
+#
+
+import os
+from mysql import connector
+from dotenv import load_dotenv
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+from mailer.listener import BotMutexes
+from mailer.listener import Listener
+
+from gnuweeb import GWClient
+from mailer import Scraper
+
+
+def main():
+ load_dotenv()
+
+ sched = AsyncIOScheduler(
+ job_defaults={
+ "max_instances": 3,
+ "misfire_grace_time": None
+ }
+ )
+
+ client = GWClient(
+ db_conn=connector.connect(
+ host=os.getenv("DB_HOST"),
+ user=os.getenv("DB_USER"),
+ password=os.getenv("DB_PASS"),
+ database=os.getenv("DB_NAME")
+ )
+ )
+
+ mailer = Listener(
+ client=client,
+ sched=sched,
+ scraper=Scraper(),
+ mutexes=BotMutexes()
+ )
+ client.mailer = mailer
+
+ client.run(os.getenv("DISCORD_TOKEN"))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/daemon/discord/storage/.gitignore b/daemon/discord/storage/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/daemon/discord/storage/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/daemon/telegram/packages/plugins/admin.py b/daemon/telegram/packages/plugins/admin.py
deleted file mode 100644
index e0f145e..0000000
--- a/daemon/telegram/packages/plugins/admin.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-only
-#
-# Copyright (C) 2022 Muhammad Rizki <[email protected]>
-#
-
-from pyrogram import Client, filters, enums
-from pyrogram.types import Message
-from textwrap import indent
-import io, import_expression, contextlib, traceback
-
[email protected]_message(
- filters.command(['d','debug']) &
- filters.user(["nekoha", "kiizuah"])
-)
-async def execute_v2(c: Client, m: Message):
- sep = m.text.split('\n')
- body = m.text.replace(sep[0] + '\n','')
-
- env = {"bot": c}
- env.update(globals())
-
- stdout = io.StringIO()
- to_compile = f'async def func(_, m):\n{indent(body, " ")}'
-
- try:
- import_expression.exec(to_compile, env)
- except Exception as e:
- text = f"```{e.__class__.__name__}: {e}"[0:4096]+"```"
-
- func = env["func"]
-
- try:
- with contextlib.redirect_stdout(stdout):
- await func(c, m)
- except Exception:
- value = stdout.getvalue()
- text = f"```{value}{traceback.format_exc()}"[0:4096]+"```"
- else:
- value = stdout.getvalue()
- text = f"```{value}"[0:4096]+"```"
-
- await c.send_message(m.chat.id, text, parse_mode=enums.ParseMode.MARKDOWN)
diff --git a/daemon/telegram/packages/plugins/scrape.py b/daemon/telegram/packages/plugins/scrape.py
deleted file mode 100644
index 1698c6d..0000000
--- a/daemon/telegram/packages/plugins/scrape.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-only
-#
-# Copyright (C) 2022 Muhammad Rizki <[email protected]>
-# Copyright (C) 2022 Ammar Faizi <[email protected]>
-#
-
-from pyrogram.types import InlineKeyboardMarkup
-from pyrogram.types import InlineKeyboardButton
-from pyrogram.types import Message
-from pyrogram import filters
-from pyrogram import Client
-from scraper import Scraper
-from pyrogram import enums
-from scraper import utils
-from scraper import Bot
-import shutil
-import re
-import asyncio
-
-
-#
-# This allows user to invoke the following commands:
-# /lore https://lore.kernel.org/path/message_id/raw
-# !lore https://lore.kernel.org/path/message_id/raw
-# .lore https://lore.kernel.org/path/message_id/raw
-#
-LORE_CMD_URL_PATTERN = r"^(?:\/|\.|\!)lore\s+(https?:\/\/lore\.kernel\.org\/\S+)"
[email protected]_message(
- filters.regex(LORE_CMD_URL_PATTERN) &
- filters.chat(["kiizuah", "nekoha", -1001673279485])
-)
-async def scrap_email(_, m: Message):
- p = re.search(LORE_CMD_URL_PATTERN, m.text)
- if not p:
- return
-
- url = p.group(1)
- if not url:
- return
-
- s = Scraper()
- mail = await s.get_email_from_url(url)
- text, files, is_patch = utils.create_template(mail)
-
- if is_patch:
- m = await __send_patch_msg(m, mail, text, url)
- else:
- text = "#ml\n" + text
- m = await __send_text_msg(m, text, url)
-
- for d, f in files:
- await m.reply_document(f"{d}/{f}", file_name=f)
- await asyncio.sleep(1)
-
- if files:
- shutil.rmtree(str(files[0][0]))
-
-
-async def __send_patch_msg(m, mail, text, url):
- tmp, fnm, caption, url = Bot.prepare_send_patch(mail, text, url)
- ret = await m.reply_document(
- fnm,
- caption=caption,
- parse_mode=enums.ParseMode.HTML,
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton(
- "See the full message",
- url=url
- )]
- ])
- )
- Bot.clean_up_after_send_patch(tmp)
- return ret
-
-
-async def __send_text_msg(m, text, url):
- return await m.reply(
- text,
- parse_mode=enums.ParseMode.HTML,
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton(
- "See the full message",
- url=url.replace("/raw","")
- )]
- ])
- )
--
Muhammad Rizki
next prev parent reply other threads:[~2022-08-25 16:10 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-08-25 16:09 [PATCH v1 0/3] New Discord bot and full refactor scripts Muhammad Rizki
2022-08-25 16:09 ` [PATCH v1 1/3] Move the Telegram bot source code Muhammad Rizki
2022-08-25 16:09 ` Muhammad Rizki [this message]
2022-08-26 1:49 ` [PATCH v1 2/3] First release Discord bot Alviro Iskandar Setiawan
2022-08-26 1:54 ` Ammar Faizi
2022-08-26 1:59 ` Muhammad Rizki
2022-08-25 16:09 ` [PATCH v1 3/3] Full refactor bot scripts Muhammad Rizki
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox