Compare commits

..

42 Commits

Author SHA1 Message Date
f887d38add Merge branch 'main' into iputils-refactor 2025-06-05 23:23:07 +00:00
ef59f95bc0 fix iputils f strings 2025-06-05 18:21:19 -05:00
aeeec42358 Merge pull request 'fix iputils embeds' (#3) from iputils-refactor into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
Reviewed-on: #3
2025-06-05 23:18:49 +00:00
c491cb0563 fix iputils embeds 2025-06-05 18:17:06 -05:00
9627af9ff3 Merge pull request 'iputils-refactor' (#2) from iputils-refactor into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 5s
Reviewed-on: #2
2025-06-05 22:27:09 +00:00
a131c4c2a4 Merge branch 'main' into iputils-refactor 2025-06-05 22:26:57 +00:00
6b9c2b638a format etc 2025-06-05 17:25:43 -05:00
2415a8146d looks good 2025-06-05 16:25:45 -05:00
d3eb82f1bd Merge pull request 'refactor-cleanup' (#1) from refactor-cleanup into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 5s
Reviewed-on: #1
2025-06-05 18:34:22 +00:00
353284b2c3 looks good 2025-06-05 13:33:14 -05:00
82561f050f getting classy 2025-06-04 21:36:43 -05:00
39b821ff7f fix regex hopefully
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 5s
2025-06-04 03:14:25 -05:00
d9ac0cc36e some cleanup etc
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-04 02:55:49 -05:00
d707398c26 some cleanup on main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 20s
2025-06-04 02:33:37 -05:00
0cb5f8fa38 implement auto response
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-04 02:22:19 -05:00
d70584d4f8 log test
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-03 20:57:39 -05:00
49831aeb47 response log
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-03 20:55:41 -05:00
4226ce86c8 fix GarfbotRespond instance
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 14s
2025-06-03 20:41:49 -05:00
b4ca6b778d add auto responses
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
2025-06-03 20:33:55 -05:00
9d5765c492 fix gitea deploy pipeline
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
2025-06-02 17:59:32 -05:00
8216af5f2a deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:52:24 -05:00
219b1745da deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 2s
2025-06-02 13:51:46 -05:00
d570069e6b test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:51:06 -05:00
f267526411 deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:49:49 -05:00
86fa47d259 remove docker socket mount
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 5s
2025-06-02 13:47:57 -05:00
888e647e1f add gitea deploy script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 1s
2025-06-02 13:06:23 -05:00
c1beb8374e fix qr sendfile 2025-05-25 04:15:08 -05:00
48a7033ba6 add pillow 2025-05-25 04:11:57 -05:00
9e436d8d78 fix qrcode call async 2025-05-25 04:08:18 -05:00
d8c286bb2d add qrcode 2025-05-25 04:04:32 -05:00
a081edfa97 add qr code gen 2025-05-25 04:02:34 -05:00
716e30e2b1 fix chat gen 2025-05-23 20:46:26 -05:00
73499a9586 oops 2025-05-23 13:35:19 -05:00
e3a762fe10 await chat gen 2025-05-23 13:33:02 -05:00
842b592bfc garfsum 2025-05-23 13:30:36 -05:00
f9f88be8af wiki except 2025-05-23 11:41:26 -05:00
5c8696216e fix wiki init 2025-05-22 17:12:56 -05:00
aa16064d17 fix wikisum import 2025-05-22 17:11:39 -05:00
ffd642fd2a add wiki stuff 2025-05-22 17:09:48 -05:00
3945eb6763 add wiki 2025-05-22 16:53:37 -05:00
0438b55ce6 remove tenor gif bs 2025-05-22 16:46:22 -05:00
8fb4e089be Update README.md 2024-12-28 05:08:50 +00:00
14 changed files with 680 additions and 323 deletions

View File

@ -0,0 +1,28 @@
name: Garfbot CI/CD Deployment
on:
push:
branches: [ main ]
jobs:
Deploy:
container:
volumes:
- /home/crate/garfbot:/workspace/crate/garfbot/deploy
steps:
- name: Pull Garfbot and restart container
run: |
cd /workspace/crate/garfbot/deploy
git pull origin main
CHANGED=$(git diff --name-only HEAD~1 HEAD)
if echo "$CHANGED" | grep -qE "(Dockerfile|requirements\.txt|docker-compose\.yml)"; then
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
else
docker restart garfbot
fi

3
.gitignore vendored
View File

@ -6,3 +6,6 @@ garfpy/__pycache__/
*.old *.old
*.log* *.log*
meows.py meows.py
meow_counts.json
user_stats.json
responses.json

View File

@ -12,5 +12,8 @@ RUN pip3 install discord
RUN pip3 install openai RUN pip3 install openai
RUN pip3 install aiohttp RUN pip3 install aiohttp
RUN pip3 install requests RUN pip3 install requests
RUN pip3 install wikipedia
RUN pip3 install pillow
RUN pip3 install qrcode
CMD [ "python", "garfmain.py" ] CMD [ "python", "garfmain.py" ]

View File

@ -16,7 +16,7 @@ Responds with iputils-ping result from target.
`garfpic {target}` `garfpic {target}`
Responds with dns lookup result from target. Responds with dns lookup result from target.
`garfpic {target}` `garfhack {target}`
Responds with nmap scan result from target. Responds with nmap scan result from target.
`garfshop {item} {zip}` `garfshop {item} {zip}`
@ -36,7 +36,6 @@ Add your various API tokens:
```python ```python
GARFBOT_TOKEN = "Discord API token" GARFBOT_TOKEN = "Discord API token"
OPENAI_TOKEN = "OpenAI API token" OPENAI_TOKEN = "OpenAI API token"
GIF_TOKEN = "tenor.com 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:

View File

@ -1,14 +1,16 @@
import config import config
# import random
import asyncio import asyncio
import discord import discord
import subprocess
from garfpy import( from garfpy import (
logger, is_private, logger,
kroger_token, find_store, search_product, IPUtils,
picture_time, process_image_requests, generate_chat, aod_message,
aod_message) generate_qr,
Kroger,
GarfAI,
GarfbotRespond,
)
gapikey = config.GIF_TOKEN gapikey = config.GIF_TOKEN
@ -22,12 +24,20 @@ intents.messages = True
intents.message_content = True intents.message_content = True
garfbot = discord.Client(intents=intents) garfbot = discord.Client(intents=intents)
garf_respond = GarfbotRespond()
garfield = GarfAI()
iputils = IPUtils()
kroger = Kroger()
@garfbot.event @garfbot.event
async def on_ready(): async def on_ready():
try: try:
asyncio.create_task(process_image_requests()) garf_respond.load_responses()
logger.info(f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}.") asyncio.create_task(garfield.process_image_requests())
logger.info(
f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}."
)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@ -37,133 +47,94 @@ async def on_message(message):
if message.author == garfbot.user: if message.author == garfbot.user:
return return
if message.content.lower().startswith("hey garfield") or isinstance(message.channel, discord.DMChannel): content = message.content.strip()
question = message.content[12:] if message.content.lower().startswith("hey garfield") else message.content lower = content.lower()
answer = await generate_chat(question) user_name = message.author.name
await message.channel.send(answer) guild_id = message.guild.id
guild_name = message.guild.name if message.guild else "Direct Message"
if message.content.lower().startswith('garfpic '): # IP utils
user = message.author.name if message.guild and lower.startswith(("garfping ", "garfdns ", "garfhack ")):
server = message.guild.name if message.guild else "Direct Message" await iputils.scan(message, user_name, guild_name, lower)
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 picture_time(message, prompt)
# if message.content.lower() == "lasagna": # Wikipedia
# await send_gif(message, "garfield lasagna") if lower.startswith("garfwiki "):
query = message.content[9:]
summary = await garfield.wikisum(query)
await message.channel.send(summary)
# if message.content.lower() == "monday": # QR codes
# await send_gif(message, "garfield monday") if lower.startswith("garfqr "):
text = message.content[7:]
# if message.content.lower().startswith("garfgif "): if len(text) > 1000:
# search_term = message.content[8:] await message.channel.send("❌ Text too long! Maximum 1000 characters.")
# await send_gif(message, search_term) else:
try:
if message.content.lower().startswith("garfping "): qr_code = await generate_qr(text)
try: sendfile = discord.File(fp=qr_code, filename="qrcode.png")
query = message.content.split() await message.channel.send(file=sendfile)
user = message.author.name except Exception as e:
server = message.guild.name if message.guild else "Direct Message" logger.error(e)
target = query[-1] await message.channel.send(e)
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 # Kroger Shopping
if message.content.lower().startswith("garfshop "): if lower.startswith("garfshop "):
try: try:
kroken = kroger_token() query = message.content[9:]
kroger_query = message.content.split() response = kroger.garfshop(query)
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"
await message.channel.send(response) await message.channel.send(response)
except Exception as e: except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`") await message.channel.send(f"`GarfBot Error: {str(e)}`")
# Army of Dawn Server only!! # Chats & pics
if message.guild and message.guild.id == 719605634772893757: 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)
# Army of Dawn Server only!!
elif message.guild and message.guild.id == 719605634772893757:
await aod_message(garfbot, message) await aod_message(garfbot, message)
# Auto-responses
elif message.guild:
responses = garf_respond.get_responses(guild_id)
# # GarfGifs if lower.startswith("garfbot response "):
# @garfbot.event await garf_respond.garfbot_response(message, content)
# async def send_gif(message, search_term): return
# lmt = 50
# ckey = "garfbot" for trigger, response in responses.items():
# r = requests.get(f"https://tenor.googleapis.com/v2/search?q={search_term}&key={gapikey}&client_key={ckey}&limit={lmt}") if trigger.lower() in lower:
# if r.status_code == 200: await message.channel.send(response)
# top_50gifs = json.loads(r.content) break
# gif_url = random.choice(top_50gifs["results"])["itemurl"]
# logger.info(gif_url)
# # logger.info(gif_url)
# try:
# await message.channel.send(gif_url)
# except KeyError:
# await message.channel.send("Oops, something went wrong.")
# else:
# await message.channel.send(f"`Oops, something went wrong. Error code: {r.status_code}`")
# Run Garfbot
async def garfbot_connect(): async def garfbot_connect():
while True: while True:
try: try:
await garfbot.start(garfkey) await garfbot.start(garfkey)
except Exception as e: except Exception as e:
e = str(e) e = str(e)
logger.error(f"Garfbot couldn't connect! {e}") logger.error(f"Garfbot couldn't connect! {e}")
await asyncio.sleep(60) await asyncio.sleep(60)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(garfbot_connect()) asyncio.run(garfbot_connect())

View File

@ -1,12 +1,10 @@
# garfpy/__init__.py # garfpy/__init__.py
from .log import logger from .log import logger
from .kroger import( from .kroger import Kroger
kroger_token, find_store, search_product from .kroger import Kroger
) from .garfai import GarfAI
from .garfai import( from .respond import GarfbotRespond
picture_time,
process_image_requests,
generate_chat
)
from .iputils import is_private
from .aod import aod_message from .aod import aod_message
from .qr import generate_qr
from .iputils import IPUtils

View File

@ -11,6 +11,7 @@ from collections import defaultdict
meows_file = "meow_counts.json" meows_file = "meow_counts.json"
stats_file = "user_stats.json" stats_file = "user_stats.json"
def json_load(file_path, default): def json_load(file_path, default):
if os.path.isfile(file_path): if os.path.isfile(file_path):
with open(file_path, "r") as f: with open(file_path, "r") as f:
@ -18,77 +19,105 @@ def json_load(file_path, default):
else: else:
return default return default
meow_counts = defaultdict(int, json_load(meows_file, {})) meow_counts = defaultdict(int, json_load(meows_file, {}))
user_stats = json_load(stats_file, {}) user_stats = json_load(stats_file, {})
async def aod_message(garfbot, message): async def aod_message(garfbot, message):
if "meow" in message.content.lower(): if "meow" in message.content.lower():
logger.info(f"Meow detected! {message.author.name} said: {message.content}") 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: with open(meows_file, "w") as f:
json.dump(dict(meow_counts), f) json.dump(dict(meow_counts), f)
if message.content.lower() == "meowcount": 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." 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) await message.channel.send(response)
if message.content.lower() == "top meowers": if message.content.lower() == "top meowers":
top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[:10] top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[
embed = discord.Embed(title="Top Meowers :cat:", color=0x000000) :10
for i, (user_id, meow_count) in enumerate(top_meowers): ]
user = await garfbot.fetch_user(int(user_id)) embed = discord.Embed(title="Top Meowers :cat:", color=0x000000)
embed.add_field(name=f"{i+1}. {user.name}", value=f"{meow_count} meows", inline=False) for i, (user_id, meow_count) in enumerate(top_meowers):
await message.channel.send(embed=embed) 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": if message.content.lower() == "checking in":
user_id = str(message.author.id) user_id = str(message.author.id)
if user_id in user_stats and user_stats[user_id]["check_in_time"] is not None: 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.") await message.channel.send(
return f"{message.author.mention} You are already checked in. Please check out first."
)
return
check_in_time = datetime.now().timestamp() check_in_time = datetime.now().timestamp()
if user_id not in user_stats: 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] = {
user_stats[user_id]["check_in_time"] = check_in_time "check_ins": 0,
await message.channel.send(f"{message.author.mention} You have been checked in. Please mute your microphone.") "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": elif message.content.lower() == "checking out":
user_id = str(message.author.id) user_id = str(message.author.id)
if user_id not in user_stats or user_stats[user_id]["check_in_time"] is None: 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.") await message.channel.send(
return f"{message.author.mention} You have not checked in yet. Please check in first."
)
return
check_out_time = datetime.now().timestamp() check_out_time = datetime.now().timestamp()
check_in_time = user_stats[user_id]["check_in_time"] check_in_time = user_stats[user_id]["check_in_time"]
time_delta = check_out_time - check_in_time time_delta = check_out_time - check_in_time
user_stats[user_id]["check_ins"] += 1 user_stats[user_id]["check_ins"] += 1
user_stats[user_id]["total_time"] += time_delta user_stats[user_id]["total_time"] += time_delta
user_stats[user_id]["check_in_time"] = None user_stats[user_id]["check_in_time"] = None
with open("user_stats.json", "w") as f: with open("user_stats.json", "w") as f:
json.dump(user_stats, 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.") 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": elif message.content.lower() == "stats":
stats_embed = discord.Embed(title="User stats :trophy:", color=0x000000) 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) sorted_user_stats = sorted(
table_rows = [["Name", "Check-ins", "Total Time"]] user_stats.items(), key=lambda x: x[1]["total_time"], reverse=True
for user_id, stats in sorted_user_stats: )
if stats["check_in_time"] is None: table_rows = [["Name", "Check-ins", "Total Time"]]
total_time_seconds = stats["total_time"] for user_id, stats in sorted_user_stats:
hours, total_time_seconds = divmod(total_time_seconds, 3600) if stats["check_in_time"] is None:
minutes, total_time_seconds = divmod(total_time_seconds, 60) total_time_seconds = stats["total_time"]
seconds, fractions = divmod(total_time_seconds, 1) hours, total_time_seconds = divmod(total_time_seconds, 3600)
fractions_str = f"{fractions:.3f}"[2:] minutes, total_time_seconds = divmod(total_time_seconds, 60)
username = garfbot.get_user(int(user_id)).name seconds, fractions = divmod(total_time_seconds, 1)
table_rows.append([username, str(stats["check_ins"]), f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms"]) fractions_str = f"{fractions:.3f}"[2:]
else: username = garfbot.get_user(int(user_id)).name
username = garfbot.get_user(int(user_id)).name table_rows.append(
table_rows.append([username, "Currently checked in", "-"]) [
table_columns = list(zip(*table_rows[1:])) username,
table_fields = table_rows[0] str(stats["check_ins"]),
for field, values in zip(table_fields, table_columns): f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms",
stats_embed.add_field(name=field, value="\n".join(values), inline=True) ]
await message.channel.send(embed=stats_embed) )
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)

View File

@ -4,87 +4,97 @@ import config
import aiohttp import aiohttp
import asyncio import asyncio
import discord import discord
import wikipedia
from openai import AsyncOpenAI from openai import AsyncOpenAI
from garfpy import logger from garfpy import logger
openaikey = config.OPENAI_TOKEN
txtmodel = config.TXT_MODEL class GarfAI:
imgmodel = config.IMG_MODEL def __init__(self):
self.openaikey = config.OPENAI_TOKEN
self.txtmodel = config.TXT_MODEL
self.imgmodel = config.IMG_MODEL
self.image_request_queue = asyncio.Queue()
# GarfPics async def garfpic(self, message, prompt):
image_request_queue = asyncio.Queue() await self.image_request_queue.put({"message": message, "prompt": prompt})
async def picture_time(message, prompt): async def generate_image(self, prompt):
await image_request_queue.put({'message': message, 'prompt': 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): async def process_image_requests(self):
try: async with aiohttp.ClientSession() as session:
client = AsyncOpenAI(api_key = openaikey) while True:
response = await client.images.generate( request = await self.image_request_queue.get()
model=imgmodel, message = request["message"]
prompt=prompt, prompt = request["prompt"]
n=1, image_url = await self.generate_image(prompt)
size="1024x1024" if "GarfBot Error" not in image_url:
) logger.info("Downloading & sending image...")
image_url = response.data[0].url async with session.get(image_url) as resp:
return image_url if resp.status == 200:
except openai.BadRequestError as e: image_data = await resp.read()
return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`" ram_image = io.BytesIO(image_data)
except openai.InternalServerError as e: ram_image.seek(0)
logger.error(e) timestamp = message.created_at.strftime("%Y%m%d%H%M%S")
return f"`GarfBot Error: ({e.status_code}) - Monday`" filename = f"{timestamp}_generated_image.png"
except Exception as e: sendfile = discord.File(fp=ram_image, filename=filename)
logger.error(e) try:
return f"`GarfBot Error: Lasagna`" 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 def generate_chat(self, question):
async with aiohttp.ClientSession() as session: try:
while True: client = AsyncOpenAI(api_key=self.openaikey)
request = await image_request_queue.get() response = await client.chat.completions.create(
message = request['message'] model=self.txtmodel,
prompt = request['prompt'] messages=[
image_url = await generate_image(prompt) {
if "GarfBot Error" not in image_url: "role": "system",
logger.info("Downloading & sending image...") "content": "Pretend you are sarcastic Garfield.",
async with session.get(image_url) as resp: },
if resp.status == 200: {"role": "user", "content": f"{question}"},
image_data = await resp.read() ],
ram_image = io.BytesIO(image_data) max_tokens=400,
ram_image.seek(0) )
timestamp = message.created_at.strftime('%Y%m%d%H%M%S') answer = response.choices[0].message.content
filename = f"{timestamp}_generated_image.png" return answer.replace("an AI language model", "a cartoon animal")
sendfile = discord.File(fp=ram_image, filename=filename) except openai.BadRequestError as e:
try: return f"`GarfBot Error: {e}`"
await message.channel.send(file=sendfile) except openai.APIError as e:
except Exception as e: logger.info(e, flush=True)
logger.error(e) return "`GarfBot Error: Monday`"
else: except Exception as e:
await message.channel.send("`GarfBot Error: Odie`") logger.info(e, flush=True)
else: return "`GarfBot Error: Lasagna`"
await message.channel.send(image_url)
image_request_queue.task_done()
await asyncio.sleep(2)
# GarfChats async def wikisum(self, query):
async def generate_chat(question): try:
try: summary = wikipedia.summary(query)
client = AsyncOpenAI(api_key = openaikey) garfsum = await self.generate_chat(
response = await client.chat.completions.create( f"Please summarize in your own words: {summary}"
model=txtmodel, )
messages=[ return garfsum
{"role": "system", "content": "Pretend you are sarcastic Garfield."}, except Exception as e:
{"role": "user", "content": f"{question}"} return e
],
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`"

View File

@ -1,19 +1,73 @@
import discord
import ipaddress import ipaddress
import subprocess
from garfpy import logger
def is_private(target):
try: class IPUtils:
ip_obj = ipaddress.ip_address(target) def is_private(self, target):
if ip_obj.is_private: try:
return True ip_obj = ipaddress.ip_address(target)
except ValueError: if ip_obj.is_private:
if "crate.lan" in target.lower(): return True
return True except ValueError:
if "crate.zip" in target.lower(): if "crate.lan" in target.lower():
return True return True
if "memtec.org" in target.lower(): if "crate.zip" in target.lower():
return True return True
if "crateit.net" in target.lower(): if "memtec.org" in target.lower():
return True return True
if "garfbot.art" in target.lower(): if "crateit.net" in target.lower():
return True return True
return False 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)}`")

View File

@ -4,45 +4,76 @@ from base64 import b64encode
from garfpy import logger from garfpy import logger
client_id = config.CLIENT_ID class Kroger:
client_secret = config.CLIENT_SECRET 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(): response = requests.post(
headers = { "https://api.kroger.com/v1/connect/oauth2/token",
'Content-Type': 'application/x-www-form-urlencoded', headers=headers,
'Authorization': f'Basic {auth}' data={"grant_type": "client_credentials", "scope": "product.compact"},
} )
response = requests.post('https://api.kroger.com/v1/connect/oauth2/token', headers=headers, data={ response.raise_for_status()
'grant_type': 'client_credentials', return response.json()["access_token"]
'scope': 'product.compact'
})
response.raise_for_status() def find_store(self, zipcode, kroken):
return response.json()['access_token'] 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): def search_product(self, product, loc_id, kroken):
headers = { logger.info(f"Searching for {product}...")
'Authorization': f'Bearer {kroken}', headers = {
} "Authorization": f"Bearer {kroken}",
params = { }
'filter.zipCode.near': zipcode, params = {
'filter.limit': 1, "filter.term": product,
} "filter.locationId": loc_id,
response = requests.get('https://api.kroger.com/v1/locations', headers=headers, params=params) "filter.limit": 10,
return response.json() }
response = requests.get(
"https://api.kroger.com/v1/products", headers=headers, params=params
)
return response.json()
def search_product(product, loc_id, kroken): def garfshop(self, query):
logger.info(f"Searching for {product}...") try:
headers = { query = query.split()
'Authorization': f'Bearer {kroken}', kroken = self.kroger_token()
} product = query[-2]
params = { zipcode = query[-1]
'filter.term': product, loc_data = self.find_store(zipcode, kroken)
'filter.locationId': loc_id, loc_id = loc_data["data"][0]["locationId"]
'filter.limit': 10 store_name = loc_data["data"][0]["name"]
} product_query = self.search_product(product, loc_id, kroken)
response = requests.get('https://api.kroger.com/v1/products', headers=headers, params=params) products = product_query["data"]
return response.json() 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

View File

@ -1,19 +1,18 @@
import logging import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger('garflog') logger = logging.getLogger("garflog")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
formatter=logging.Formatter( formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s', "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
datefmt='%Y-%m-%d %H:%M:%S' )
)
file_handler = TimedRotatingFileHandler( file_handler = TimedRotatingFileHandler(
'garfbot.log', "garfbot.log",
when='midnight', when="midnight",
interval=1, interval=1,
backupCount=7, backupCount=7,
delay=True # Counter-intuitively, this will flush output immediately delay=True, # Counter-intuitively, this will flush output immediately
) )
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)

