diff --git a/botbotbot/__init__.py b/botbotbot/__init__.py
index 46d82be..aca06b4 100644
--- a/botbotbot/__init__.py
+++ b/botbotbot/__init__.py
@@ -6,6 +6,7 @@ import tomllib
 import discord
 
 from botbotbot.ai import AIBot
+from botbotbot.text import TextBot
 from botbotbot.tts import CambAI
 
 
@@ -50,81 +51,15 @@ def main() -> None:
 
     bot = discord.Bot(description=description, intents=intents)
 
+    text_bot = TextBot(bot, aibot, word_list)
+    text_bot.init_events()
+
     shuffle_tasks = set()
 
     @bot.listen("on_ready")
     async def on_ready() -> None:
         logger.info(f"We have logged in as {bot.user}")
 
-    async def reply(message: discord.Message) -> None:
-        logger.info(f"Reply to {message.author}")
-        mention = random.choices(
-            [f"<@{message.author.id}>", "@everyone", "@here"], weights=(98, 1, 1)
-        )[0]
-        content = random.choice(
-            (
-                f"{mention}, {random.choice(word_list)}",
-                f"{random.choice(word_list)}",
-            )
-        )
-
-        if isinstance(message.channel, discord.TextChannel) and random.random() < 0.1:
-            await send_as_webhook(
-                message.channel,
-                message.author.display_name,
-                message.author.avatar.url if message.author.avatar else None,
-                content,
-            )
-        else:
-            fct = random.choice((message.reply, message.channel.send))
-
-            await fct(content)
-
-    async def ai_reply(message: discord.Message) -> None:
-        if aibot is None:
-            return
-
-        logger.info(f"AI Reply to {message.author}")
-        prompt = message.clean_content
-        if prompt == "" and message.embeds and message.embeds[0].description:
-            prompt = message.embeds[0].description
-
-        answer = aibot.answer(prompt)
-        if not isinstance(answer, str):
-            return
-
-        if len(answer) > 2000:
-            embed = discord.Embed(
-                description=answer,
-                thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png",
-            )
-            await message.reply(embed=embed)
-        else:
-            await message.reply(answer)
-
-    async def react(message: discord.Message) -> None:
-        if message.guild is None:
-            return
-        await message.add_reaction(random.choice(message.guild.emojis))
-
-    @bot.listen("on_message")
-    async def on_message(message: discord.Message) -> None:
-        if message.flags.ephemeral:
-            return
-
-        if message.author != bot.user and bot.user in message.mentions:
-            logger.info(f"{message.author} metion")
-            await random.choice((reply, react))(message)
-            return
-
-        probas = [10, 5, 10]
-
-        func = random.choices(
-            (reply, ai_reply, react, None), weights=probas + [100 - sum(probas)]
-        )[0]
-        if func is not None:
-            await func(message)
-
     @bot.listen("on_reaction_add")
     async def add_more_reaction(
         reaction: discord.Reaction, user: discord.Member | discord.User
@@ -329,18 +264,4 @@ def main() -> None:
         await ctx.respond(embed=embed)
         logger.info("FIN CHAN")
 
-    async def send_as_webhook(
-        channel: discord.TextChannel,
-        name: str,
-        avatar_url: str | None,
-        content: str,
-    ) -> None:
-        webhooks = await channel.webhooks()
-        webhook = discord.utils.get(webhooks, name="BotbotbotHook")
-
-        if webhook is None:
-            webhook = await channel.create_webhook(name="BotbotbotHook")
-
-        await webhook.send(content=content, username=name, avatar_url=avatar_url)
-
     bot.run(config.get("token"))
diff --git a/botbotbot/text.py b/botbotbot/text.py
new file mode 100644
index 0000000..a03161f
--- /dev/null
+++ b/botbotbot/text.py
@@ -0,0 +1,146 @@
+import logging
+import random
+from typing import Any, Callable, Coroutine
+
+import discord
+import emoji
+
+from botbotbot.ai import AIBot
+
+logger = logging.getLogger(__name__)
+
+
+class TextBot:
+    uni_emojis = tuple(k for k in emoji.EMOJI_DATA.keys() if len(k) == 1)
+
+    def __init__(
+        self,
+        bot: discord.Bot,
+        aibot: AIBot | None = None,
+        wordlist: list[str] = [],
+        rnd_weights: list[float] = [10, 5, 10],
+    ) -> None:
+        self.bot = bot
+        self.aibot = aibot
+        self.word_list = wordlist
+        self._rnd_weights = rnd_weights
+
+    def init_events(self) -> None:
+        self.bot.add_listener(self.on_message, "on_message")
+
+    @property
+    def rnd_weights(self) -> list[float]:
+        return self._rnd_weights + [100 - sum(self._rnd_weights)]
+
+    @property
+    def rnd_functions(
+        self,
+    ) -> list[Callable[[discord.Message], Coroutine[Any, Any, None]]]:
+        return [self.reply, self.ai_reply, self.react]
+
+    @property
+    def rnd_functions_or_not(
+        self,
+    ) -> list[Callable[[discord.Message], Coroutine[Any, Any, None]] | None]:
+        return [*self.rnd_functions, None]
+
+    async def on_message(self, message: discord.Message) -> None:
+        logger.debug(
+            f"Received message from <{message.author}> on channel <{message.channel}>."
+        )
+        if message.flags.ephemeral:
+            logger.debug("Ephemeral message, ignoring.")
+            return
+
+        if message.author != self.bot.user and self.bot.user in message.mentions:
+            logger.info(
+                f"Mention from <{message.author}> in channel <{message.channel}>."
+            )
+            await random.choices(self.rnd_functions, weights=self.rnd_weights[:-1])[0](
+                message
+            )
+            return
+
+        func = random.choices(self.rnd_functions_or_not, weights=self.rnd_weights)[0]
+
+        if func is None:
+            logger.debug("No action.")
+            return
+
+        await func(message)
+
+    async def reply(self, message: discord.Message) -> None:
+        logger.info(f"Replying to <{message.author}> in channel <{message.channel}>.")
+
+        mention = random.choices(
+            [f"<@{message.author.id}>", "@everyone", "@here"], weights=(97, 1, 2)
+        )[0]
+        content = random.choice(
+            (
+                f"{mention}, {random.choice(self.word_list)}",
+                f"{random.choice(self.word_list)}",
+            )
+        )
+
+        if (
+            isinstance(message.channel, discord.TextChannel)
+            and random.random() < 10 / 100
+        ):
+            await self.send_as_webhook(
+                message.channel,
+                message.author.display_name,
+                message.author.avatar.url if message.author.avatar else None,
+                content,
+            )
+        else:
+            fct = random.choice((message.reply, message.channel.send))
+
+            await fct(content)
+
+    async def ai_reply(self, message: discord.Message) -> None:
+        if self.aibot is None:
+            logger.debug("No AI bot, ignoring.")
+            return
+
+        logger.info(f"AI Reply to {message.author}")
+        prompt = message.clean_content
+        if prompt == "" and message.embeds and message.embeds[0].description:
+            prompt = message.embeds[0].description
+
+        answer = self.aibot.answer(prompt)
+        if not isinstance(answer, str):
+            logger.error(f"Got unexpected result from AIBot : {answer}")
+            return
+
+        if len(answer) > 2000:
+            logger.debug("Answer too long, sending as embed.")
+            embed = discord.Embed(
+                description=answer,
+                thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png",
+            )
+            await message.reply(embed=embed)
+        else:
+            await message.reply(answer)
+
+    async def react(self, message: discord.Message) -> None:
+        emojis: tuple[str | discord.Emoji, ...] = self.uni_emojis
+        if message.guild is not None and random.random() < 50 / 100:
+            emojis = message.guild.emojis
+
+        emo: str | discord.Emoji = random.choice(emojis)
+        await message.add_reaction(emo)
+
+    async def send_as_webhook(
+        self,
+        channel: discord.TextChannel,
+        name: str,
+        avatar_url: str | None,
+        content: str,
+    ) -> None:
+        webhooks = await channel.webhooks()
+        webhook = discord.utils.get(webhooks, name="BotbotbotHook")
+
+        if webhook is None:
+            webhook = await channel.create_webhook(name="BotbotbotHook")
+
+        await webhook.send(content=content, username=name, avatar_url=avatar_url)
diff --git a/pyproject.toml b/pyproject.toml
index 73588be..0d9c9bb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,6 +6,7 @@ readme = "README.md"
 requires-python = ">=3.13"
 dependencies = [
     "audioop-lts>=0.2.1",
+    "emoji>=2.14.1",
     "mistralai>=1.6.0",
     "py-cord>=2.6.1",
     "pynacl>=1.5.0",
@@ -21,6 +22,7 @@ dev = [
     "mypy>=1.15.0",
     "pre-commit>=4.2.0",
     "ruff>=0.11.2",
+    "types-requests>=2.32.0.20250306",
 ]
 
 [tool.mypy]
diff --git a/uv.lock b/uv.lock
index 8584e4e..accb65c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -133,6 +133,7 @@ version = "0.1.0"
 source = { virtual = "." }
 dependencies = [
     { name = "audioop-lts" },
+    { name = "emoji" },
     { name = "mistralai" },
     { name = "py-cord" },
     { name = "pynacl" },
@@ -145,11 +146,13 @@ dev = [
     { name = "mypy" },
     { name = "pre-commit" },
     { name = "ruff" },
+    { name = "types-requests" },
 ]
 
 [package.metadata]
 requires-dist = [
     { name = "audioop-lts", specifier = ">=0.2.1" },
+    { name = "emoji", specifier = ">=2.14.1" },
     { name = "mistralai", specifier = ">=1.6.0" },
     { name = "py-cord", specifier = ">=2.6.1" },
     { name = "pynacl", specifier = ">=1.5.0" },
@@ -162,6 +165,7 @@ dev = [
     { name = "mypy", specifier = ">=1.15.0" },
     { name = "pre-commit", specifier = ">=4.2.0" },
     { name = "ruff", specifier = ">=0.11.2" },
+    { name = "types-requests", specifier = ">=2.32.0.20250306" },
 ]
 
 [[package]]
@@ -235,6 +239,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
 ]
 
+[[package]]
+name = "emoji"
+version = "2.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617 },
+]
+
 [[package]]
 name = "eval-type-backport"
 version = "0.2.2"
@@ -666,6 +679,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
 ]
 
+[[package]]
+name = "types-requests"
+version = "2.32.0.20250306"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/1a/beaeff79ef9efd186566ba5f0d95b44ae21f6d31e9413bcfbef3489b6ae3/types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", size = 23012 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/99/26/645d89f56004aa0ba3b96fec27793e3c7e62b40982ee069e52568922b6db/types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b", size = 20673 },
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.12.2"