Compare commits

..

73 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
39b821ff7f fix regex hopefully
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 5s
2025-06-04 03:14:25 -05:00
d9ac0cc36e some cleanup etc
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-04 02:55:49 -05:00
d707398c26 some cleanup on main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 20s
2025-06-04 02:33:37 -05:00
0cb5f8fa38 implement auto response
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-04 02:22:19 -05:00
d70584d4f8 log test
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-03 20:57:39 -05:00
49831aeb47 response log
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 15s
2025-06-03 20:55:41 -05:00
4226ce86c8 fix GarfbotRespond instance
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 14s
2025-06-03 20:41:49 -05:00
b4ca6b778d add auto responses
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
2025-06-03 20:33:55 -05:00
4b077e3fd9 weather 2025-06-03 17:54:40 -05:00
9d5765c492 fix gitea deploy pipeline
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
2025-06-02 17:59:32 -05:00
8216af5f2a deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:52:24 -05:00
219b1745da deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 2s
2025-06-02 13:51:46 -05:00
d570069e6b test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:51:06 -05:00
f267526411 deploy test
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 3s
2025-06-02 13:49:49 -05:00
86fa47d259 remove docker socket mount
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 5s
2025-06-02 13:47:57 -05:00
888e647e1f add gitea deploy script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 1s
2025-06-02 13:06:23 -05:00
c1beb8374e fix qr sendfile 2025-05-25 04:15:08 -05:00
48a7033ba6 add pillow 2025-05-25 04:11:57 -05:00
9e436d8d78 fix qrcode call async 2025-05-25 04:08:18 -05:00
d8c286bb2d add qrcode 2025-05-25 04:04:32 -05:00
a081edfa97 add qr code gen 2025-05-25 04:02:34 -05:00
716e30e2b1 fix chat gen 2025-05-23 20:46:26 -05:00
73499a9586 oops 2025-05-23 13:35:19 -05:00
e3a762fe10 await chat gen 2025-05-23 13:33:02 -05:00
842b592bfc garfsum 2025-05-23 13:30:36 -05:00
f9f88be8af wiki except 2025-05-23 11:41:26 -05:00
5c8696216e fix wiki init 2025-05-22 17:12:56 -05:00
aa16064d17 fix wikisum import 2025-05-22 17:11:39 -05:00
ffd642fd2a add wiki stuff 2025-05-22 17:09:48 -05:00
3945eb6763 add wiki 2025-05-22 16:53:37 -05:00
0438b55ce6 remove tenor gif bs 2025-05-22 16:46:22 -05:00
8fb4e089be Update README.md 2024-12-28 05:08:50 +00:00
b92356a42d Update README.md 2024-10-25 11:13:34 -05:00
9b26aa113b Update README.md 2024-10-21 11:40:51 -05:00
b29ed09f71 update garfbot.sh 2024-10-21 01:51:43 -05:00
81233011b0 fix jonbot and moneybot 2024-10-21 01:39:42 -05:00
99b9680ef2 Update garfbot.sh 2024-10-17 17:24:34 +00:00
ff812f0afe Update README.md 2024-10-05 18:23:31 +00:00
6fb9268bc3 Update README.md 2024-10-05 18:22:37 +00:00
0d849f71c2 Merge branch 'refactor' into 'main'
Refactor

See merge request crate/garfbot!3
2024-10-05 16:50:19 +00:00
b9a7dd298c Refactor 2024-10-05 16:50:19 +00:00
47da73aec6 small changes 2024-10-03 03:06:36 +00:00
2a367c321e small changes 2024-10-03 01:46:17 +00:00
7b00e844fd image change 2024-10-03 01:44:19 +00:00
108471edfd delete image locally after send 2024-10-03 01:10:10 +00:00
de27f5bb62 forgot to await garfbot 2024-10-02 02:33:03 +00:00
21 changed files with 1242 additions and 488 deletions

View File

