Compare commits

...

28 Commits

Author SHA1 Message Date
aa3996bbd9 fix deployment script
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 6s
2025-06-07 13:43:14 -05:00
c71a55da2f fix deployment script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 4s
2025-06-07 13:38:16 -05:00
7f0ce32eea fix deployment script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 4s
2025-06-07 13:34:59 -05:00
e34469a755 Merge pull request 'move help and update dockerfile' (#7) from help into main
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 2m3s
Reviewed-on: #7

looks good
2025-06-07 18:08:30 +00:00
e1707e789c move help and update dockerfile 2025-06-07 13:07:26 -05:00
a53a81a180 Merge pull request 'weather' (#6) from weather into main
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 4s
Reviewed-on: #6
2025-06-07 01:19:07 +00:00
4d2a0dc2d9 add weather 2025-06-06 20:14:50 -05:00
6ec931265f Merge remote-tracking branch 'origin/main' into weather 2025-06-06 18:37:23 -05:00
bdc3c13edb rename cicd compose stack
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 1m22s
2025-06-06 17:36:28 -05:00
b63e1738d1 add docker compose and update pipeline
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 18s
2025-06-06 17:20:43 -05:00
00bc9db5b3 hopefully this fixes it
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 6s
2025-06-06 16:44:04 -05:00
9f9a484613 change Dockerfile to use python alpine img
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 1m3s
2025-06-06 16:39:10 -05:00
aecafc53fc fix garfbot help
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 17s
2025-06-06 01:22:29 -05:00
af1c689fe7 add garfbot help
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 4s
2025-06-06 01:20:43 -05:00
1f8fc09562 update README
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 22s
2025-06-06 00:41:52 -05:00
2686f7e8b5 Merge pull request 'fix iputils f strings' (#4) from iputils-refactor into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
Reviewed-on: #4
2025-06-05 23:23:51 +00:00
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
4b077e3fd9 weather 2025-06-03 17:54:40 -05:00
16 changed files with 832 additions and 431 deletions

View File

@ -8,21 +8,20 @@ jobs:
Deploy: Deploy:
container: container:
volumes: volumes:
- /home/crate/garfbot:/workspace/crate/garfbot/deploy - /home/crate/garfbot:/workspace/crate/garfbot/garfbot
steps: steps:
- name: Pull Garfbot and restart container - name: Pull GarfBot and re-deploy
run: | run: |
cd /workspace/crate/garfbot/deploy cd /workspace/crate/garfbot/garfbot
git pull origin main git pull origin main
CHANGED=$(git diff --name-only HEAD~1 HEAD)
CHANGED=$(git diff --name-only HEAD~1 HEAD) if echo "$CHANGED" | grep -qE "(Dockerfile|requirements\.txt|docker-compose\.yml|\.gitea/workflows/deploy\.yaml)"; then
docker compose down
if echo "$CHANGED" | grep -qE "(Dockerfile|requirements\.txt|docker-compose\.yml)"; then docker build -t git.crate.zip/crate/garfbot:latest .
docker stop garfbot docker compose up -d
docker rm garfbot else
docker build -t git.crate.zip/crate/garfbot:latest . docker restart garfbot
docker run -d --restart always -v $PWD:/usr/src/app --name garfbot git.crate.zip/crate/garfbot:latest fi
else
docker restart garfbot
fi

View File

@ -1,19 +1,29 @@
FROM python:3.11.10-bookworm FROM python:alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt update RUN apk update && \
RUN apt install -y iputils-ping apk add --no-cache \
RUN apt install -y dnsutils iputils \
RUN apt install -y nmap bind-tools \
RUN apt install -y python3 nmap \
RUN apt install -y python3-pip gcc \
RUN pip3 install discord musl-dev \
RUN pip3 install openai jpeg-dev \
RUN pip3 install aiohttp zlib-dev \
RUN pip3 install requests freetype-dev \
RUN pip3 install wikipedia lcms2-dev \
RUN pip3 install pillow openjpeg-dev \
RUN pip3 install qrcode 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" ]

View File

@ -2,25 +2,44 @@ Who is GarfBot?
====== ======
![garfield](https://www.crate.zip/garfield.png) ![garfield](https://www.crate.zip/garfield.png)
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.
<br>There are a few ways you can interact with him on discord, either in a public server or by direct message:
`hey garfield {prompt}` `hey garfield {prompt}`
Responds with text. <br>Responds with text.
`garfpic {prompt}` `garfpic {prompt}`
Responds with an image. <br>Responds with an image.
`garfping {target}` `garfping {target}`
Responds with iputils-ping result from target. <br>Responds with iputils-ping result from target.
`garfpic {target}` `garfdns {target}`
Responds with dns lookup result from target. <br>Responds with dns lookup result from target.
`garfhack {target}` `garfhack {target}`
Responds with nmap scan result from target. <br>Responds with nmap scan result from target.
`garfshop {item} {zip}` `garfshop {item} {zip}`
Responds with 10 grocery {item}s from the nearest Kroger location, listed from least to most expensive. <br>Responds with 10 grocery {item}s from the nearest Kroger location, listed from least to most expensive.
`garfwiki {query}`
<br>Garfbot looks up a wikipedia article and will summarize it for you.
`garfqr {text}`
<br>Create a QR code for any string up to 1000 characters.
`garfbot response {add} {trigger} {response}`
<br>Add a GarfBot auto response for your server. Use "quotes" if you like.
`garfbot response {remove} {trigger}`
<br>Remove a GarfBot auto response for your server.
`garfbot response {list}`
<br>List current GarfBot auto responses for your server.
`garfbot help`
<br>Show a list of these commands.
Installation Installation
====== ======
@ -38,7 +57,8 @@ GARFBOT_TOKEN = "Discord API token"
OPENAI_TOKEN = "OpenAI 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.
<br>Run the container binding /usr/src/app to GarfBot's CWD:
```console ```console
$ docker build -t garfbot . $ 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 $ sudo nano /etc/systemd/system/garfbot.service
``` ```
Replace {user} with your username: Replace $USER with your username:
```console ```console
[Unit] [Unit]
@ -63,8 +83,8 @@ After=multi-user.target
[Service] [Service]
Type=simple Type=simple
Restart=always Restart=always
User={user} User=$USER
WorkingDirectory=/home/{user}/garfbot WorkingDirectory=/home/$USER/garfbot
ExecStart=/usr/bin/python garfbot.py ExecStart=/usr/bin/python garfbot.py
[Install] [Install]

7
docker-compose.yml Normal file
View File

@ -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

View File

@ -1,13 +1,18 @@
import config import config
import asyncio import asyncio
import discord import discord
import subprocess
from garfpy import( from garfpy import (
logger, is_private, help,
kroger_token, find_store, search_product, logger,
garfpic, process_image_requests, generate_chat, IPUtils,
aod_message, wikisum, generate_qr, GarfbotRespond) aod_message,
generate_qr,
Kroger,
GarfAI,
GarfbotRespond,
WeatherAPI,
)
gapikey = config.GIF_TOKEN gapikey = config.GIF_TOKEN
@ -22,51 +27,47 @@ intents.message_content = True
garfbot = discord.Client(intents=intents) garfbot = discord.Client(intents=intents)
garf_respond = GarfbotRespond() garf_respond = GarfbotRespond()
garfield = GarfAI()
iputils = IPUtils()
kroger = Kroger()
weather = WeatherAPI()
@garfbot.event @garfbot.event
async def on_ready(): async def on_ready():
try: try:
asyncio.create_task(process_image_requests())
garf_respond.load_responses() 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)
@garfbot.event @garfbot.event
async def on_message(message): async def on_message(message):
content = message.content.strip()
lower = content.lower()
user = message.author.name
guild = message.guild.name if message.guild else "Direct Message"
guild_id = message.guild.id
# Chats & pics
if message.author == garfbot.user: if message.author == garfbot.user:
return return
if lower.startswith("hey garfield") or isinstance(message.channel, discord.DMChannel): content = message.content.strip()
question = content[12:] if lower.startswith("hey garfield") else message.content lower = content.lower()
answer = await generate_chat(question) user_name = message.author.name
logger.info(f"Chat Request - User: {user}, Server: {guild}, Prompt: {question}") guild_id = message.guild.id
await message.channel.send(answer) guild_name = message.guild.name if message.guild else "Direct Message"
if lower.startswith('garfpic '): # IP utils
prompt = content[8:] if message.guild and lower.startswith(("garfping ", "garfdns ", "garfhack ")):
logger.info(f"Image Request - User: {user}, Server: {guild}, Prompt: {prompt}") await iputils.scan(message, user_name, guild_name, lower)
await message.channel.send(f"`Please wait... image generation queued: {prompt}`")
await garfpic(message, prompt)
# Wikipedia # Wikipedia
if lower.startswith('garfwiki '): if lower.startswith("garfwiki "):
search_term = message.content[9:] query = message.content[9:]
summary = await wikisum(search_term) summary = await garfield.wikisum(query)
await message.channel.send(summary) await message.channel.send(summary)
# QR codes # QR codes
if lower.startswith('garfqr '): if lower.startswith("garfqr "):
text = message.content[7:] text = message.content[7:]
if len(text) > 1000: if len(text) > 1000:
await message.channel.send("❌ Text too long! Maximum 1000 characters.") await message.channel.send("❌ Text too long! Maximum 1000 characters.")
@ -79,95 +80,74 @@ async def on_message(message):
logger.error(e) logger.error(e)
await message.channel.send(e) await message.channel.send(e)
# IP utils
query = message.content.split()
target = query[-1]
if lower.startswith("garfping "):
try:
logger.info(f"Ping Request - User: {user}, Server: {guild}, 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 lower.startswith("garfdns "):
try:
logger.info(f"NSLookup Request - User: {user}, Server: {guild}, 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 lower.startswith("garfhack "):
try:
logger.info(f"Nmap Request - User: {user}, Server: {guild}, 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 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)}`")
# 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)
# Weather
elif lower.startswith("garfbot weather "):
location = lower[16:]
embed = await weather.weather(location)
await message.channel.send(embed=embed)
# GarfBot help
elif lower.strip() == "garfbot help":
await help(message)
# Army of Dawn Server only!! # Army of Dawn Server only!!
if message.guild and message.guild.id == 719605634772893757: elif message.guild and message.guild.id == 719605634772893757:
await aod_message(garfbot, message) await aod_message(garfbot, message)
# Auto-responses # Auto-responses
if message.guild: elif message.guild:
responses = garf_respond.get_responses(guild_id) responses = garf_respond.get_responses(guild_id)
if lower.startswith('garfbot response '): if lower.startswith("garfbot response "):
await garf_respond.garfbot_response(message, content) await garf_respond.garfbot_response(message, content)
return return
for trigger, response in responses.items(): for trigger, response in responses.items():
if trigger.lower() in lower: if trigger.lower() in lower:
await message.channel.send(response) await message.channel.send(response)
break break
# Run Garfbot
# 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,16 +1,12 @@
# garfpy/__init__.py # garfpy/__init__.py
from .log import logger from .log import logger
from .kroger import( from .help import help
kroger_token, find_store, search_product from .kroger import Kroger
) from .kroger import Kroger
from .garfai import( from .garfai import GarfAI
garfpic, from .respond import GarfbotRespond
process_image_requests,
generate_chat
)
from .iputils import is_private
from .aod import aod_message from .aod import aod_message
from .wiki import wikisum
from .qr import generate_qr from .qr import generate_qr
from .respond import GarfbotRespond from .iputils import IPUtils
from .weather import WeatherAPI

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,86 +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()
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): 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.`" image = io.BytesIO(image_data)
except openai.InternalServerError as e: 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=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`"

60
garfpy/help.py Normal file
View File

@ -0,0 +1,60 @@
import discord
async def help(message):
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)

View File

@ -1,19 +1,85 @@
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)

View File

@ -4,7 +4,7 @@ from io import BytesIO
def calculate_qr_settings(text): def calculate_qr_settings(text):
text_length = len(text) text_length = len(text)
if text_length <= 25: if text_length <= 25:
version = 1 version = 1
box_size = 12 box_size = 12
@ -38,26 +38,27 @@ def calculate_qr_settings(text):
else: else:
version = None version = None
box_size = 3 box_size = 3
return version, box_size return version, box_size
async def generate_qr(text): async def generate_qr(text):
version, box_size = calculate_qr_settings(text) version, box_size = calculate_qr_settings(text)
qr = qrcode.QRCode( qr = qrcode.QRCode(
version=version, version=version,
error_correction=qrcode.constants.ERROR_CORRECT_L, error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=box_size, box_size=box_size,
border=4, border=4,
) )
qr.add_data(text) qr.add_data(text)
qr.make(fit=True) qr.make(fit=True)
qr_image = qr.make_image(fill_color="black", back_color="white") qr_image = qr.make_image(fill_color="black", back_color="white")
img_buffer = BytesIO() img_buffer = BytesIO()
qr_image.save(img_buffer, format='PNG') qr_image.save(img_buffer, format="PNG")
img_buffer.seek(0) img_buffer.seek(0)
return img_buffer return img_buffer

View File

@ -7,157 +7,159 @@ import re
class GarfbotRespond: class GarfbotRespond:
def __init__(self): def __init__(self):
self.garfbot_guild_responses = {} self.garfbot_responses = {}
self.responses_file = 'responses.json' self.responses_file = "responses.json"
def load_responses(self): def load_responses(self):
if os.path.exists(self.responses_file): if os.path.exists(self.responses_file):
try: try:
with open(self.responses_file, 'r', encoding='utf-8') as f: with open(self.responses_file, "r", encoding="utf-8") as f:
self.garfbot_guild_responses = json.load(f) self.garfbot_responses = json.load(f)
self.garfbot_guild_responses = {int(k): v for k, v in self.garfbot_guild_responses.items()} self.garfbot_responses = {
total_responses = sum(len(responses) for responses in self.garfbot_guild_responses.values()) int(k): v for k, v in self.garfbot_responses.items()
logger.info(f"Loaded responses for {len(self.garfbot_guild_responses)} server(s), ({total_responses} total responses)") }
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: except Exception as e:
logger.info(f"Error loading responses: {e}") logger.info(f"Error loading responses: {e}")
self.garfbot_guild_responses = {} self.garfbot_responses = {}
else: else:
self.garfbot_guild_responses = {} self.garfbot_responses = {}
def save_responses(self): def save_responses(self):
try: try:
save_data = {str(k): v for k, v in self.garfbot_guild_responses.items()} save_data = {str(k): v for k, v in self.garfbot_responses.items()}
with open(self.responses_file, 'w', encoding='utf-8') as f: with open(self.responses_file, "w", encoding="utf-8") as f:
json.dump(save_data, f, indent=2, ensure_ascii=False) json.dump(save_data, f, indent=2, ensure_ascii=False)
total_responses = sum(len(responses) for responses in self.garfbot_guild_responses.values()) total_responses = sum(
logger.info(f"Saved responses for {len(self.garfbot_guild_responses)} servers ({total_responses} total responses)") 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: except Exception as e:
logger.info(f"Error saving responses: {e}") logger.info(f"Error saving responses: {e}")
def get_responses(self, guild_id): def get_responses(self, guild_id):
if guild_id not in self.garfbot_guild_responses: if guild_id not in self.garfbot_responses:
self.garfbot_guild_responses[guild_id] = {} self.garfbot_responses[guild_id] = {}
return self.garfbot_guild_responses[guild_id] return self.garfbot_responses[guild_id]
async def garfbot_response(self, message, content): async def garfbot_response(self, message, content):
guild_id = message.guild.id guild_id = message.guild.id
logger.info(message.content) logger.info(message.content)
match = re.search(r'garfbot response add "(.+)" "(.+)"', content, re.IGNORECASE) match = re.search(r'garfbot response add "(.+)" "(.+)"', content, re.IGNORECASE)
if match: if match:
trigger = match.group(1) trigger = match.group(1)
response_text = match.group(2) response_text = match.group(2)
await self.add_response(message, guild_id, trigger, response_text) await self.add_response(message, guild_id, trigger, response_text)
return return
match = re.search(r'garfbot response add (\S+) (.+)', content, re.IGNORECASE) match = re.search(r"garfbot response add (\S+) (.+)", content, re.IGNORECASE)
if match: if match:
trigger = match.group(1) trigger = match.group(1)
response_text = match.group(2) response_text = match.group(2)
await self.add_response(message, guild_id, trigger, response_text) await self.add_response(message, guild_id, trigger, response_text)
return return
match = re.search(r'garfbot\s+response\s+remove\s+(.+)', content, re.IGNORECASE) match = re.search(r"garfbot\s+response\s+remove\s+(.+)", content, re.IGNORECASE)
if match: if match:
trigger = match.group(1).strip() trigger = match.group(1).strip()
await self.remove_response(message, guild_id, trigger) await self.remove_response(message, guild_id, trigger)
return return
if content.lower() == "garfbot response list": if content.lower() == "garfbot response list":
await self.list_responses(message, guild_id) await self.list_responses(message, guild_id)
return return
await message.channel.send( await message.channel.send(
"**Garfbot Auto-Response Commands:**\n" "**Garfbot Auto-Response Commands:**\n"
"`garfbot response add \"trigger\" \"response\"`\n" '`garfbot response add "trigger" "response"`\n'
"`garfbot response remove \"trigger\"`\n" '`garfbot response remove "trigger"`\n'
"`garfbot response list`\n\n" "`garfbot response list`\n\n"
"**Examples:**\n" "**Examples:**\n"
"`garfbot response add \"hi\" \"Hello there!\"`\n" '`garfbot response add "hi" "Hello there!"`\n'
"`garfbot response add \"that's what\" \"That's what she said!\"`\n" '`garfbot response add "that\'s what" "That\'s what she said!"`\n'
"`garfbot response remove \"hi\"`" '`garfbot response remove "hi"`'
) )
async def add_response(self, message, guild_id, trigger, response_text): async def add_response(self, message, guild_id, trigger, response_text):
if not response_text or not trigger: if not response_text or not trigger:
await message.channel.send("❌ Trigger and response must not be null.") await message.channel.send("❌ Trigger and response must not be null.")
return return
responses = self.get_responses(guild_id) responses = self.get_responses(guild_id)
responses[trigger] = response_text responses[trigger] = response_text
self.garfbot_guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed( embed = discord.Embed(title="✅ Auto-response Added.", color=0x00FF00)
title="✅ Auto-response Added.",
color=0x00ff00
)
embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True) embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True)
embed.add_field(name="Response", value=f"`{response_text}`", inline=True) embed.add_field(name="Response", value=f"`{response_text}`", inline=True)
embed.set_footer(text=f"Server: {message.guild.name}") embed.set_footer(text=f"Server: {message.guild.name}")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
async def remove_response(self, message, guild_id, trigger): async def remove_response(self, message, guild_id, trigger):
responses = self.get_responses(guild_id) responses = self.get_responses(guild_id)
if trigger in responses: if trigger in responses:
removed_response = responses[trigger] removed_response = responses[trigger]
del responses[trigger] del responses[trigger]
self.garfbot_guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed( embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
title="✅ Auto-response Removed.",
color=0xff6b6b
)
embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True) embed.add_field(name="Trigger", value=f"`{trigger}`", inline=True)
embed.add_field(name="Response", value=f"`{removed_response}`", inline=True) embed.add_field(name="Response", value=f"`{removed_response}`", inline=True)
embed.set_footer(text=f"Server: {message.guild.name}") embed.set_footer(text=f"Server: {message.guild.name}")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
return return
for key in responses.keys(): for key in responses.keys():
if key.lower() == trigger.lower(): if key.lower() == trigger.lower():
removed_response = responses[key] removed_response = responses[key]
del responses[key] del responses[key]
self.garfbot_guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed( embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
title="✅ Auto-response Removed.",
color=0xff6b6b
)
embed.add_field(name="Trigger", value=f"`{key}`", inline=True) embed.add_field(name="Trigger", value=f"`{key}`", inline=True)
embed.add_field(name="Response", value=f"`{removed_response}`", inline=True) embed.add_field(
name="Response", value=f"`{removed_response}`", inline=True
)
embed.set_footer(text=f"Server: {message.guild.name}") embed.set_footer(text=f"Server: {message.guild.name}")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
return return
await message.channel.send(f"❌ No auto-response found for trigger: `{trigger}`") await message.channel.send(
f"❌ No auto-response found for trigger: `{trigger}`"
)
async def list_responses(self, message, guild_id): async def list_responses(self, message, guild_id):
responses = self.get_responses(guild_id) responses = self.get_responses(guild_id)
if not responses: if not responses:
await message.channel.send("No auto-responses configured for this server.") await message.channel.send("No auto-responses configured for this server.")
return return
embed = discord.Embed( embed = discord.Embed(
title=f"Auto-Responses for {message.guild.name}", title=f"Auto-Responses for {message.guild.name}", color=0x3498DB
color=0x3498db
) )
for i, (trigger, response) in enumerate(responses.items(), 1): for i, (trigger, response) in enumerate(responses.items(), 1):
display_response = response[:50] + "..." if len(response) > 50 else response display_response = response[:50] + "..." if len(response) > 50 else response
embed.add_field( embed.add_field(
name=f"{i}. `{trigger}`", name=f"{i}. `{trigger}`", value=f"{display_response}", inline=False
value=f"{display_response}",
inline=False
) )
embed.set_footer(text=f"Total responses: {len(responses)}") embed.set_footer(text=f"Total responses: {len(responses)}")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)

