From 4721b685b44eb6ca67e6dd2c2183222c95861004 Mon Sep 17 00:00:00 2001 From: xory Date: Thu, 14 Aug 2025 19:27:21 +0000 Subject: [PATCH] initial code --- .env.example | 2 + .gitignore | 3 ++ main.py | 79 ++++++++++++++++++++++++++++ requirements.txt | 4 ++ tools.py | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 tools.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e093867 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +GEM_API_KEY="" +DSC_API_KEY="" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1eb6e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +.env \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..db10ec4 --- /dev/null +++ b/main.py @@ -0,0 +1,79 @@ +from google import genai +from google.genai import types +from dotenv import load_dotenv +from discord import app_commands +from discord.ext import commands +from tools import searxng, open_url +import os +import io +import discord + +load_dotenv() + +client = genai.Client(api_key=os.getenv("GEM_API_KEY")) +config = types.GenerateContentConfig( + tools=[searxng, open_url] +) +intents = discord.Intents.default() +intents.message_content = True +bot = commands.Bot(intents=intents, command_prefix="-") + + +@bot.tree.command(name="test", description="Test command") +@app_commands.allowed_installs(guilds=False, users=True) +@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) +async def test(interaction: discord.Interaction) -> None: + await interaction.response.send_message("hai :3") + + +@bot.tree.command(name="generation", description="generation") +@app_commands.allowed_installs(guilds=False, users=True) +@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) +async def generation(interaction: discord.Interaction) -> None: + await interaction.response.send_message("generation") + + +@bot.tree.command(name="ask", description="ai thing yes 👍") +@app_commands.allowed_installs(guilds=False, users=True) +@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) +async def ask(interaction: discord.Interaction, prompt: str) -> None: + await interaction.response.defer() + response = await client.aio.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=config + ) + if not response.text: + await interaction.edit_original_response(content="`[E] Model returned no response`") + generation: str = response.text or "" + if len(generation) > 2000: + buffer = io.BytesIO(generation.encode("utf-8")) + buffer.seek(0) + file = discord.File(buffer, filename="output.md") + await interaction.edit_original_response(content="Sent as a file", attachments=[file]) + else: + await interaction.edit_original_response(content=generation) + + +@bot.tree.command(name="sync", description="bot.tree.sync()") +@app_commands.allowed_installs(guilds=False, users=True) +@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) +async def sync(interaction: discord.Interaction) -> None: + if interaction.user.id == 1139850599085645844: + await interaction.response.send_message("`[I] Syncing...`") + await bot.tree.sync() + await interaction.edit_original_response(content="`[I] Syncing... OK`") + else: + await interaction.response.send_message("`[E] 403 Forbidden`") + + +@bot.event +async def on_ready(): + await bot.tree.sync() + print("Logged in!") + + +api_key: str | None = os.getenv("DSC_API_KEY") +if not api_key: + raise RuntimeError +bot.run(api_key) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ea1148 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +google-genai +python-dotenv +discord.py +markdownify diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..1208f98 --- /dev/null +++ b/tools.py @@ -0,0 +1,134 @@ +import aiohttp +from markdownify import markdownify + +SUPPORTED_TEXT_MIMETYPES = [ + "text/plain", + "text/html", + "text/css", + "text/csv", + "text/javascript", + "text/markdown", + "text/xml", + "text/yaml", + "text/rtf", + "text/x-python", + "text/x-c", + "text/x-java-source", + "text/x-lua", + "text/x-sh", + "text/x-sass", + "text/x-scss", + "application/javascript", + "application/json", + "application/xml", + "application/rtf", + "application/xhtml+xml", + "application/atom+xml", + "application/rss+xml", + "application/sql", + "application/ld+json", + "application/x-yaml", +] + + +async def searxng(query: str) -> list: + """ + Search the web with SearXNG. + + Arguments: + query (str): The search query + Returns: a list of the first 10 search results. + """ + params = { + "q": query, + "format": "json", + "engines": "google,duckduckgo,brave" + } + + # Use an aiohttp.ClientSession for making HTTP requests. + # The 'async with' ensures the session is properly closed when done. + async with aiohttp.ClientSession() as session: + try: + # Make an asynchronous GET request + # The 'async with' here ensures the response object is properly closed + async with session.get("https://searx.xorydev.xyz/search", params=params) as response: + # Raise an exception for bad status codes (4xx or 5xx) + response.raise_for_status() + + # Await the JSON parsing of the response body + data = await response.json() + except aiohttp.ClientError as e: + # Catch any aiohttp-related errors (network issues, invalid URL, etc.) + print(f"Error making request to SearXNG: {e}") + return [] # Return an empty list on error + except Exception as e: + # Catch any other unexpected errors + print(f"An unexpected error occurred: {e}") + return [] + + results = [] + # Safely get "results" array, defaulting to empty list if not present + for r in data.get("results", []): + title = r.get("title") + url = r.get("url") + + # Only append if both title and URL are present and we have less than 10 results + if title and url and len(results) < 10: + results.append({"title": title, "url": url}) + + return results + + +async def open_url(url: str) -> dict: + """ + Opens a URL and returns its full content (if it's HTML, it will be converted to clean Markdown). + Use this when a `search` result's content is insufficient or when a user provides a direct URL to analyze. + """ + + async with aiohttp.ClientSession( + headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-US,en;q=0.9", + "Sec-Ch-Ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "Priority": "u=0, i", + }, + ) as session: + async with session.get(url) as response: + response.raise_for_status() + content_type = response.content_type.split(";")[0].strip() + content_length = response.content_length or 0 + + if content_type not in SUPPORTED_TEXT_MIMETYPES: + return { + "content_type": content_type, + "content_length": content_length, + "content": None, + } + + if "text/html" in content_type: + content = markdownify(await response.text()) + if len(content) > 262144: + content = content[:262144] + return { + "content_type": content_type, + "content_length": content_length, + "content": content, + } + + content = await response.text() + if len(content) > 262144: + content = content[:262144] + return { + "content_type": content_type, + "content_length": content_length, + "content": content, + }