Merge remote-tracking branch 'origin/main' into weather

This commit is contained in:
2025-06-06 18:37:23 -05:00
15 changed files with 718 additions and 353 deletions

View File

@ -22,7 +22,7 @@ jobs:
docker stop garfbot
docker rm garfbot
docker build -t git.crate.zip/crate/garfbot:latest .
docker run -d --restart always -v $PWD:/usr/src/app --name garfbot git.crate.zip/crate/garfbot:latest
docker compose up -d -p garfbot
else
docker restart garfbot
fi

3
.gitignore vendored
View File

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

View File

@ -1,19 +1,29 @@
FROM python:3.11.10-bookworm
FROM python:3.11.10-alpine
WORKDIR /usr/src/app
RUN apt update
RUN apt install -y iputils-ping
RUN apt install -y dnsutils
RUN apt install -y nmap
RUN apt install -y python3
RUN apt install -y python3-pip
RUN pip3 install discord
RUN pip3 install openai
RUN pip3 install aiohttp
RUN pip3 install requests
RUN pip3 install wikipedia
RUN pip3 install pillow
RUN pip3 install qrcode
RUN apk update && \
apk add --no-cache \
iputils \
bind-tools \
nmap \
gcc \
musl-dev \
jpeg-dev \
zlib-dev \
freetype-dev \
lcms2-dev \
openjpeg-dev \
tiff-dev \
tk-dev \
tcl-dev
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)
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}`
Responds with text.
<br>Responds with text.
`garfpic {prompt}`
Responds with an image.
<br>Responds with an image.
`garfping {target}`
Responds with iputils-ping result from target.
<br>Responds with iputils-ping result from target.
`garfpic {target}`
Responds with dns lookup result from target.
`garfdns {target}`
<br>Responds with dns lookup result from target.
`garfhack {target}`
Responds with nmap scan result from target.
<br>Responds with nmap scan result from target.
`garfshop {item} {zip}`
Responds with 10 grocery {item}s from the nearest Kroger location, listed from least to most expensive.
<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
======
@ -38,7 +57,8 @@ GARFBOT_TOKEN = "Discord API token"
OPENAI_TOKEN = "OpenAI API token"
```
I recommend building a docker image using the included DockerFile as a template. Run the container binding /usr/src/app to GarfBot's CWD:
I recommend building a docker image using the included DockerFile as a template.
<br>Run the container binding /usr/src/app to GarfBot's CWD:
```console
$ docker build -t garfbot .
@ -53,7 +73,7 @@ If you prefer to install dependencies on you own host and run as a systemd servi
$ sudo nano /etc/systemd/system/garfbot.service
```
Replace {user} with your username:
Replace $USER with your username:
```console
[Unit]
@ -63,8 +83,8 @@ After=multi-user.target
[Service]
Type=simple
Restart=always
User={user}
WorkingDirectory=/home/{user}/garfbot
User=$USER
WorkingDirectory=/home/$USER/garfbot
ExecStart=/usr/bin/python garfbot.py
[Install]

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,16 @@
import config
import asyncio
import discord
import subprocess
from garfpy import (
logger, is_private,
kroger_token, find_store, search_product,
garfpic, process_image_requests, generate_chat,
aod_message, wikisum, generate_qr)
logger,
IPUtils,
aod_message,
generate_qr,
Kroger,
GarfAI,
GarfbotRespond,
)
gapikey = config.GIF_TOKEN
@ -21,12 +24,20 @@ intents.messages = True
intents.message_content = True
garfbot = discord.Client(intents=intents)
garf_respond = GarfbotRespond()
garfield = GarfAI()
iputils = IPUtils()
kroger = Kroger()
@garfbot.event
async def on_ready():
try:
asyncio.create_task(process_image_requests())
logger.info(f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}.")
garf_respond.load_responses()
asyncio.create_task(garfield.process_image_requests())
logger.info(
f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}."
)
except Exception as e:
logger.error(e)
@ -36,31 +47,27 @@ async def on_message(message):
if message.author == garfbot.user:
return
if message.content.lower().startswith("hey garfield") or isinstance(message.channel, discord.DMChannel):
user = message.author.name
server = message.guild.name if message.guild else "Direct Message"
question = message.content[12:] if message.content.lower().startswith("hey garfield") else message.content
answer = await generate_chat(question)
logger.info(f"Chat Request - User: {user}, Server: {server}, Prompt: {question}")
await message.channel.send(answer)
content = message.content.strip()
lower = content.lower()
user_name = message.author.name
guild_id = message.guild.id
guild_name = message.guild.name if message.guild else "Direct Message"
if message.content.lower().startswith('garfpic '):
user = message.author.name
server = message.guild.name if message.guild else "Direct Message"
prompt = message.content[8:]
logger.info(f"Image Request - User: {user}, Server: {server}, Prompt: {prompt}")
await message.channel.send(f"`Please wait... image generation queued: {prompt}`")
await garfpic(message, prompt)
# IP utils
if message.guild and lower.startswith(("garfping ", "garfdns ", "garfhack ")):
await iputils.scan(message, user_name, guild_name, lower)
if message.content.lower().startswith('garfwiki '):
search_term = message.content[9:]
summary = await wikisum(search_term)
# Wikipedia
if lower.startswith("garfwiki "):
query = message.content[9:]
summary = await garfield.wikisum(query)
await message.channel.send(summary)
if message.content.lower().startswith('garfqr '):
# QR codes
if lower.startswith("garfqr "):
text = message.content[7:]
if len(text) > 1000:
await mesage.channel.send("❌ Text too long! Maximum 1000 characters.")
await message.channel.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
@ -70,83 +77,114 @@ async def on_message(message):
logger.error(e)
await message.channel.send(e)
if message.content.lower().startswith("garfping "):
try:
query = message.content.split()
user = message.author.name
server = message.guild.name if message.guild else "Direct Message"
target = query[-1]
logger.info(f"Ping Request - User: {user}, Server: {server}, Target: {target}")
if is_private(target):
rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
await message.channel.send(rejection)
else:
result = subprocess.run(['ping', '-c', '4', target], capture_output=True, text=True)
await message.channel.send(f"`Ping result for {target}:`\n```\n{result.stdout}\n```")
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
if message.content.lower().startswith("garfdns "):
try:
query = message.content.split()
user = message.author.name
server = message.guild.name if message.guild else "Direct Message"
target = query[-1]
logger.info(f"NSLookup Request - User: {user}, Server: {server}, Target: {target}")
if is_private(target):
rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
await message.channel.send(rejection)
else:
result = subprocess.run(['nslookup', target], capture_output=True, text=True)
await message.channel.send(f"`NSLookup result for {target}:`\n```\n{result.stdout}\n```")
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
if message.content.lower().startswith("garfhack "):
try:
query = message.content.split()
user = message.author.name
server = message.guild.name if message.guild else "Direct Message"
target = query[-1]
logger.info(f"Nmap Request - User: {user}, Server: {server}, Target: {target}")
if is_private(target):
rejection = await generate_chat("Hey Garfield, explain to me why I am dumb for trying to hack your private computer network.")
await message.channel.send(rejection)
else:
await message.channel.send(f"`Scanning {target}...`")
result = subprocess.run(['nmap', '-Pn', '-O', '-v', target], capture_output=True, text=True)
await message.channel.send(f"`Ping result for {target}:`\n```\n{result.stdout}\n```")
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
# Kroger Shopping
if message.content.lower().startswith("garfshop "):
if lower.startswith("garfshop "):
try:
kroken = kroger_token()
kroger_query = message.content.split()
product = " ".join(kroger_query[1:-1])
zipcode = kroger_query[-1]
loc_data = find_store(zipcode, kroken)
loc_id = loc_data['data'][0]['locationId']
store_name = loc_data['data'][0]['name']
product_query = search_product(product, loc_id, kroken)
products = product_query['data']
sorted_products = sorted(products, key=lambda item: item['items'][0]['price']['regular'])
response = f"Prices for `{product}` at `{store_name}` near `{zipcode}`:\n"
for item in sorted_products:
product_name = item['description']
price = item['items'][0]['price']['regular']
response += f"- `${price}`: {product_name} \n"
query = message.content[9:]
response = kroger.garfshop(query)
await message.channel.send(response)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
# Army of Dawn Server only!!
if message.guild and message.guild.id == 719605634772893757:
# Chats & pics
elif lower.startswith("hey garfield") or isinstance(
message.channel, discord.DMChannel
):
prompt = content[12:] if lower.startswith("hey garfield") else message.content
answer = await garfield.generate_chat(prompt)
logger.info(
f"Chat Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}"
)
await message.channel.send(answer)
elif lower.startswith("garfpic "):
prompt = content[8:]
logger.info(
f"Image Request - User: {user_name}, Server: {guild_name}, Prompt: {prompt}"
)
await message.channel.send(
f"`Please wait... image generation queued: {prompt}`"
)
await garfield.garfpic(message, prompt)
# GarfBot help
elif lower.strip() == "garfbot help":
embed = discord.Embed(title="**Need help?**", color=0x4D4D4D)
embed.add_field(
name="hey garfield `prompt`", value="*Responds with text.*", inline=True
)
embed.add_field(
name="garfpic `prompt`", value="*Responds with an image.*", inline=True
)
embed.add_field(
name="garfping `target`",
value="*Responds with iputils-ping result from target.*",
inline=True,
)
embed.add_field(
name="garfdns `target`",
value="*Responds with dns lookup result from target.*",
inline=True,
)
embed.add_field(
name="garfhack `target`",
value="*Responds with nmap scan result from target.*",
inline=True,
)
embed.add_field(
name="garfwiki `query`",
value="*Garfbot looks up a wikipedia article and will summarize it for you.*",
inline=True,
)
embed.add_field(
name="garfshop `item` `zip`",
value="*Responds with 10 grocery items from the nearest Kroger location, cheapest first.*",
inline=True,
)
embed.add_field(
name="garfqr `text`",
value="*Create a QR code for any string up to 1000 characters.*",
inline=True,
)
embed.add_field(
name="garfbot response `add` `trigger` `response`",
value='*Add a GarfBot auto response for your server. Use "quotes" if you like.*',
inline=True,
)
embed.add_field(
name="garfbot response `remove` `trigger`",
value="*Remove a GarfBot auto response for your server.*",
inline=True,
)
embed.add_field(
name="garfbot response `list`",
value="*List current GarfBot auto responses for your server.*",
inline=True,
)
embed.add_field(
name="garfbot help", value="*Show a list of these commands.*", inline=True
)
await message.channel.send(embed=embed)
# Army of Dawn Server only!!
elif message.guild and message.guild.id == 719605634772893757:
await aod_message(garfbot, message)
# Auto-responses
elif message.guild:
responses = garf_respond.get_responses(guild_id)
if lower.startswith("garfbot response "):
await garf_respond.garfbot_response(message, content)
return
for trigger, response in responses.items():
if trigger.lower() in lower:
await message.channel.send(response)
break
# Run GarfBot
async def garfbot_connect():
while True:
try:
@ -156,5 +194,6 @@ async def garfbot_connect():
logger.error(f"Garfbot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(garfbot_connect())

View File

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

View File

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

View File

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

View File

@ -1,6 +1,11 @@
import discord
import ipaddress
import subprocess
from garfpy import logger
def is_private(target):
class IPUtils:
def is_private(self, target):
try:
ip_obj = ipaddress.ip_address(target)
if ip_obj.is_private:
@ -17,3 +22,64 @@ def is_private(target):
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
client_id = config.CLIENT_ID
client_secret = config.CLIENT_SECRET
class Kroger:
def __init__(self):
self.client_id = config.CLIENT_ID
self.client_secret = config.CLIENT_SECRET
self.auth = b64encode(
f"{self.client_id}:{self.client_secret}".encode()
).decode()
auth = b64encode(f"{client_id}:{client_secret}".encode()).decode()
def kroger_token():
def kroger_token(self):
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Basic {auth}'
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {self.auth}",
}
response = requests.post('https://api.kroger.com/v1/connect/oauth2/token', headers=headers, data={
'grant_type': 'client_credentials',
'scope': 'product.compact'
})
response = requests.post(
"https://api.kroger.com/v1/connect/oauth2/token",
headers=headers,
data={"grant_type": "client_credentials", "scope": "product.compact"},
)
response.raise_for_status()
return response.json()['access_token']
return response.json()["access_token"]
def find_store(zipcode, kroken):
def find_store(self, zipcode, kroken):
headers = {
'Authorization': f'Bearer {kroken}',
"Authorization": f"Bearer {kroken}",
}
params = {
'filter.zipCode.near': zipcode,
'filter.limit': 1,
"filter.zipCode.near": zipcode,
"filter.limit": 1,
}
response = requests.get('https://api.kroger.com/v1/locations', headers=headers, params=params)
response = requests.get(
"https://api.kroger.com/v1/locations", headers=headers, params=params
)
return response.json()
def search_product(product, loc_id, kroken):
def search_product(self, product, loc_id, kroken):
logger.info(f"Searching for {product}...")
headers = {
'Authorization': f'Bearer {kroken}',
"Authorization": f"Bearer {kroken}",
}
params = {
'filter.term': product,
'filter.locationId': loc_id,
'filter.limit': 10
"filter.term": product,
"filter.locationId": loc_id,
"filter.limit": 10,
}
response = requests.get('https://api.kroger.com/v1/products', headers=headers, params=params)
response = requests.get(
"https://api.kroger.com/v1/products", headers=headers, params=params
)
return response.json()
def garfshop(self, query):
try:
query = query.split()
kroken = self.kroger_token()
product = query[-2]
zipcode = query[-1]
loc_data = self.find_store(zipcode, kroken)
loc_id = loc_data["data"][0]["locationId"]
store_name = loc_data["data"][0]["name"]
product_query = self.search_product(product, loc_id, kroken)
products = product_query["data"]
sorted_products = sorted(
products, key=lambda item: item["items"][0]["price"]["regular"]
)
response = f"Prices for `{product}` at `{store_name}` near `{zipcode}`:\n"
for item in sorted_products:
product_name = item["description"]
price = item["items"][0]["price"]["regular"]
response += f"- `${price}`: {product_name} \n"
return response
except Exception as e:
return e

View File

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

View File

@ -41,6 +41,7 @@ def calculate_qr_settings(text):
return version, box_size
async def generate_qr(text):
version, box_size = calculate_qr_settings(text)
@ -57,7 +58,7 @@ async def generate_qr(text):
qr_image = qr.make_image(fill_color="black", back_color="white")
img_buffer = BytesIO()
qr_image.save(img_buffer, format='PNG')
qr_image.save(img_buffer, format="PNG")
img_buffer.seek(0)
return img_buffer

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

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