202
garfpy/weather.py Normal file
View File

@ -0,0 +1,202 @@
import discord
import aiohttp
import config
from garfpy import logger
class WeatherAPI:
def __init__(self, api_key=None):
self.api_key = api_key or config.WEATHER_TOKEN
self.base_url = "https://api.openweathermap.org/data/2.5/weather"
def parse_location(self, location):
location = location.strip().lower()
if location.isdigit():
if len(location) == 5:
return {"zip": f"{location},US"}
else:
return {"zip": location}
parts = location.split()
if len(parts) == 1:
return {"q": f"{parts[0]},US"}
elif len(parts) == 2:
city, second = parts
if len(second) == 2 and second.upper() not in [
"AK",
"AL",
"AR",
"AZ",
"CA",
"CO",
"CT",
"DE",
"FL",
"GA",
"HI",
"IA",
"ID",
"IL",
"IN",
"KS",
"KY",
"LA",
"MA",
"MD",
"ME",
"MI",
"MN",
"MO",
"MS",
"MT",
"NC",
"ND",
"NE",
"NH",
"NJ",
"NM",
"NV",
"NY",
"OH",
"OK",
"OR",
"PA",
"RI",
"SC",
"SD",
"TN",
"TX",
"UT",
"VA",
"VT",
"WA",
"WI",
"WV",
"WY",
"DC",
"AS",
"GU",
"MP",
"PR",
"VI",
]:
return {"q": f"{city},{second.upper()}"}
else:
return {"q": f"{city},{second},US"}
elif len(parts) == 3:
city, state, country = parts
return {"q": f"{city},{state},{country.upper()}"}
else:
if len(parts[-1]) == 2:
city_parts = parts[:-1]
country = parts[-1]
city_name = " ".join(city_parts)
return {"q": f"{city_name},{country.upper()}"}
elif len(parts) >= 2 and len(parts[-1]) == 2 and len(parts[-2]) <= 2:
city_parts = parts[:-2]
state = parts[-2]
country = parts[-1]
city_name = " ".join(city_parts)
return {"q": f"{city_name},{state},{country.upper()}"}
else:
city_name = " ".join(parts)
return {"q": f"{city_name},US"}
async def get_weather(self, location, units="metric"):
location_params = self.parse_location(location)
params = {
**location_params,
"appid": self.api_key,
"units": units,
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(self.base_url, params=params) as response:
response.raise_for_status()
return await response.json()
except aiohttp.ClientError as e:
logger.error(f"Error fetching weather data for '{location}': {e}")
return None
def weather_embed(self, weather_data):
if not weather_data:
embed = discord.Embed(
title="❌ Error",
description="Could not fetch weather data",
color=discord.Color.red(),
)
return embed
weather_emojis = {
"clear sky": "☀️",
"few clouds": "🌤️",
"scattered clouds": "",
"broken clouds": "☁️",
"shower rain": "🌦️",
"rain": "🌧️",
"thunderstorm": "⛈️",
"snow": "❄️",
"mist": "🌫️",
}
condition = weather_data["weather"][0]["description"].lower()
emoji = weather_emojis.get(condition, "🌍")
embed = discord.Embed(
title=f"{emoji} Weather in {weather_data['name']}",
description=f"{weather_data['weather'][0]['description'].title()}",
color=discord.Color.blue(),
)
embed.add_field(
name="🌡️ Temperature",
value=f"{weather_data['main']['temp']}°C\nFeels like {weather_data['main']['feels_like']}°C",
inline=True,
)
embed.add_field(
name="💧 Humidity",
value=f"{weather_data['main']['humidity']}%",
inline=True,
)
embed.add_field(
name="🗜️ Pressure",
value=f"{weather_data['main']['pressure']} hPa",
inline=True,
)
if "wind" in weather_data:
embed.add_field(
name="💨 Wind Speed",
value=f"{weather_data['wind']['speed']} m/s",
inline=True,
)
if "visibility" in weather_data:
embed.add_field(
name="👁️ Visibility",
value=f"{weather_data['visibility'] / 1000} km",
inline=True,
)
embed.set_footer(
text=f"Lat: {weather_data['coord']['lat']}, Lon: {weather_data['coord']['lon']}"
)
return embed
async def weather(self, location):
weather_data = await self.get_weather(location)
embed = self.weather_embed(weather_data)
return embed

View File

@ -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