@ -0,0 +1,27 @@
name: Garfbot CI/CD Deployment
on:
push:
branches: [ main ]
jobs:
Deploy:
container:
volumes:
- /home/crate/garfbot:/workspace/crate/garfbot/garfbot
steps:
- name: Pull GarfBot and re-deploy
run: |
cd /workspace/crate/garfbot/garfbot
git pull origin main
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
docker build -t git.crate.zip/crate/garfbot:latest .
docker compose up -d
else
docker restart garfbot
fi

11
.gitignore vendored
View File

@ -1,6 +1,11 @@
config.py config.py
/__pycache__/* __pycache__/
/images/* garfpy/__pycache__/
*.venv*
*.json *.json
*.old *.old
*.log* *.log*
meows.py
meow_counts.json
user_stats.json
responses.json

View File

@ -1,16 +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 \
lcms2-dev \
openjpeg-dev \
tiff-dev \
tk-dev \
tcl-dev
CMD [ "python", "garfbot.py" ] RUN pip3 install --no-cache-dir \
discord \
openai \
aiohttp \
requests \
wikipedia \
pillow \
qrcode
CMD [ "python", "garfmain.py" ]

101
README.md
View File

@ -1,43 +1,80 @@
GarfBot is a discord bot that uses openai generative pre-trained AI models to produce text and images for your personal entertainment and companionship. Who is GarfBot?
======
![garfield](https://www.crate.zip/garfield.png)
JonBot is his owner and MoneyBot is his eccentric friend who plays the classic F2P video game Planetside 2. 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:
To interact: (not case-sensitive) `hey garfield {prompt}`
<br>Responds with text.
`"hey garfield"` `garfpic {prompt}`
responds with text. <br>Responds with an image.
`"garfpic"` `garfping {target}`
responds with image. <br>Responds with iputils-ping result from target.
`"hey jon"` `garfdns {target}`
responds with text. <br>Responds with dns lookup result from target.
`"hey money"` `garfhack {target}`
responds with text. <br>Responds with nmap scan result from target.
To get started, clone this repo and create a `config.py` file in GarfBot's root directory. Open your favorite text editor or IDE and add your various API tokens as such: `garfshop {item} {zip}`
<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
======
To get started, clone this repo and create a config file.
```console
$ git clone https://git.crate.zip/crate/garfbot.git && cd garfbot/ && nano config.py
```
Add your various API tokens:
```python ```python
GARFBOT_TOKEN = "token" GARFBOT_TOKEN = "Discord API token"
JONBOT_TOKEN = "token" OPENAI_TOKEN = "OpenAI API token"
MONEYBOT_TOKEN = "token"
OPENAI_TOKEN = "token"
GIF_TOKEN = "token"
``` ```
If you want to configure a more secure setup, go ahead.
Next, I recommend building a docker image for each bot using the included DockerFile as a template. Run each 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:
A terraform file has been included to launch all three containers, or you can do it manually.
Example:
```console ```console
crate@raspberrypi:~/garfbot $ docker build -t garfbot . $ docker build -t garfbot .
crate@raspberrypi:~/garfbot $ docker run -d --restart always -v $PWD:/usr/src/app --name garfbot garfbot $ docker run -d --restart always -v $PWD:/usr/src/app --name garfbot garfbot
``` ```
Or if you prefer to install dependencies (from requirements.txt) on you own host and run as a systemd service: In case you'd rather not do it manually, a `garfbot.tf` file has been included to launch GarfBot and his friends' containers.
If you prefer to install dependencies on you own host and run as a systemd service:
```console
$ sudo nano /etc/systemd/system/garfbot.service
```
Replace $USER with your username:
```console ```console
[Unit] [Unit]
Description=garfbot Description=garfbot
@ -46,10 +83,18 @@ After=multi-user.target
[Service] [Service]
Type=simple Type=simple
Restart=always Restart=always
User=pi User=$USER
WorkingDirectory=/home/crate/garfbot WorkingDirectory=/home/$USER/garfbot
ExecStart=/usr/bin/python ./garfbot.py ExecStart=/usr/bin/python garfbot.py
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
And finally:
```console
$ sudo systemctl daemon-reload
$ sudo systemctl enable garfbot
$ sudo systemctl start garfbot
```

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,433 +0,0 @@
import os
import json
import config
import random
import openai
import logging
import aiohttp
import asyncio
import discord
import requests
import ipaddress
import subprocess
from base64 import b64encode
from openai import AsyncOpenAI
from datetime import datetime
from collections import defaultdict
from operator import itemgetter
from logging.handlers import TimedRotatingFileHandler
# Log setup
logger = logging.getLogger('garflog')
logger.setLevel(logging.INFO)
file_handler = TimedRotatingFileHandler(
'garfbot.log',
when='midnight',
interval=1,
backupCount=7,
delay=True # Counterintuitively, will flush output immediately
)
console_handler = logging.StreamHandler()
formatter=logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Bot Setup
openaikey = config.OPENAI_TOKEN
gapikey = config.GIF_TOKEN
garfkey = config.GARFBOT_TOKEN
txtmodel = "gpt-4o-mini"
imgmodel = "dall-e-3"
intents = discord.Intents.default()
intents.members = True
intents.messages = True
intents.message_content = True
garfbot = discord.Client(intents=intents)
# Network Utils Setup
def is_private(target):
try:
ip_obj = ipaddress.ip_address(target)
if ip_obj.is_private:
return True
except ValueError:
if "crate.lan" in target.lower():
return True
if "crate.zip" in target.lower():
return True
if "memtec.org" in target.lower():
return True
if "crateit.net" in target.lower():
return True
if "garfbot.art" in target.lower():
return True
return False
# Kroger Setup
client_id = config.CLIENT_ID
client_secret = config.CLIENT_SECRET
auth = b64encode(f"{client_id}:{client_secret}".encode()).decode()
def kroger_token():
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Basic {auth}'
}
response = requests.post('https://api.kroger.com/v1/connect/oauth2/token', headers=headers, data={
'grant_type': 'client_credentials',
'scope': 'product.compact'
})
response.raise_for_status()
return response.json()['access_token']
def find_store(zipcode, kroken):
headers = {
'Authorization': f'Bearer {kroken}',
}
params = {
'filter.zipCode.near': zipcode,
'filter.limit': 1,
}
response = requests.get('https://api.kroger.com/v1/locations', headers=headers, params=params)
return response.json()
def search_product(product, loc_id, kroken):
headers = {
'Authorization': f'Bearer {kroken}',
}
params = {
'filter.term': product,
'filter.locationId': loc_id,
'filter.limit': 10
}
response = requests.get('https://api.kroger.com/v1/products', headers=headers, params=params)
return response.json()
# Meows Json Handling
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:
return json.load(f)
else:
return default
meow_counts = defaultdict(int, json_load(meows_file, {}))
user_stats = json_load(stats_file, {})
# GarfBot Ready!
@garfbot.event
async def on_ready():
asyncio.create_task(process_image_requests()) # Important!
logger.info(f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}.")
print(f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}.", flush=True)
# GarfChats
async def generate_chat_response(question):
try:
client = AsyncOpenAI(api_key = openaikey)
response = await client.chat.completions.create(
model=txtmodel,
messages=[
{"role": "system", "content": "Pretend you are sarcastic Garfield."},
{"role": "user", "content": f"{question}"}
],
max_tokens=400
)
answer = response.choices[0].message.content
return answer.replace("an AI language model", "a cartoon animal")
except openai.BadRequestError as e:
return f"`GarfBot Error: {e}`"
except openai.APIError as e:
print(e, flush=True)
return f"`GarfBot Error: Monday`"
except Exception as e:
print(e, flush=True)
return f"`GarfBot Error: Lasagna`"
# GarfPics
async def generate_image(prompt):
try:
client = AsyncOpenAI(api_key = openaikey)
response = await client.images.generate(
model=imgmodel,
prompt=prompt,
n=1,
size="1024x1024"
)
image_url = response.data[0].url
return image_url
except openai.BadRequestError as e:
return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
except openai.InternalServerError as e:
logger.error(e)
print(e, flush=True)
return f"`GarfBot Error: ({e.status_code}) - Monday`"
except Exception as e:
logger.error(e)
print(e, flush=True)
return f"`GarfBot Error: Lasagna`"
image_request_queue = asyncio.Queue()
async def process_image_requests():
async with aiohttp.ClientSession() as session:
while True:
request = await image_request_queue.get()
message = request['message']
prompt = request['prompt']
image_url = await generate_image(prompt)
if "GarfBot Error" not in image_url:
async with session.get(image_url) as resp:
if resp.status == 200:
image_data = await resp.read()
timestamp = message.created_at.strftime('%Y%m%d%H%M%S')
save_filename = f"images/{timestamp}_generated_image.png"
send_filename = f"{timestamp}_generated_image.png" # There is probably a better way to do this.
with open(save_filename, "wb") as f:
f.write(image_data)
with open(save_filename, "rb") as f:
await message.channel.send(file=discord.File(f, send_filename))
else:
await message.channel.send("`GarfBot Error: Odie`")
else:
await message.channel.send(image_url)
image_request_queue.task_done()
await asyncio.sleep(2)
# Message Listener
@garfbot.event
async def on_message(message):
if message.author == garfbot.user:
return
if message.content.lower().startswith("hey garfield") or isinstance(message.channel, discord.DMChannel):
question = message.content[12:] if message.content.lower().startswith("hey garfield") else message.content
answer = await generate_chat_response(question)
await message.channel.send(answer)
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}")
print(f"Image Request - User: {user}, Server: {server}, Prompt: {prompt}", flush=True)
await message.channel.send(f"`Please wait... image generation queued: {prompt}`")
await image_request_queue.put({'message': message, 'prompt': prompt})
if message.content.lower() == "lasagna":
await send_gif(message, "garfield lasagna")
if message.content.lower() == "monday":
await send_gif(message, "garfield monday")
if message.content.lower().startswith("garfgif "):
search_term = message.content[8:]
await send_gif(message, search_term)
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]
print(f"Ping Request - User: {user}, Server: {server}, Target: {target}", flush=True)
if is_private(target):
rejection = await generate_chat_response("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]
print(f"NSLookup Request - User: {user}, Server: {server}, Target: {target}", flush=True)
if is_private(target):
rejection = await generate_chat_response("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]
print(f"Nmap Request - User: {user}, Server: {server}, Target: {target}", flush=True)
if is_private(target):
rejection = await generate_chat_response("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 "):
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"
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:
if "meow" in message.content.lower():
logger.info(f"Meow detected! {message.author.name} said: {message.content}")
print(f"Meow detected! {message.author.name} said: {message.content}", flush=True)
meow_counts[str(message.author.id)] += 1
with open(meows_file, "w") as f:
json.dump(dict(meow_counts), f)
if message.content.lower() == "meowcount":
response = f"My records show that <@{message.author.id}> has meowed {meow_counts[str(message.author.id)]} time(s). Have a nice day."
await message.channel.send(response)
if message.content.lower() == "top meowers":
top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[:10]
embed = discord.Embed(title="Top Meowers :cat:", color=0x000000)
for i, (user_id, meow_count) in enumerate(top_meowers):
user = await garfbot.fetch_user(int(user_id))
embed.add_field(name=f"{i+1}. {user.name}", value=f"{meow_count} meows", inline=False)
await message.channel.send(embed=embed)
if message.content.lower() == "checking in":
user_id = str(message.author.id)
if user_id in user_stats and user_stats[user_id]["check_in_time"] is not None:
await message.channel.send(f"{message.author.mention} You are already checked in. Please check out first.")
return
check_in_time = datetime.utcnow().timestamp()
if user_id not in user_stats:
user_stats[user_id] = {"check_ins": 0, "total_time": 0, "check_in_time": None}
user_stats[user_id]["check_in_time"] = check_in_time
await message.channel.send(f"{message.author.mention} You have been checked in. Please mute your microphone.")
elif message.content.lower() == "checking out":
user_id = str(message.author.id)
if user_id not in user_stats or user_stats[user_id]["check_in_time"] is None:
await message.channel.send(f"{message.author.mention} You have not checked in yet. Please check in first.")
return
check_out_time = datetime.utcnow().timestamp()
check_in_time = user_stats[user_id]["check_in_time"]
time_delta = check_out_time - check_in_time
user_stats[user_id]["check_ins"] += 1
user_stats[user_id]["total_time"] += time_delta
user_stats[user_id]["check_in_time"] = None
with open("user_stats.json", "w") as f:
json.dump(user_stats, f)
await message.channel.send(f"{message.author.mention} You have been checked out. Your session was {time_delta:.2f} seconds.")
elif message.content.lower() == "stats":
stats_embed = discord.Embed(title="User stats :trophy:", color=0x000000)
sorted_user_stats = sorted(user_stats.items(), key=lambda x: x[1]["total_time"], reverse=True)
table_rows = [["Name", "Check-ins", "Total Time"]]
for user_id, stats in sorted_user_stats:
if stats["check_in_time"] is None:
total_time_seconds = stats["total_time"]
hours, total_time_seconds = divmod(total_time_seconds, 3600)
minutes, total_time_seconds = divmod(total_time_seconds, 60)
seconds, fractions = divmod(total_time_seconds, 1)
fractions_str = f"{fractions:.3f}"[2:]
username = garfbot.get_user(int(user_id)).name
table_rows.append([username, str(stats["check_ins"]), f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms"])
else:
username = garfbot.get_user(int(user_id)).name
table_rows.append([username, "Currently checked in", "-"])
table_columns = list(zip(*table_rows[1:]))
table_fields = table_rows[0]
for field, values in zip(table_fields, table_columns):
stats_embed.add_field(name=field, value="\n".join(values), inline=True)
await message.channel.send(embed=stats_embed)
# GarfGifs
@garfbot.event
async def send_gif(message, search_term):
lmt = 50
ckey = "garfbot"
r = requests.get(f"https://tenor.googleapis.com/v2/search?q={search_term}&key={gapikey}&client_key={ckey}&limit={lmt}")
if r.status_code == 200:
top_50gifs = json.loads(r.content)
gif_url = random.choice(top_50gifs["results"])["itemurl"]
print(gif_url)
try:
await message.channel.send(gif_url)
except KeyError:
await message.channel.send("Oops, something went wrong.")
else:
await message.channel.send(f"`Oops, something went wrong. Error code: {r.status_code}`")
# discord.py Error Handling
@garfbot.event
async def on_error(event, *args, **kwargs):
logger.error(f'GarfBot Error: {event}')
print(f'GarfBot Error: {event}', flush=True)
# Run GarfBot!
async def garfbot_connect():
while True:
try:
garfbot.start(garfkey)
except Exception as e:
e = str(e)
logger.error(f"Garfbot couldn't connect! {e}")
print(f"Garfbot couldn't connect! {e}", flush=True)
await asyncio.sleep(300)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(garfbot_connect())

View File

@ -1 +1,3 @@
docker run -d --restart always -v $PWD:/usr/src/app --name garfbot garfbot #!/bin/bash
docker run -d --restart always -v $PWD:/usr/src/app --name garfbot abcrate/garfbot

153
garfmain.py Normal file
View File

@ -0,0 +1,153 @@
import config
import asyncio
import discord
from garfpy import (
help,
logger,
IPUtils,
aod_message,
generate_qr,
Kroger,
GarfAI,
GarfbotRespond,
WeatherAPI,
)
gapikey = config.GIF_TOKEN
garfkey = config.GARFBOT_TOKEN
txtmodel = config.TXT_MODEL
imgmodel = config.IMG_MODEL
intents = discord.Intents.default()
intents.members = True
intents.messages = True
intents.message_content = True
garfbot = discord.Client(intents=intents)
garf_respond = GarfbotRespond()
garfield = GarfAI()
iputils = IPUtils()
kroger = Kroger()
weather = WeatherAPI()
@garfbot.event
async def on_ready():
try:
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)
@garfbot.event
async def on_message(message):
if message.author == garfbot.user:
return
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"
# 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
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!!
elif message.guild and message.guild.id == 719605634772893757:
await aod_message(garfbot, message)
# Auto-responses
elif message.guild:
responses = garf_respond.get_responses(guild_id)
if lower.startswith("garfbot response "):
await garf_respond.garfbot_response(message, content)
return
for trigger, response in responses.items():
if trigger.lower() in lower:
await message.channel.send(response)
break
# Run GarfBot
async def garfbot_connect():
while True:
try:
await garfbot.start(garfkey)
except Exception as e:
e = str(e)
logger.error(f"Garfbot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(garfbot_connect())

12
garfpy/__init__.py Normal file
View File

@ -0,0 +1,12 @@
# garfpy/__init__.py
from .log import logger
from .help import help
from .kroger import Kroger
from .kroger import Kroger
from .garfai import GarfAI
from .respond import GarfbotRespond
from .aod import aod_message
from .qr import generate_qr
from .iputils import IPUtils
from .weather import WeatherAPI

123
garfpy/aod.py Normal file
View File

@ -0,0 +1,123 @@
import os
import json
import discord
from garfpy import logger
from datetime import datetime
from operator import itemgetter
from collections import defaultdict
# Meows Json Handling
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:
return json.load(f)
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}")
meow_counts[str(message.author.id)] += 1
with open(meows_file, "w") as f:
json.dump(dict(meow_counts), f)
if message.content.lower() == "meowcount":
response = f"My records show that <@{message.author.id}> has meowed {meow_counts[str(message.author.id)]} time(s). Have a nice day."
await message.channel.send(response)
if message.content.lower() == "top meowers":
top_meowers = sorted(meow_counts.items(), key=itemgetter(1), reverse=True)[
:10
]
embed = discord.Embed(title="Top Meowers :cat:", color=0x000000)
for i, (user_id, meow_count) in enumerate(top_meowers):
user = await garfbot.fetch_user(int(user_id))
embed.add_field(
name=f"{i + 1}. {user.name}",
value=f"{meow_count} meows",
inline=False,
)
await message.channel.send(embed=embed)
if message.content.lower() == "checking in":
user_id = str(message.author.id)
if user_id in user_stats and user_stats[user_id]["check_in_time"] is not None:
await message.channel.send(
f"{message.author.mention} You are already checked in. Please check out first."
)
return
check_in_time = datetime.now().timestamp()
if user_id not in user_stats:
user_stats[user_id] = {
"check_ins": 0,
"total_time": 0,
"check_in_time": None,
}
user_stats[user_id]["check_in_time"] = check_in_time
await message.channel.send(
f"{message.author.mention} You have been checked in. Please mute your microphone."
)
elif message.content.lower() == "checking out":
user_id = str(message.author.id)
if user_id not in user_stats or user_stats[user_id]["check_in_time"] is None:
await message.channel.send(
f"{message.author.mention} You have not checked in yet. Please check in first."
)
return
check_out_time = datetime.now().timestamp()
check_in_time = user_stats[user_id]["check_in_time"]
time_delta = check_out_time - check_in_time
user_stats[user_id]["check_ins"] += 1
user_stats[user_id]["total_time"] += time_delta
user_stats[user_id]["check_in_time"] = None
with open("user_stats.json", "w") as f:
json.dump(user_stats, f)
await message.channel.send(
f"{message.author.mention} You have been checked out. Your session was {time_delta:.2f} seconds."
)
elif message.content.lower() == "stats":
stats_embed = discord.Embed(title="User stats :trophy:", color=0x000000)
sorted_user_stats = sorted(
user_stats.items(), key=lambda x: x[1]["total_time"], reverse=True
)
table_rows = [["Name", "Check-ins", "Total Time"]]
for user_id, stats in sorted_user_stats:
if stats["check_in_time"] is None:
total_time_seconds = stats["total_time"]
hours, total_time_seconds = divmod(total_time_seconds, 3600)
minutes, total_time_seconds = divmod(total_time_seconds, 60)
seconds, fractions = divmod(total_time_seconds, 1)
fractions_str = f"{fractions:.3f}"[2:]
username = garfbot.get_user(int(user_id)).name
table_rows.append(
[
username,
str(stats["check_ins"]),
f"{int(hours)}h {int(minutes)}m {int(seconds)}s {fractions_str}ms",
]
)
else:
username = garfbot.get_user(int(user_id)).name
table_rows.append([username, "Currently checked in", "-"])
table_columns = list(zip(*table_rows[1:]))
table_fields = table_rows[0]
for field, values in zip(table_fields, table_columns):
stats_embed.add_field(name=field, value="\n".join(values), inline=True)
await message.channel.send(embed=stats_embed)

100
garfpy/garfai.py Normal file
View File

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

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)

85
garfpy/iputils.py Normal file
View File

@ -0,0 +1,85 @@
import discord
import ipaddress
import subprocess
from garfpy import logger
class IPUtils:
def is_private(self, target):
try:
ip_obj = ipaddress.ip_address(target)
if ip_obj.is_private:
return True
except ValueError:
if "crate.lan" in target.lower():
return True
if "crate.zip" in target.lower():
return True
if "memtec.org" in target.lower():
return True
if "crateit.net" in target.lower():
return True
if "garfbot.art" in target.lower():
return True
return False
async def scan(self, message, user, guild, query):
split = query.split()
target = split[-1]
if self.is_private(target):
return
if query.startswith("garfping "):
try:
logger.info(
f"Ping Request - User: {user}, Server: {guild}, Target: {target}"
)
await message.channel.send(f"`Pinging {target}...`")
result = subprocess.run(
["ping", "-c", "4", target], capture_output=True, text=True
)
embed = discord.Embed(
title=f"Ping result: {target}",
color=0x4D4D4D,
description=f"```{result.stdout}```",
)
await message.channel.send(embed=embed)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
if query.startswith("garfdns "):
try:
logger.info(
f"NSLookup Request - User: {user}, Server: {guild}, Target: {target}"
)
await message.channel.send(f"`Requesting {target}...`")
result = subprocess.run(
["nslookup", target], capture_output=True, text=True
)
embed = discord.Embed(
title=f"NSLookup result: {target}",
color=0x4D4D4D,
description=f"```{result.stdout}```",
)
await message.channel.send(embed=embed)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")
if query.startswith("garfhack "):
try:
logger.info(
f"Nmap Request - User: {user}, Server: {guild}, Target: {target}"
)
await message.channel.send(f"`Scanning {target}...`")
result = subprocess.run(
["nmap", "-Pn", "-O", "-v", target], capture_output=True, text=True
)
embed = discord.Embed(
title=f"Nmap scan result: {target}",
color=0x4D4D4D,
description=f"```{result.stdout}```",
)
embed.set_footer(text="https://nmap.org/")
await message.channel.send(embed=embed)
except Exception as e:
await message.channel.send(f"`GarfBot Error: {str(e)}`")

79
garfpy/kroger.py Normal file
View File

@ -0,0 +1,79 @@
import config
import requests
from base64 import b64encode
from garfpy import logger
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()
def kroger_token(self):
headers = {
"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.raise_for_status()
return response.json()["access_token"]
def find_store(self, zipcode, kroken):
headers = {
"Authorization": f"Bearer {kroken}",
}
params = {
"filter.zipCode.near": zipcode,
"filter.limit": 1,
}
response = requests.get(
"https://api.kroger.com/v1/locations", headers=headers, params=params
)
return response.json()
def search_product(self, product, loc_id, kroken):
logger.info(f"Searching for {product}...")
headers = {
"Authorization": f"Bearer {kroken}",
}
params = {
"filter.term": product,
"filter.locationId": loc_id,
"filter.limit": 10,
}
response = requests.get(
"https://api.kroger.com/v1/products", headers=headers, params=params
)
return response.json()
def 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

24
garfpy/log.py Normal file
View File

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

64
garfpy/qr.py Normal file
View File

@ -0,0 +1,64 @@
import qrcode
from io import BytesIO
def calculate_qr_settings(text):
text_length = len(text)
if text_length <= 25:
version = 1
box_size = 12
elif text_length <= 47:
version = 2
box_size = 10
elif text_length <= 77:
version = 3
box_size = 8
elif text_length <= 114:
version = 4
box_size = 7
elif text_length <= 154:
version = 5
box_size = 6
elif text_length <= 195:
version = 6
box_size = 5
elif text_length <= 224:
version = 7
box_size = 5
elif text_length <= 279:
version = 8
box_size = 4
elif text_length <= 335:
version = 9
box_size = 4
elif text_length <= 395:
version = 10
box_size = 3
else:
version = None
box_size = 3
return version, box_size
async def generate_qr(text):
version, box_size = calculate_qr_settings(text)
qr = qrcode.QRCode(
version=version,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=box_size,
border=4,
)
qr.add_data(text)
qr.make(fit=True)
qr_image = qr.make_image(fill_color="black", back_color="white")
img_buffer = BytesIO()
qr_image.save(img_buffer, format="PNG")
img_buffer.seek(0)
return img_buffer

165
garfpy/respond.py Normal file
View File

@ -0,0 +1,165 @@
from garfpy import logger
import discord
import json
import os
import re
class GarfbotRespond:
def __init__(self):
self.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)

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,11 +1,12 @@
import config import config
import openai import openai
import discord import discord
import asyncio
import os import os
openai.api_key = config.OPENAI_TOKEN openai.api_key = config.OPENAI_TOKEN
jonkey = config.JONBOT_TOKEN jonkey = config.JONBOT_TOKEN
model = "gpt-3.5-turbo" model = config.TXT_MODEL
intents = discord.Intents.default() intents = discord.Intents.default()
intents.messages = True intents.messages = True
@ -14,7 +15,7 @@ client = discord.Client(intents=intents)
@client.event @client.event
async def on_ready(): async def on_ready():
print(f"Logged in as {client.user.name} running gpt-3.5-turbo.", flush=True) print(f"Logged in as {client.user.name} running {model}.", flush=True)
@client.event @client.event
async def on_message(message): async def on_message(message):
@ -39,8 +40,14 @@ async def on_message(message):
e = str(e) e = str(e)
await message.channel.send(f"`JonBot Error: {e}`") await message.channel.send(f"`JonBot Error: {e}`")
try: async def jonbot_connect():
client.run(jonkey) while True:
except Exception as e: try:
e = str(e) await client.start(jonkey)
print(f"JonBot Error: {e}") except Exception as e:
e = str(e)
logger.error(f"Jonbot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(jonbot_connect())

View File

@ -1,11 +1,12 @@
import config import config
import openai import openai
import discord import discord
import asyncio
import os import os
openai.api_key = config.OPENAI_TOKEN openai.api_key = config.OPENAI_TOKEN
moneykey = config.MONEYBOT_TOKEN moneykey = config.MONEYBOT_TOKEN
model = "gpt-4" model = config.TXT_MODEL
intents = discord.Intents.default() intents = discord.Intents.default()
intents.messages = True intents.messages = True
@ -14,7 +15,7 @@ client = discord.Client(intents=intents)
@client.event @client.event
async def on_ready(): async def on_ready():
print(f"Logged in as {client.user.name} running gpt-4.", flush=True) print(f"Logged in as {client.user.name} running {model}.", flush=True)
@client.event @client.event
async def on_message(message): async def on_message(message):
@ -39,4 +40,14 @@ async def on_message(message):
e = str(e) e = str(e)
await message.channel.send(f"`MoneyBot Error: {e}`") await message.channel.send(f"`MoneyBot Error: {e}`")
client.run(moneykey) async def moneybot_connect():
while True:
try:
await client.start(moneykey)
except Exception as e:
e = str(e)
logger.error(f"Moneybot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(moneybot_connect())

View File

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