diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml
index b4ffe96..7dfaf5c 100644
--- a/.gitea/workflows/deploy.yaml
+++ b/.gitea/workflows/deploy.yaml
@@ -22,7 +22,7 @@ jobs:
docker stop garfbot
docker rm garfbot
docker build -t git.crate.zip/crate/garfbot:latest .
- docker run -d --restart always -v $PWD:/usr/src/app --name garfbot git.crate.zip/crate/garfbot:latest
+ docker compose up -d -p garfbot
else
docker restart garfbot
fi
diff --git a/.gitignore b/.gitignore
index 6c33acd..0b96242 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ garfpy/__pycache__/
*.old
*.log*
meows.py
+meow_counts.json
+user_stats.json
+responses.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 534855c..a658fbf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,29 @@
-FROM python:3.11.10-bookworm
-
+FROM python:3.11.10-alpine
WORKDIR /usr/src/app
-RUN apt update
-RUN apt install -y iputils-ping
-RUN apt install -y dnsutils
-RUN apt install -y nmap
-RUN apt install -y python3
-RUN apt install -y python3-pip
-RUN pip3 install discord
-RUN pip3 install openai
-RUN pip3 install aiohttp
-RUN pip3 install requests
-RUN pip3 install wikipedia
-RUN pip3 install pillow
-RUN pip3 install qrcode
+RUN apk update && \
+ apk add --no-cache \
+ iputils \
+ bind-tools \
+ nmap \
+ gcc \
+ musl-dev \
+ jpeg-dev \
+ zlib-dev \
+ freetype-dev \
+ lcms2-dev \
+ openjpeg-dev \
+ tiff-dev \
+ tk-dev \
+ tcl-dev
-CMD [ "python", "garfmain.py" ]
+RUN pip3 install --no-cache-dir \
+ discord \
+ openai \
+ aiohttp \
+ requests \
+ wikipedia \
+ pillow \
+ qrcode
+
+CMD [ "python", "garfmain.py" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index 7ac0ede..d750de1 100644
--- a/README.md
+++ b/README.md
@@ -2,25 +2,44 @@ Who is GarfBot?
======

-GarfBot is a discord bot that uses OpenAI's generative pre-trained models to produce text and images for your personal entertainment and companionship. There are a few ways you can interact with him on discord, either in a public server or by direct message:
+GarfBot is a discord bot that uses OpenAI's generative pre-trained models to produce text and images for your personal entertainment and companionship.
+
There are a few ways you can interact with him on discord, either in a public server or by direct message:
`hey garfield {prompt}`
-Responds with text.
+
Responds with text.
`garfpic {prompt}`
-Responds with an image.
+
Responds with an image.
`garfping {target}`
-Responds with iputils-ping result from target.
+
Responds with iputils-ping result from target.
-`garfpic {target}`
-Responds with dns lookup result from target.
+`garfdns {target}`
+
Responds with dns lookup result from target.
`garfhack {target}`
-Responds with nmap scan result from target.
+
Responds with nmap scan result from target.
`garfshop {item} {zip}`
-Responds with 10 grocery {item}s from the nearest Kroger location, listed from least to most expensive.
+
Responds with 10 grocery {item}s from the nearest Kroger location, listed from least to most expensive.
+
+`garfwiki {query}`
+
Garfbot looks up a wikipedia article and will summarize it for you.
+
+`garfqr {text}`
+
Create a QR code for any string up to 1000 characters.
+
+`garfbot response {add} {trigger} {response}`
+
Add a GarfBot auto response for your server. Use "quotes" if you like.
+
+`garfbot response {remove} {trigger}`
+
Remove a GarfBot auto response for your server.
+
+`garfbot response {list}`
+
List current GarfBot auto responses for your server.
+
+`garfbot help`
+
Show a list of these commands.
Installation
======
@@ -38,7 +57,8 @@ GARFBOT_TOKEN = "Discord API token"
OPENAI_TOKEN = "OpenAI API token"
```
-I recommend building a docker image using the included DockerFile as a template. Run the container binding /usr/src/app to GarfBot's CWD:
+I recommend building a docker image using the included DockerFile as a template.
+
Run the container binding /usr/src/app to GarfBot's CWD:
```console
$ docker build -t garfbot .
@@ -53,7 +73,7 @@ If you prefer to install dependencies on you own host and run as a systemd servi
$ sudo nano /etc/systemd/system/garfbot.service
```
-Replace {user} with your username:
+Replace $USER with your username:
```console
[Unit]
@@ -63,8 +83,8 @@ After=multi-user.target
[Service]
Type=simple
Restart=always
-User={user}
-WorkingDirectory=/home/{user}/garfbot
+User=$USER
+WorkingDirectory=/home/$USER/garfbot
ExecStart=/usr/bin/python garfbot.py
[Install]
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4066fb3
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,7 @@
+services:
+ garfbot:
+ image: git.crate.zip/crate/garfbot:latest
+ container_name: garfbot
+ restart: always
+ volumes:
+ - /home/crate/garfbot:/usr/src/app
diff --git a/garfmain.py b/garfmain.py
index 951ced6..ed8d9fb 100644
--- a/garfmain.py
+++ b/garfmain.py
@@ -1,13 +1,16 @@
import config
import asyncio
import discord
-import subprocess
-from garfpy import(
- logger, is_private,
- kroger_token, find_store, search_product,
- garfpic, process_image_requests, generate_chat,
- aod_message, wikisum, generate_qr)
+from garfpy import (
+ logger,
+ IPUtils,
+ aod_message,
+ generate_qr,
+ Kroger,
+ GarfAI,
+ GarfbotRespond,
+)
gapikey = config.GIF_TOKEN
@@ -21,12 +24,20 @@ intents.messages = True
intents.message_content = True
garfbot = discord.Client(intents=intents)
+garf_respond = GarfbotRespond()
+garfield = GarfAI()
+iputils = IPUtils()
+kroger = Kroger()
+
@garfbot.event
async def on_ready():
try:
- asyncio.create_task(process_image_requests())
- logger.info(f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}.")
+ garf_respond.load_responses()
+ asyncio.create_task(garfield.process_image_requests())
+ logger.info(
+ f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}."
+ )
except Exception as e:
logger.error(e)
@@ -36,31 +47,27 @@ async def on_message(message):
if message.author == garfbot.user:
return
- if message.content.lower().startswith("hey garfield") or isinstance(message.channel, discord.DMChannel):
- user = message.author.name
- server = message.guild.name if message.guild else "Direct Message"
- question = message.content[12:] if message.content.lower().startswith("hey garfield") else message.content
- answer = await generate_chat(question)
- logger.info(f"Chat Request - User: {user}, Server: {server}, Prompt: {question}")
- await message.channel.send(answer)
+ content = message.content.strip()
+ lower = content.lower()
+ user_name = message.author.name
+ guild_id = message.guild.id
+ guild_name = message.guild.name if message.guild else "Direct Message"
- if message.content.lower().startswith('garfpic '):
- user = message.author.name
- server = message.guild.name if message.guild else "Direct Message"
- prompt = message.content[8:]
- logger.info(f"Image Request - User: {user}, Server: {server}, Prompt: {prompt}")
- await message.channel.send(f"`Please wait... image generation queued: {prompt}`")
- await garfpic(message, prompt)
+ # IP utils
+ if message.guild and lower.startswith(("garfping ", "garfdns ", "garfhack ")):
+ await iputils.scan(message, user_name, guild_name, lower)
- if message.content.lower().startswith('garfwiki '):
- search_term = message.content[9:]
- summary = await wikisum(search_term)
+ # Wikipedia
+ if lower.startswith("garfwiki "):
+ query = message.content[9:]
+ summary = await garfield.wikisum(query)
await message.channel.send(summary)
- if message.content.lower().startswith('garfqr '):
+ # QR codes
+ if lower.startswith("garfqr "):
text = message.content[7:]
if len(text) > 1000:
- await mesage.channel.send("❌ Text too long! Maximum 1000 characters.")
+ await message.channel.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
@@ -70,91 +77,123 @@ async def on_message(message):
logger.error(e)
await message.channel.send(e)
- if message.content.lower().startswith("garfping "):
- try:
- query = message.content.split()
- user = message.author.name
- server = message.guild.name if message.guild else "Direct Message"
- target = query[-1]
- logger.info(f"Ping Request - User: {user}, Server: {server}, Target: {target}")
- if is_private(target):
- rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
- await message.channel.send(rejection)
- else:
- result = subprocess.run(['ping', '-c', '4', target], capture_output=True, text=True)
- await message.channel.send(f"`Ping result for {target}:`\n```\n{result.stdout}\n```")
- except Exception as e:
- await message.channel.send(f"`GarfBot Error: {str(e)}`")
-
- if message.content.lower().startswith("garfdns "):
- try:
- query = message.content.split()
- user = message.author.name
- server = message.guild.name if message.guild else "Direct Message"
- target = query[-1]
- logger.info(f"NSLookup Request - User: {user}, Server: {server}, Target: {target}")
- if is_private(target):
- rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
- await message.channel.send(rejection)
- else:
- result = subprocess.run(['nslookup', target], capture_output=True, text=True)
- await message.channel.send(f"`NSLookup result for {target}:`\n```\n{result.stdout}\n```")
- except Exception as e:
- await message.channel.send(f"`GarfBot Error: {str(e)}`")
-
- if message.content.lower().startswith("garfhack "):
- try:
- query = message.content.split()
- user = message.author.name
- server = message.guild.name if message.guild else "Direct Message"
- target = query[-1]
- logger.info(f"Nmap Request - User: {user}, Server: {server}, Target: {target}")
- if is_private(target):
- rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
- await message.channel.send(rejection)
- else:
- await message.channel.send(f"`Scanning {target}...`")
- result = subprocess.run(['nmap', '-Pn', '-O', '-v', target], capture_output=True, text=True)
- await message.channel.send(f"`Ping result for {target}:`\n```\n{result.stdout}\n```")
- except Exception as e:
- await message.channel.send(f"`GarfBot Error: {str(e)}`")
-
# Kroger Shopping
- if message.content.lower().startswith("garfshop "):
+ if lower.startswith("garfshop "):
try:
- kroken = kroger_token()
- kroger_query = message.content.split()
- product = " ".join(kroger_query[1:-1])
- zipcode = kroger_query[-1]
- loc_data = find_store(zipcode, kroken)
- loc_id = loc_data['data'][0]['locationId']
- store_name = loc_data['data'][0]['name']
- product_query = search_product(product, loc_id, kroken)
- products = product_query['data']
- sorted_products = sorted(products, key=lambda item: item['items'][0]['price']['regular'])
- response = f"Prices for `{product}` at `{store_name}` near `{zipcode}`:\n"
- for item in sorted_products:
- product_name = item['description']
- price = item['items'][0]['price']['regular']
- response += f"- `${price}`: {product_name} \n"
+ query = message.content[9:]
+ response = kroger.garfshop(query)
await message.channel.send(response)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
- # Army of Dawn Server only!!
- if message.guild and message.guild.id == 719605634772893757:
+ # Chats & pics
+ elif lower.startswith("hey garfield") or isinstance(
+ message.channel, discord.DMChannel
+ ):
+ prompt = content[12:] if lower.startswith("hey garfield") else message.content
+ answer = await garfield.generate_chat(prompt)
+ logger.info(
+ f"Chat Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}"
+ )
+ await message.channel.send(answer)
+ elif lower.startswith("garfpic "):
+ prompt = content[8:]
+ logger.info(
+ f"Image Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}"
+ )
+ await message.channel.send(
+ f"`Please wait... image generation queued: {prompt}`"
+ )
+ await garfield.garfpic(message, prompt)
+
+ # GarfBot help
+ elif lower.strip() == "garfbot help":
+ embed = discord.Embed(title="**Need help?**", color=0x4D4D4D)
+ embed.add_field(
+ name="hey garfield `prompt`", value="*Responds with text.*", inline=True
+ )
+ embed.add_field(
+ name="garfpic `prompt`", value="*Responds with an image.*", inline=True
+ )
+ embed.add_field(
+ name="garfping `target`",
+ value="*Responds with iputils-ping result from target.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfdns `target`",
+ value="*Responds with dns lookup result from target.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfhack `target`",
+ value="*Responds with nmap scan result from target.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfwiki `query`",
+ value="*Garfbot looks up a wikipedia article and will summarize it for you.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfshop `item` `zip`",
+ value="*Responds with 10 grocery items from the nearest Kroger location, cheapest first.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfqr `text`",
+ value="*Create a QR code for any string up to 1000 characters.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfbot response `add` `trigger` `response`",
+ value='*Add a GarfBot auto response for your server. Use "quotes" if you like.*',
+ inline=True,
+ )
+ embed.add_field(
+ name="garfbot response `remove` `trigger`",
+ value="*Remove a GarfBot auto response for your server.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfbot response `list`",
+ value="*List current GarfBot auto responses for your server.*",
+ inline=True,
+ )
+ embed.add_field(
+ name="garfbot help", value="*Show a list of these commands.*", inline=True
+ )
+ await message.channel.send(embed=embed)
+
+ # Army of Dawn Server only!!
+ elif message.guild and message.guild.id == 719605634772893757:
await aod_message(garfbot, message)
+ # Auto-responses
+ elif message.guild:
+ responses = garf_respond.get_responses(guild_id)
+ if lower.startswith("garfbot response "):
+ await garf_respond.garfbot_response(message, content)
+ return
+
+ for trigger, response in responses.items():
+ if trigger.lower() in lower:
+ await message.channel.send(response)
+ break
+
+
+# Run GarfBot
async def garfbot_connect():
while True:
try:
await garfbot.start(garfkey)
except Exception as e:
- e = str(e)
- logger.error(f"Garfbot couldn't connect! {e}")
- await asyncio.sleep(60)
+ e = str(e)
+ logger.error(f"Garfbot couldn't connect! {e}")
+ await asyncio.sleep(60)
+
if __name__ == "__main__":
asyncio.run(garfbot_connect())
diff --git a/garfpy/__init__.py b/garfpy/__init__.py
index e447804..a11fd0b 100644
--- a/garfpy/__init__.py
+++ b/garfpy/__init__.py
@@ -1,14 +1,10 @@
# garfpy/__init__.py
+
from .log import logger
-from .kroger import(
- kroger_token, find_store, search_product
-)
-from .garfai import(
- garfpic,
- process_image_requests,
- generate_chat
-)
-from .iputils import is_private
+from .kroger import Kroger
+from .kroger import Kroger
+from .garfai import GarfAI
+from .respond import GarfbotRespond
from .aod import aod_message
-from .wiki import wikisum
-from .qr import generate_qr
\ No newline at end of file
+from .qr import generate_qr
+from .iputils import IPUtils
diff --git a/garfpy/aod.py b/garfpy/aod.py
index 0013d55..e13b651 100644
--- a/garfpy/aod.py
+++ b/garfpy/aod.py
@@ -11,6 +11,7 @@ from collections import defaultdict
meows_file = "meow_counts.json"
stats_file = "user_stats.json"
+
def json_load(file_path, default):
if os.path.isfile(file_path):
with open(file_path, "r") as f:
@@ -18,77 +19,105 @@ def json_load(file_path, default):
else:
return default
+
meow_counts = defaultdict(int, json_load(meows_file, {}))
user_stats = json_load(stats_file, {})
+
async def aod_message(garfbot, message):
- if "meow" in message.content.lower():
- logger.info(f"Meow detected! {message.author.name} said: {message.content}")
+ if "meow" in message.content.lower():
+ logger.info(f"Meow detected! {message.author.name} said: {message.content}")
- meow_counts[str(message.author.id)] += 1
+ meow_counts[str(message.author.id)] += 1
- with open(meows_file, "w") as f:
- json.dump(dict(meow_counts), f)
+ with open(meows_file, "w") as f:
+ json.dump(dict(meow_counts), f)
- if message.content.lower() == "meowcount":
- response = f"My records show that <@{message.author.id}> has meowed {meow_counts[str(message.author.id)]} time(s). Have a nice day."
- await message.channel.send(response)
+ if message.content.lower() == "meowcount":
+ response = f"My records show that <@{message.author.id}> has meowed {meow_counts[str(message.author.id)]} time(s). Have a nice day."
+ await message.channel.send(response)
- if message.content.lower() == "top meowers":
- top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[:10]
- embed = discord.Embed(title="Top Meowers :cat:", color=0x000000)
- for i, (user_id, meow_count) in enumerate(top_meowers):
- user = await garfbot.fetch_user(int(user_id))
- embed.add_field(name=f"{i+1}. {user.name}", value=f"{meow_count} meows", inline=False)
- await message.channel.send(embed=embed)
+ if message.content.lower() == "top meowers":
+ top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[
+ :10
+ ]
+ embed = discord.Embed(title="Top Meowers :cat:", color=0x000000)
+ for i, (user_id, meow_count) in enumerate(top_meowers):
+ user = await garfbot.fetch_user(int(user_id))
+ embed.add_field(
+ name=f"{i + 1}. {user.name}",
+ value=f"{meow_count} meows",
+ inline=False,
+ )
+ await message.channel.send(embed=embed)
- if message.content.lower() == "checking in":
- user_id = str(message.author.id)
- if user_id in user_stats and user_stats[user_id]["check_in_time"] is not None:
- await message.channel.send(f"{message.author.mention} You are already checked in. Please check out first.")
- return
+ if message.content.lower() == "checking in":
+ user_id = str(message.author.id)
+ if user_id in user_stats and user_stats[user_id]["check_in_time"] is not None:
+ await message.channel.send(
+ f"{message.author.mention} You are already checked in. Please check out first."
+ )
+ return
- check_in_time = datetime.now().timestamp()
- if user_id not in user_stats:
- user_stats[user_id] = {"check_ins": 0, "total_time": 0, "check_in_time": None}
- user_stats[user_id]["check_in_time"] = check_in_time
- await message.channel.send(f"{message.author.mention} You have been checked in. Please mute your microphone.")
+ check_in_time = datetime.now().timestamp()
+ if user_id not in user_stats:
+ user_stats[user_id] = {
+ "check_ins": 0,
+ "total_time": 0,
+ "check_in_time": None,
+ }
+ user_stats[user_id]["check_in_time"] = check_in_time
+ await message.channel.send(
+ f"{message.author.mention} You have been checked in. Please mute your microphone."
+ )
- elif message.content.lower() == "checking out":
- user_id = str(message.author.id)
- if user_id not in user_stats or user_stats[user_id]["check_in_time"] is None:
- await message.channel.send(f"{message.author.mention} You have not checked in yet. Please check in first.")
- return
+ elif message.content.lower() == "checking out":
+ user_id = str(message.author.id)
+ if user_id not in user_stats or user_stats[user_id]["check_in_time"] is None:
+ await message.channel.send(
+ f"{message.author.mention} You have not checked in yet. Please check in first."
+ )
+ return
- check_out_time = datetime.now().timestamp()
- check_in_time = user_stats[user_id]["check_in_time"]
- time_delta = check_out_time - check_in_time
- user_stats[user_id]["check_ins"] += 1
- user_stats[user_id]["total_time"] += time_delta
- user_stats[user_id]["check_in_time"] = None
+ check_out_time = datetime.now().timestamp()
+ check_in_time = user_stats[user_id]["check_in_time"]
+ time_delta = check_out_time - check_in_time
+ user_stats[user_id]["check_ins"] += 1
+ user_stats[user_id]["total_time"] += time_delta
+ user_stats[user_id]["check_in_time"] = None
- with open("user_stats.json", "w") as f:
- json.dump(user_stats, f)
- await message.channel.send(f"{message.author.mention} You have been checked out. Your session was {time_delta:.2f} seconds.")
+ with open("user_stats.json", "w") as f:
+ json.dump(user_stats, f)
+ await message.channel.send(
+ f"{message.author.mention} You have been checked out. Your session was {time_delta:.2f} seconds."
+ )
- elif message.content.lower() == "stats":
- stats_embed = discord.Embed(title="User stats :trophy:", color=0x000000)
- sorted_user_stats = sorted(user_stats.items(), key=lambda x: x[1]["total_time"], reverse=True)
- table_rows = [["Name", "Check-ins", "Total Time"]]
- for user_id, stats in sorted_user_stats:
- if stats["check_in_time"] is None:
- total_time_seconds = stats["total_time"]
- hours, total_time_seconds = divmod(total_time_seconds, 3600)
- minutes, total_time_seconds = divmod(total_time_seconds, 60)
- seconds, fractions = divmod(total_time_seconds, 1)
- fractions_str = f"{fractions:.3f}"[2:]
- username = garfbot.get_user(int(user_id)).name
- table_rows.append([username, str(stats["check_ins"]), f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms"])
- else:
- username = garfbot.get_user(int(user_id)).name
- table_rows.append([username, "Currently checked in", "-"])
- table_columns = list(zip(*table_rows[1:]))
- table_fields = table_rows[0]
- for field, values in zip(table_fields, table_columns):
- stats_embed.add_field(name=field, value="\n".join(values), inline=True)
- await message.channel.send(embed=stats_embed)
+ elif message.content.lower() == "stats":
+ stats_embed = discord.Embed(title="User stats :trophy:", color=0x000000)
+ sorted_user_stats = sorted(
+ user_stats.items(), key=lambda x: x[1]["total_time"], reverse=True
+ )
+ table_rows = [["Name", "Check-ins", "Total Time"]]
+ for user_id, stats in sorted_user_stats:
+ if stats["check_in_time"] is None:
+ total_time_seconds = stats["total_time"]
+ hours, total_time_seconds = divmod(total_time_seconds, 3600)
+ minutes, total_time_seconds = divmod(total_time_seconds, 60)
+ seconds, fractions = divmod(total_time_seconds, 1)
+ fractions_str = f"{fractions:.3f}"[2:]
+ username = garfbot.get_user(int(user_id)).name
+ table_rows.append(
+ [
+ username,
+ str(stats["check_ins"]),
+ f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms",
+ ]
+ )
+ else:
+ username = garfbot.get_user(int(user_id)).name
+ table_rows.append([username, "Currently checked in", "-"])
+ table_columns = list(zip(*table_rows[1:]))
+ table_fields = table_rows[0]
+ for field, values in zip(table_fields, table_columns):
+ stats_embed.add_field(name=field, value="\n".join(values), inline=True)
+ await message.channel.send(embed=stats_embed)
diff --git a/garfpy/garfai.py b/garfpy/garfai.py
index 35e6f90..2e3c883 100644
--- a/garfpy/garfai.py
+++ b/garfpy/garfai.py
@@ -4,86 +4,97 @@ import config
import aiohttp
import asyncio
import discord
+import wikipedia
from openai import AsyncOpenAI
from garfpy import logger
-openaikey = config.OPENAI_TOKEN
-txtmodel = config.TXT_MODEL
-imgmodel = config.IMG_MODEL
+class GarfAI:
+ def __init__(self):
+ self.openaikey = config.OPENAI_TOKEN
+ self.txtmodel = config.TXT_MODEL
+ self.imgmodel = config.IMG_MODEL
+ self.image_request_queue = asyncio.Queue()
-image_request_queue = asyncio.Queue()
+ async def garfpic(self, message, prompt):
+ await self.image_request_queue.put({"message": message, "prompt": prompt})
-async def garfpic(message, prompt):
- await image_request_queue.put({'message': message, 'prompt': prompt})
+ async def generate_image(self, prompt):
+ try:
+ client = AsyncOpenAI(api_key=self.openaikey)
+ response = await client.images.generate(
+ model=self.imgmodel, prompt=prompt, n=1, size="1024x1024"
+ )
+ image_url = response.data[0].url
+ return image_url
+ except openai.BadRequestError as e:
+ return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
+ except openai.InternalServerError as e:
+ logger.error(e)
+ return f"`GarfBot Error: ({e.status_code}) - Monday`"
+ except Exception as e:
+ logger.error(e)
+ return "`GarfBot Error: Lasagna`"
-async def generate_image(prompt):
- try:
- client = AsyncOpenAI(api_key = openaikey)
- response = await client.images.generate(
- model=imgmodel,
- prompt=prompt,
- n=1,
- size="1024x1024"
- )
- image_url = response.data[0].url
- return image_url
- except openai.BadRequestError as e:
- return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
- except openai.InternalServerError as e:
- logger.error(e)
- return f"`GarfBot Error: ({e.status_code}) - Monday`"
- except Exception as e:
- logger.error(e)
- return f"`GarfBot Error: Lasagna`"
+ async def process_image_requests(self):
+ async with aiohttp.ClientSession() as session:
+ while True:
+ request = await self.image_request_queue.get()
+ message = request["message"]
+ prompt = request["prompt"]
+ image_url = await self.generate_image(prompt)
+ if "GarfBot Error" not in image_url:
+ logger.info("Downloading & sending image...")
+ async with session.get(image_url) as resp:
+ if resp.status == 200:
+ image_data = await resp.read()
+ ram_image = io.BytesIO(image_data)
+ ram_image.seek(0)
+ timestamp = message.created_at.strftime("%Y%m%d%H%M%S")
+ filename = f"{timestamp}_generated_image.png"
+ sendfile = discord.File(fp=ram_image, filename=filename)
+ try:
+ await message.channel.send(file=sendfile)
+ except Exception as e:
+ logger.error(e)
+ else:
+ await message.channel.send("`GarfBot Error: Odie`")
+ else:
+ await message.channel.send(image_url)
+ self.image_request_queue.task_done()
+ await asyncio.sleep(2)
-async def process_image_requests():
- async with aiohttp.ClientSession() as session:
- while True:
- request = await image_request_queue.get()
- message = request['message']
- prompt = request['prompt']
- image_url = await generate_image(prompt)
- if "GarfBot Error" not in image_url:
- logger.info("Downloading & sending image...")
- async with session.get(image_url) as resp:
- if resp.status == 200:
- image_data = await resp.read()
- ram_image = io.BytesIO(image_data)
- ram_image.seek(0)
- timestamp = message.created_at.strftime('%Y%m%d%H%M%S')
- filename = f"{timestamp}_generated_image.png"
- sendfile = discord.File(fp=ram_image, filename=filename)
- try:
- await message.channel.send(file=sendfile)
- except Exception as e:
- logger.error(e)
- else:
- await message.channel.send("`GarfBot Error: Odie`")
- else:
- await message.channel.send(image_url)
- image_request_queue.task_done()
- await asyncio.sleep(2)
+ async def generate_chat(self, question):
+ try:
+ client = AsyncOpenAI(api_key=self.openaikey)
+ response = await client.chat.completions.create(
+ model=self.txtmodel,
+ messages=[
+ {
+ "role": "system",
+ "content": "Pretend you are sarcastic Garfield.",
+ },
+ {"role": "user", "content": f"{question}"},
+ ],
+ max_tokens=400,
+ )
+ answer = response.choices[0].message.content
+ return answer.replace("an AI language model", "a cartoon animal")
+ except openai.BadRequestError as e:
+ return f"`GarfBot Error: {e}`"
+ except openai.APIError as e:
+ logger.info(e, flush=True)
+ return "`GarfBot Error: Monday`"
+ except Exception as e:
+ logger.info(e, flush=True)
+ return "`GarfBot Error: Lasagna`"
-# GarfChats
-async def generate_chat(question):
- try:
- client = AsyncOpenAI(api_key = openaikey)
- response = await client.chat.completions.create(
- model=txtmodel,
- messages=[
- {"role": "system", "content": "Pretend you are sarcastic Garfield."},
- {"role": "user", "content": f"{question}"}
- ],
- max_tokens=400
- )
- answer = response.choices[0].message.content
- return answer.replace("an AI language model", "a cartoon animal")
- except openai.BadRequestError as e:
- return f"`GarfBot Error: {e}`"
- except openai.APIError as e:
- logger.info(e, flush=True)
- return f"`GarfBot Error: Monday`"
- except Exception as e:
- logger.info(e, flush=True)
- return f"`GarfBot Error: Lasagna`"
+ async def wikisum(self, query):
+ try:
+ summary = wikipedia.summary(query)
+ garfsum = await self.generate_chat(
+ f"Please summarize in your own words: {summary}"
+ )
+ return garfsum
+ except Exception as e:
+ return e
diff --git a/garfpy/iputils.py b/garfpy/iputils.py
index 4f7d1d5..6a301d4 100644
--- a/garfpy/iputils.py
+++ b/garfpy/iputils.py
@@ -1,19 +1,85 @@
+import discord
import ipaddress
+import subprocess
+from garfpy import logger
-def is_private(target):
- try:
- ip_obj = ipaddress.ip_address(target)
- if ip_obj.is_private:
- return True
- except ValueError:
- if "crate.lan" in target.lower():
- return True
- if "crate.zip" in target.lower():
- return True
- if "memtec.org" in target.lower():
- return True
- if "crateit.net" in target.lower():
- return True
- if "garfbot.art" in target.lower():
- return True
- return False
+
+class IPUtils:
+ def is_private(self, target):
+ try:
+ ip_obj = ipaddress.ip_address(target)
+ if ip_obj.is_private:
+ return True
+ except ValueError:
+ if "crate.lan" in target.lower():
+ return True
+ if "crate.zip" in target.lower():
+ return True
+ if "memtec.org" in target.lower():
+ return True
+ if "crateit.net" in target.lower():
+ return True
+ if "garfbot.art" in target.lower():
+ return True
+ return False
+
+ async def scan(self, message, user, guild, query):
+ split = query.split()
+ target = split[-1]
+ if self.is_private(target):
+ return
+
+ if query.startswith("garfping "):
+ try:
+ logger.info(
+ f"Ping Request - User: {user}, Server: {guild}, Target: {target}"
+ )
+ await message.channel.send(f"`Pinging {target}...`")
+ result = subprocess.run(
+ ["ping", "-c", "4", target], capture_output=True, text=True
+ )
+ embed = discord.Embed(
+ title=f"Ping result: {target}",
+ color=0x4D4D4D,
+ description=f"```{result.stdout}```",
+ )
+ await message.channel.send(embed=embed)
+ except Exception as e:
+ await message.channel.send(f"`GarfBot Error: {str(e)}`")
+
+ if query.startswith("garfdns "):
+ try:
+ logger.info(
+ f"NSLookup Request - User: {user}, Server: {guild}, Target: {target}"
+ )
+ await message.channel.send(f"`Requesting {target}...`")
+ result = subprocess.run(
+ ["nslookup", target], capture_output=True, text=True
+ )
+ embed = discord.Embed(
+ title=f"NSLookup result: {target}",
+ color=0x4D4D4D,
+ description=f"```{result.stdout}```",
+ )
+ await message.channel.send(embed=embed)
+ except Exception as e:
+ await message.channel.send(f"`GarfBot Error: {str(e)}`")
+
+ if query.startswith("garfhack "):
+ try:
+ logger.info(
+ f"Nmap Request - User: {user}, Server: {guild}, Target: {target}"
+ )
+ await message.channel.send(f"`Scanning {target}...`")
+ result = subprocess.run(
+ ["nmap", "-Pn", "-O", "-v", target], capture_output=True, text=True
+ )
+ embed = discord.Embed(
+ title=f"Nmap scan result: {target}",
+ color=0x4D4D4D,
+ description=f"```{result.stdout}```",
+ )
+ embed.set_footer(text="https://nmap.org/")
+ await message.channel.send(embed=embed)
+ except Exception as e:
+ await message.channel.send(f"`GarfBot Error: {str(e)}`")
diff --git a/garfpy/kroger.py b/garfpy/kroger.py
index 3d50aef..66b7d8f 100644
--- a/garfpy/kroger.py
+++ b/garfpy/kroger.py
@@ -4,45 +4,76 @@ from base64 import b64encode
from garfpy import logger
-client_id = config.CLIENT_ID
-client_secret = config.CLIENT_SECRET
+class Kroger:
+ def __init__(self):
+ self.client_id = config.CLIENT_ID
+ self.client_secret = config.CLIENT_SECRET
+ self.auth = b64encode(
+ f"{self.client_id}:{self.client_secret}".encode()
+ ).decode()
-auth = b64encode(f"{client_id}:{client_secret}".encode()).decode()
+ def kroger_token(self):
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": f"Basic {self.auth}",
+ }
-def kroger_token():
- headers = {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Authorization': f'Basic {auth}'
- }
+ response = requests.post(
+ "https://api.kroger.com/v1/connect/oauth2/token",
+ headers=headers,
+ data={"grant_type": "client_credentials", "scope": "product.compact"},
+ )
- response = requests.post('https://api.kroger.com/v1/connect/oauth2/token', headers=headers, data={
- 'grant_type': 'client_credentials',
- 'scope': 'product.compact'
- })
+ response.raise_for_status()
+ return response.json()["access_token"]
- response.raise_for_status()
- return response.json()['access_token']
+ def find_store(self, zipcode, kroken):
+ headers = {
+ "Authorization": f"Bearer {kroken}",
+ }
+ params = {
+ "filter.zipCode.near": zipcode,
+ "filter.limit": 1,
+ }
+ response = requests.get(
+ "https://api.kroger.com/v1/locations", headers=headers, params=params
+ )
+ return response.json()
-def find_store(zipcode, kroken):
- headers = {
- 'Authorization': f'Bearer {kroken}',
- }
- params = {
- 'filter.zipCode.near': zipcode,
- 'filter.limit': 1,
- }
- response = requests.get('https://api.kroger.com/v1/locations', headers=headers, params=params)
- return response.json()
+ def search_product(self, product, loc_id, kroken):
+ logger.info(f"Searching for {product}...")
+ headers = {
+ "Authorization": f"Bearer {kroken}",
+ }
+ params = {
+ "filter.term": product,
+ "filter.locationId": loc_id,
+ "filter.limit": 10,
+ }
+ response = requests.get(
+ "https://api.kroger.com/v1/products", headers=headers, params=params
+ )
+ return response.json()
-def search_product(product, loc_id, kroken):
- logger.info(f"Searching for {product}...")
- headers = {
- 'Authorization': f'Bearer {kroken}',
- }
- params = {
- 'filter.term': product,
- 'filter.locationId': loc_id,
- 'filter.limit': 10
- }
- response = requests.get('https://api.kroger.com/v1/products', headers=headers, params=params)
- return response.json()
+ def garfshop(self, query):
+ try:
+ query = query.split()
+ kroken = self.kroger_token()
+ product = query[-2]
+ zipcode = query[-1]
+ loc_data = self.find_store(zipcode, kroken)
+ loc_id = loc_data["data"][0]["locationId"]
+ store_name = loc_data["data"][0]["name"]
+ product_query = self.search_product(product, loc_id, kroken)
+ products = product_query["data"]
+ sorted_products = sorted(
+ products, key=lambda item: item["items"][0]["price"]["regular"]
+ )
+ response = f"Prices for `{product}` at `{store_name}` near `{zipcode}`:\n"
+ for item in sorted_products:
+ product_name = item["description"]
+ price = item["items"][0]["price"]["regular"]
+ response += f"- `${price}`: {product_name} \n"
+ return response
+ except Exception as e:
+ return e
diff --git a/garfpy/log.py b/garfpy/log.py
index 3c17d94..de01b30 100644
--- a/garfpy/log.py
+++ b/garfpy/log.py
@@ -1,19 +1,18 @@
import logging
from logging.handlers import TimedRotatingFileHandler
-logger = logging.getLogger('garflog')
+logger = logging.getLogger("garflog")
logger.setLevel(logging.INFO)
-formatter=logging.Formatter(
- '%(asctime)s [%(levelname)s] %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
- )
+formatter = logging.Formatter(
+ "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
+)
file_handler = TimedRotatingFileHandler(
- 'garfbot.log',
- when='midnight',
+ "garfbot.log",
+ when="midnight",
interval=1,
backupCount=7,
- delay=True # Counter-intuitively, this will flush output immediately
- )
+ delay=True, # Counter-intuitively, this will flush output immediately
+)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
diff --git a/garfpy/qr.py b/garfpy/qr.py
index ceb3877..0ac4b48 100644
--- a/garfpy/qr.py
+++ b/garfpy/qr.py
@@ -4,7 +4,7 @@ from io import BytesIO
def calculate_qr_settings(text):
text_length = len(text)
-
+
if text_length <= 25:
version = 1
box_size = 12
@@ -38,26 +38,27 @@ def calculate_qr_settings(text):
else:
version = None
box_size = 3
-
+
return version, box_size
+
async def generate_qr(text):
version, box_size = calculate_qr_settings(text)
-
+
qr = qrcode.QRCode(
version=version,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=box_size,
border=4,
)
-
+
qr.add_data(text)
qr.make(fit=True)
-
+
qr_image = qr.make_image(fill_color="black", back_color="white")
-
+
img_buffer = BytesIO()
- qr_image.save(img_buffer, format='PNG')
+ qr_image.save(img_buffer, format="PNG")
img_buffer.seek(0)
- return img_buffer
\ No newline at end of file
+ return img_buffer
diff --git a/garfpy/respond.py b/garfpy/respond.py
new file mode 100644
index 0000000..d2c6293
--- /dev/null
+++ b/garfpy/respond.py
@@ -0,0 +1,165 @@
+from garfpy import logger
+import discord
+import json
+import os
+import re
+
+
+class GarfbotRespond:
+ def __init__(self):
+ self.garfbot_responses = {}
+ self.responses_file = "responses.json"
+
+ def load_responses(self):
+ if os.path.exists(self.responses_file):
+ try:
+ with open(self.responses_file, "r", encoding="utf-8") as f:
+ self.garfbot_responses = json.load(f)
+ self.garfbot_responses = {
+ int(k): v for k, v in self.garfbot_responses.items()
+ }
+ total_responses = sum(
+ len(responses) for responses in self.garfbot_responses.values()
+ )
+ logger.info(
+ f"Loaded responses for {len(self.garfbot_responses)} server(s), ({total_responses} total responses)"
+ )
+ except Exception as e:
+ logger.info(f"Error loading responses: {e}")
+ self.garfbot_responses = {}
+ else:
+ self.garfbot_responses = {}
+
+ def save_responses(self):
+ try:
+ save_data = {str(k): v for k, v in self.garfbot_responses.items()}
+ with open(self.responses_file, "w", encoding="utf-8") as f:
+ json.dump(save_data, f, indent=2, ensure_ascii=False)
+ total_responses = sum(
+ len(responses) for responses in self.garfbot_responses.values()
+ )
+ logger.info(
+ f"Saved responses for {len(self.garfbot_responses)} servers ({total_responses} total responses)"
+ )
+ except Exception as e:
+ logger.info(f"Error saving responses: {e}")
+
+ def get_responses(self, guild_id):
+ if guild_id not in self.garfbot_responses:
+ self.garfbot_responses[guild_id] = {}
+ return self.garfbot_responses[guild_id]
+
+ async def garfbot_response(self, message, content):
+ guild_id = message.guild.id
+
+ logger.info(message.content)
+
+ match = re.search(r'garfbot response add "(.+)" "(.+)"', content, re.IGNORECASE)
+ if match:
+ trigger = match.group(1)
+ response_text = match.group(2)
+ await self.add_response(message, guild_id, trigger, response_text)
+ return
+
+ match = re.search(r"garfbot response add (\S+) (.+)", content, re.IGNORECASE)
+ if match:
+ trigger = match.group(1)
+ response_text = match.group(2)
+ await self.add_response(message, guild_id, trigger, response_text)
+ return
+
+ match = re.search(r"garfbot\s+response\s+remove\s+(.+)", content, re.IGNORECASE)
+ if match:
+ trigger = match.group(1).strip()
+ await self.remove_response(message, guild_id, trigger)
+ return
+
+ if content.lower() == "garfbot response list":
+ await self.list_responses(message, guild_id)
+ return
+
+ await message.channel.send(
+ "**Garfbot Auto-Response Commands:**\n"
+ '`garfbot response add "trigger" "response"`\n'
+ '`garfbot response remove "trigger"`\n'
+ "`garfbot response list`\n\n"
+ "**Examples:**\n"
+ '`garfbot response add "hi" "Hello there!"`\n'
+ '`garfbot response add "that\'s what" "That\'s what she said!"`\n'
+ '`garfbot response remove "hi"`'
+ )
+
+ async def add_response(self, message, guild_id, trigger, response_text):
+ if not response_text or not trigger:
+ await message.channel.send("❌ Trigger and response must not be null.")
+ return
+
+ responses = self.get_responses(guild_id)
+ responses[trigger] = response_text
+ self.garfbot_responses[guild_id] = responses
+ self.save_responses()
+
+ embed = discord.Embed(title="✅ Auto-response Added.", color=0x00FF00)
+ embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True)
+ embed.add_field(name="Response", value=f"`{response_text}`", inline=True)
+ embed.set_footer(text=f"Server: {message.guild.name}")
+
+ await message.channel.send(embed=embed)
+
+ async def remove_response(self, message, guild_id, trigger):
+ responses = self.get_responses(guild_id)
+
+ if trigger in responses:
+ removed_response = responses[trigger]
+ del responses[trigger]
+ self.garfbot_responses[guild_id] = responses
+ self.save_responses()
+
+ embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
+ embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True)
+ embed.add_field(name="Response", value=f"`{removed_response}`", inline=True)
+ embed.set_footer(text=f"Server: {message.guild.name}")
+
+ await message.channel.send(embed=embed)
+ return
+
+ for key in responses.keys():
+ if key.lower() == trigger.lower():
+ removed_response = responses[key]
+ del responses[key]
+ self.garfbot_responses[guild_id] = responses
+ self.save_responses()
+
+ embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
+ embed.add_field(name="Trigger", value=f"`{key}`", inline=True)
+ embed.add_field(
+ name="Response", value=f"`{removed_response}`", inline=True
+ )
+ embed.set_footer(text=f"Server: {message.guild.name}")
+
+ await message.channel.send(embed=embed)
+ return
+
+ await message.channel.send(
+ f"❌ No auto-response found for trigger: `{trigger}`"
+ )
+
+ async def list_responses(self, message, guild_id):
+ responses = self.get_responses(guild_id)
+
+ if not responses:
+ await message.channel.send("No auto-responses configured for this server.")
+ return
+
+ embed = discord.Embed(
+ title=f"Auto-Responses for {message.guild.name}", color=0x3498DB
+ )
+
+ for i, (trigger, response) in enumerate(responses.items(), 1):
+ display_response = response[:50] + "..." if len(response) > 50 else response
+ embed.add_field(
+ name=f"{i}. `{trigger}`", value=f"→ {display_response}", inline=False
+ )
+
+ embed.set_footer(text=f"Total responses: {len(responses)}")
+ await message.channel.send(embed=embed)
diff --git a/garfpy/wiki.py b/garfpy/wiki.py
deleted file mode 100644
index 0904bc4..0000000
--- a/garfpy/wiki.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import wikipedia
-from garfpy import generate_chat
-
-async def wikisum(search_term):
- try:
- summary = wikipedia.summary(search_term)
- garfsum = await generate_chat(f"Please summarize in your own words: {summary}")
-
- return garfsum
-
- except Exception as e:
- return e
\ No newline at end of file