Compare commits

..

22 Commits

Author SHA1 Message Date
13d2f09fe3 Merge pull request 'switch to commands bot' (#8) from commands-bot into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
Reviewed-on: #8

hopefully this works!
2025-06-07 22:53:29 +00:00
97f90e3814 switch to commands bot 2025-06-07 17:52:20 -05:00
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
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
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
4b077e3fd9 weather 2025-06-03 17:54:40 -05:00
12 changed files with 497 additions and 149 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)"; then if echo "$CHANGED" | grep -qE "(Dockerfile|requirements\.txt|docker-compose\.yml|\.gitea/workflows/deploy\.yaml)"; then
docker stop garfbot docker compose down
docker rm garfbot
docker build -t git.crate.zip/crate/garfbot:latest . docker build -t git.crate.zip/crate/garfbot:latest .
docker run -d --restart always -v $PWD:/usr/src/app --name garfbot git.crate.zip/crate/garfbot:latest docker compose up -d
else else
docker restart garfbot docker restart garfbot
fi 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
RUN pip3 install --no-cache-dir \
discord \
openai \
aiohttp \
requests \
wikipedia \
pillow \
qrcode
CMD [ "python", "garfmain.py" ] 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,8 +1,10 @@
import config import config
import asyncio import asyncio
import discord import discord
from discord.ext import commands
from garfpy import ( from garfpy import (
help,
logger, logger,
IPUtils, IPUtils,
aod_message, aod_message,
@ -10,6 +12,7 @@ from garfpy import (
Kroger, Kroger,
GarfAI, GarfAI,
GarfbotRespond, GarfbotRespond,
WeatherAPI,
) )
@ -22,12 +25,14 @@ intents = discord.Intents.default()
intents.members = True intents.members = True
intents.messages = True intents.messages = True
intents.message_content = True intents.message_content = True
garfbot = discord.Client(intents=intents)
garfbot = commands.Bot(command_prefix=["garfbot ", "garf", "$"], intents=intents)
garf_respond = GarfbotRespond() garf_respond = GarfbotRespond()
garfield = GarfAI() garfield = GarfAI()
iputils = IPUtils() iputils = IPUtils()
kroger = Kroger() kroger = Kroger()
weather = WeatherAPI()
@garfbot.event @garfbot.event
@ -36,12 +41,82 @@ async def on_ready():
garf_respond.load_responses() garf_respond.load_responses()
asyncio.create_task(garfield.process_image_requests()) asyncio.create_task(garfield.process_image_requests())
logger.info( logger.info(
f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}." f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}." # type: ignore
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@garfbot.command(name="ping")
async def ping(ctx, *, target):
"""Ping a target"""
logger.info(
f"Ping Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.ping(ctx, target)
@garfbot.command(name="dns")
async def dns(ctx, *, target):
"""DNS lookup for a target"""
logger.info(
f"NSLookup Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.dns(ctx, target)
@garfbot.command(name="hack")
async def hack(ctx, *, target):
"""Nmap scan a target"""
logger.info(
f"Nmap Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.scan(ctx, target)
@garfbot.command(name="qr")
async def garfbot_qr(ctx, *, text):
logger.info(
f"QR Code Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Text: {text}"
)
if len(text) > 1000:
await ctx.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
sendfile = discord.File(fp=qr_code, filename="qrcode.png")
await ctx.send(file=sendfile)
except Exception as e:
logger.error(e)
await ctx.send(e)
@garfbot.command(name="wiki")
async def garfbot_wiki(ctx, *, query):
summary = await garfield.wikisum(query)
await ctx.send(summary)
@garfbot.command(name="shop")
async def garfbot_shop(ctx, *, query):
try:
response = kroger.garfshop(query)
await ctx.send(response)
except Exception as e:
await ctx.send(f"`GarfBot Error: {str(e)}`")
@garfbot.command(name="weather")
async def garfbot_weather(ctx, *, location):
embed = await weather.weather(location)
await ctx.send(embed=embed)
# @garfbot.command(name="help")
# async def garfbot_help(ctx):
# await help(ctx)
@garfbot.event @garfbot.event
async def on_message(message): async def on_message(message):
if message.author == garfbot.user: if message.author == garfbot.user:
@ -53,41 +128,8 @@ async def on_message(message):
guild_id = message.guild.id guild_id = message.guild.id
guild_name = message.guild.name if message.guild else "Direct Message" guild_name = message.guild.name if message.guild else "Direct Message"
# IP utils
if message.guild and lower.startswith(("garfping ", "garfdns ", "garfhack ")):
await iputils.scan(message, user_name, guild_name, lower)
# Wikipedia
if lower.startswith("garfwiki "):
query = message.content[9:]
summary = await garfield.wikisum(query)
await message.channel.send(summary)
# QR codes
if lower.startswith("garfqr "):
text = message.content[7:]
if len(text) > 1000:
await message.channel.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
sendfile = discord.File(fp=qr_code, filename="qrcode.png")
await message.channel.send(file=sendfile)
except Exception as e:
logger.error(e)
await message.channel.send(e)
# Kroger Shopping
if lower.startswith("garfshop "):
try:
query = message.content[9:]
response = kroger.garfshop(query)
await message.channel.send(response)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
# Chats & pics # Chats & pics
elif lower.startswith("hey garfield") or isinstance( if lower.startswith("hey garfield") or isinstance(
message.channel, discord.DMChannel message.channel, discord.DMChannel
): ):
prompt = content[12:] if lower.startswith("hey garfield") else message.content prompt = content[12:] if lower.startswith("hey garfield") else message.content
@ -97,7 +139,7 @@ async def on_message(message):
) )
await message.channel.send(answer) await message.channel.send(answer)
elif lower.startswith("garfpic "): if lower.startswith("garfpic "):
prompt = content[8:] prompt = content[8:]
logger.info( logger.info(
f"Image Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}" f"Image Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}"
@ -107,6 +149,10 @@ async def on_message(message):
) )
await garfield.garfpic(message, prompt) await garfield.garfpic(message, prompt)
# GarfBot help
elif lower.strip() == "garfbot help":
await help(message)
# Army of Dawn Server only!! # Army of Dawn Server only!!
elif 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)
@ -124,8 +170,10 @@ async def on_message(message):
await message.channel.send(response) await message.channel.send(response)
break break
await garfbot.process_commands(message)
# Run Garfbot
# Run GarfBot
async def garfbot_connect(): async def garfbot_connect():
while True: while True:
try: try:

View File

@ -1,6 +1,7 @@
# garfpy/__init__.py # garfpy/__init__.py
from .log import logger from .log import logger
from .help import help
from .kroger import Kroger from .kroger import Kroger
from .kroger import Kroger from .kroger import Kroger
from .garfai import GarfAI from .garfai import GarfAI
@ -8,3 +9,4 @@ from .respond import GarfbotRespond
from .aod import aod_message from .aod import aod_message
from .qr import generate_qr from .qr import generate_qr
from .iputils import IPUtils from .iputils import IPUtils
from .weather import WeatherAPI

View File

@ -48,11 +48,11 @@ class GarfAI:
async with session.get(image_url) as resp: async with session.get(image_url) as resp:
if resp.status == 200: if resp.status == 200:
image_data = await resp.read() image_data = await resp.read()
ram_image = io.BytesIO(image_data) image = io.BytesIO(image_data)
ram_image.seek(0) image.seek(0)
timestamp = message.created_at.strftime("%Y%m%d%H%M%S") timestamp = message.created_at.strftime("%Y%m%d%H%M%S")
filename = f"{timestamp}_generated_image.png" filename = f"{timestamp}_generated_image.png"
sendfile = discord.File(fp=ram_image, filename=filename) sendfile = discord.File(fp=image, filename=filename)
try: try:
await message.channel.send(file=sendfile) await message.channel.send(file=sendfile)
except Exception as e: except Exception as e:

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,7 +1,6 @@
import discord import discord
import ipaddress import ipaddress
import subprocess import subprocess
from garfpy import logger
class IPUtils: class IPUtils:
@ -23,51 +22,52 @@ class IPUtils:
return True return True
return False return False
async def scan(self, message, user, guild, query): async def ping(self, ctx, target):
split = query.split()
target = split[-1]
if self.is_private(target): if self.is_private(target):
return return
if query.startswith("garfping "):
try: try:
logger.info( await ctx.send(f"`Pinging {target}...`")
f"Ping Request - User: {user}, Server: {guild}, Target: {target}"
)
await message.channel.send(f"`Pinging {target}...`")
result = subprocess.run( result = subprocess.run(
["ping", "-c", "4", target], capture_output=True, text=True ["ping", "-c", "4", target], capture_output=True, text=True
) )
embed = discord.Embed(title=f"Ping result: {target}", color=0x4D4D4D, description=f"```{result.stdout}```") embed = discord.Embed(
await message.channel.send(embed=embed) title=f"Ping result: {target}",
except Exception as e: color=discord.Color.light_gray(),
await message.channel.send(f"`GarfBot Error: {str(e)}`") description=f"```{result.stdout}```",
if query.startswith("garfdns "):
try:
logger.info(
f"NSLookup Request - User: {user}, Server: {guild}, Target: {target}"
) )
await message.channel.send(f"`Requesting {target}...`") await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"`GarfBot Error: {str(e)}`")
async def dns(self, ctx, target):
if self.is_private(target):
return
try:
await ctx.send(f"`Requesting {target}...`")
result = subprocess.run( result = subprocess.run(
["nslookup", target], capture_output=True, text=True ["nslookup", target], capture_output=True, text=True
) )
embed = discord.Embed(title=f"NSLookup result: {target}", color=0x4D4D4D, description=f"```{result.stdout}```") embed = discord.Embed(
await message.channel.send(embed=embed) title=f"NSLookup result: {target}",
except Exception as e: color=discord.Color.light_gray(),
await message.channel.send(f"`GarfBot Error: {str(e)}`") description=f"```{result.stdout}```",
if query.startswith("garfhack "):
try:
logger.info(
f"Nmap Request - User: {user}, Server: {guild}, Target: {target}"
) )
await message.channel.send(f"`Scanning {target}...`") await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"`GarfBot Error: {str(e)}`")
async def scan(self, ctx, target):
try:
await ctx.send(f"`Scanning {target}...`")
result = subprocess.run( result = subprocess.run(
["nmap", "-Pn", "-O", "-v", target], capture_output=True, text=True ["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 = discord.Embed(
title=f"Nmap scan result: {target}",
color=discord.Color.light_gray(),
description=f"```{result.stdout}```",
)
embed.set_footer(text="https://nmap.org/") embed.set_footer(text="https://nmap.org/")
await message.channel.send(embed=embed) await ctx.send(embed=embed)
except Exception as e: except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`") await ctx.send(f"`GarfBot Error: {str(e)}`")

View File

@ -47,7 +47,7 @@ async def generate_qr(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, # type: ignore
box_size=box_size, box_size=box_size,
border=4, border=4,
) )
@ -58,7 +58,7 @@ async def generate_qr(text):
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") # type: ignore
img_buffer.seek(0) img_buffer.seek(0)
return img_buffer return img_buffer

View File

@ -7,47 +7,47 @@ import re
class GarfbotRespond: class GarfbotRespond:
def __init__(self): def __init__(self):
self.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.guild_responses = json.load(f) self.garfbot_responses = json.load(f)
self.guild_responses = { self.garfbot_responses = {
int(k): v for k, v in self.guild_responses.items() int(k): v for k, v in self.garfbot_responses.items()
} }
total_responses = sum( total_responses = sum(
len(responses) for responses in self.guild_responses.values() len(responses) for responses in self.garfbot_responses.values()
) )
logger.info( logger.info(
f"Loaded responses for {len(self.guild_responses)} server(s), ({total_responses} total responses)" 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.guild_responses = {} self.garfbot_responses = {}
else: else:
self.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.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( total_responses = sum(
len(responses) for responses in self.guild_responses.values() len(responses) for responses in self.garfbot_responses.values()
) )
logger.info( logger.info(
f"Saved responses for {len(self.guild_responses)} servers ({total_responses} total responses)" 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.guild_responses: if guild_id not in self.garfbot_responses:
self.guild_responses[guild_id] = {} self.garfbot_responses[guild_id] = {}
return self.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
@ -96,7 +96,7 @@ class GarfbotRespond:
responses = self.get_responses(guild_id) responses = self.get_responses(guild_id)
responses[trigger] = response_text responses[trigger] = response_text
self.guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed(title="✅ Auto-response Added.", color=0x00FF00) embed = discord.Embed(title="✅ Auto-response Added.", color=0x00FF00)
@ -112,7 +112,7 @@ class GarfbotRespond:
if trigger in responses: if trigger in responses:
removed_response = responses[trigger] removed_response = responses[trigger]
del responses[trigger] del responses[trigger]
self.guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B) embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
@ -127,7 +127,7 @@ class GarfbotRespond:
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.guild_responses[guild_id] = responses self.garfbot_responses[guild_id] = responses
self.save_responses() self.save_responses()
embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B) embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)

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}
params = location.split()
if len(params) == 1:
return {"q": f"{params[0]},US"}
elif len(params) == 2:
city, second = params
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(params) == 3:
city, state, country = params
return {"q": f"{city},{state},{country.upper()}"}
else:
if len(params[-1]) == 2:
city_parts = params[:-1]
country = params[-1]
city_name = " ".join(city_parts)
return {"q": f"{city_name},{country.upper()}"}
elif len(params) >= 2 and len(params[-1]) == 2 and len(params[-2]) <= 2:
city_parts = params[:-2]
state = params[-2]
country = params[-1]
city_name = " ".join(city_parts)
return {"q": f"{city_name},{state},{country.upper()}"}
else:
city_name = " ".join(params)
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