64
garfpy/qr.py Normal file
View File

@ -0,0 +1,64 @@
import qrcode
from io import BytesIO
def calculate_qr_settings(text):
text_length = len(text)
if text_length <= 25:
version = 1
box_size = 12
elif text_length <= 47:
version = 2
box_size = 10
elif text_length <= 77:
version = 3
box_size = 8
elif text_length <= 114:
version = 4
box_size = 7
elif text_length <= 154:
version = 5
box_size = 6
elif text_length <= 195:
version = 6
box_size = 5
elif text_length <= 224:
version = 7
box_size = 5
elif text_length <= 279:
version = 8
box_size = 4
elif text_length <= 335:
version = 9
box_size = 4
elif text_length <= 395:
version = 10
box_size = 3
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")
img_buffer.seek(0)
return img_buffer

165
garfpy/respond.py Normal file
View File

@ -0,0 +1,165 @@
from garfpy import logger
import discord
import json
import os
import re
class GarfbotRespond:
def __init__(self):
self.guild_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.guild_responses = json.load(f)
self.guild_responses = {
int(k): v for k, v in self.guild_responses.items()
}
total_responses = sum(
len(responses) for responses in self.guild_responses.values()
)
logger.info(
f"Loaded responses for {len(self.guild_responses)} server(s), ({total_responses} total responses)"
)
except Exception as e:
logger.info(f"Error loading responses: {e}")
self.guild_responses = {}
else:
self.guild_responses = {}
def save_responses(self):
try:
save_data = {str(k): v for k, v in self.guild_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.guild_responses.values()
)
logger.info(
f"Saved responses for {len(self.guild_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.guild_responses:
self.guild_responses[guild_id] = {}
return self.guild_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.guild_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.guild_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.guild_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)

View File

@ -2,3 +2,6 @@ discord.py
openai openai
aiohttp aiohttp
requests requests
wikipedia
pillow
qrcode