Compare commits
11 Commits
2686f7e8b5
...
main
Author | SHA1 | Date | |
---|---|---|---|
a53a81a180 | |||
4d2a0dc2d9 | |||
6ec931265f | |||
bdc3c13edb | |||
b63e1738d1 | |||
00bc9db5b3 | |||
9f9a484613 | |||
aecafc53fc | |||
af1c689fe7 | |||
1f8fc09562 | |||
4b077e3fd9 |
@ -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
|
||||
|
42
Dockerfile
42
Dockerfile
@ -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
|
||||
|
||||
CMD [ "python", "garfmain.py" ]
|
||||
RUN pip3 install --no-cache-dir \
|
||||
discord \
|
||||
openai \
|
||||
aiohttp \
|
||||
requests \
|
||||
wikipedia \
|
||||
pillow \
|
||||
qrcode
|
||||
|
||||
CMD [ "python", "garfmain.py" ]
|
44
README.md
44
README.md
@ -2,25 +2,44 @@ Who is GarfBot?
|
||||
======
|
||||

|
||||
|
||||
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
7
docker-compose.yml
Normal 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
|
69
garfmain.py
69
garfmain.py
@ -10,6 +10,7 @@ from garfpy import (
|
||||
Kroger,
|
||||
GarfAI,
|
||||
GarfbotRespond,
|
||||
WeatherAPI,
|
||||
)
|
||||
|
||||
|
||||
@ -28,6 +29,7 @@ garf_respond = GarfbotRespond()
|
||||
garfield = GarfAI()
|
||||
iputils = IPUtils()
|
||||
kroger = Kroger()
|
||||
weather = WeatherAPI()
|
||||
|
||||
|
||||
@garfbot.event
|
||||
@ -107,6 +109,71 @@ async def on_message(message):
|
||||
)
|
||||
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":
|
||||
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)
|
||||
@ -125,7 +192,7 @@ async def on_message(message):
|
||||
break
|
||||
|
||||
|
||||
# Run Garfbot
|
||||
# Run GarfBot
|
||||
async def garfbot_connect():
|
||||
while True:
|
||||
try:
|
||||
|
@ -8,3 +8,4 @@ from .respond import GarfbotRespond
|
||||
from .aod import aod_message
|
||||
from .qr import generate_qr
|
||||
from .iputils import IPUtils
|
||||
from .weather import WeatherAPI
|
@ -48,11 +48,11 @@ class GarfAI:
|
||||
async with session.get(image_url) as resp:
|
||||
if resp.status == 200:
|
||||
image_data = await resp.read()
|
||||
ram_image = io.BytesIO(image_data)
|
||||
ram_image.seek(0)
|
||||
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=ram_image, filename=filename)
|
||||
sendfile = discord.File(fp=image, filename=filename)
|
||||
try:
|
||||
await message.channel.send(file=sendfile)
|
||||
except Exception as e:
|
||||
|
@ -38,7 +38,11 @@ class IPUtils:
|
||||
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}```")
|
||||
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)}`")
|
||||
@ -52,7 +56,11 @@ class IPUtils:
|
||||
result = subprocess.run(
|
||||
["nslookup", target], capture_output=True, text=True
|
||||
)
|
||||
embed = discord.Embed(title=f"NSLookup result: {target}", color=0x4D4D4D, description=f"```{result.stdout}```")
|
||||
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)}`")
|
||||
@ -66,7 +74,11 @@ class IPUtils:
|
||||
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 = 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:
|
||||
|
@ -7,47 +7,47 @@ import re
|
||||
|
||||
class GarfbotRespond:
|
||||
def __init__(self):
|
||||
self.guild_responses = {}
|
||||
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.guild_responses = json.load(f)
|
||||
self.guild_responses = {
|
||||
int(k): v for k, v in self.guild_responses.items()
|
||||
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.guild_responses.values()
|
||||
len(responses) for responses in self.garfbot_responses.values()
|
||||
)
|
||||
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:
|
||||
logger.info(f"Error loading responses: {e}")
|
||||
self.guild_responses = {}
|
||||
self.garfbot_responses = {}
|
||||
else:
|
||||
self.guild_responses = {}
|
||||
self.garfbot_responses = {}
|
||||
|
||||
def save_responses(self):
|
||||
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:
|
||||
json.dump(save_data, f, indent=2, ensure_ascii=False)
|
||||
total_responses = sum(
|
||||
len(responses) for responses in self.guild_responses.values()
|
||||
len(responses) for responses in self.garfbot_responses.values()
|
||||
)
|
||||
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:
|
||||
logger.info(f"Error saving responses: {e}")
|
||||
|
||||
def get_responses(self, guild_id):
|
||||
if guild_id not in self.guild_responses:
|
||||
self.guild_responses[guild_id] = {}
|
||||
return self.guild_responses[guild_id]
|
||||
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
|
||||
@ -96,7 +96,7 @@ class GarfbotRespond:
|
||||
|
||||
responses = self.get_responses(guild_id)
|
||||
responses[trigger] = response_text
|
||||
self.guild_responses[guild_id] = responses
|
||||
self.garfbot_responses[guild_id] = responses
|
||||
self.save_responses()
|
||||
|
||||
embed = discord.Embed(title="✅ Auto-response Added.", color=0x00FF00)
|
||||
@ -112,7 +112,7 @@ class GarfbotRespond:
|
||||
if trigger in responses:
|
||||
removed_response = responses[trigger]
|
||||
del responses[trigger]
|
||||
self.guild_responses[guild_id] = responses
|
||||
self.garfbot_responses[guild_id] = responses
|
||||
self.save_responses()
|
||||
|
||||
embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
|
||||
@ -127,7 +127,7 @@ class GarfbotRespond:
|
||||
if key.lower() == trigger.lower():
|
||||
removed_response = responses[key]
|
||||
del responses[key]
|
||||
self.guild_responses[guild_id] = responses
|
||||
self.garfbot_responses[guild_id] = responses
|
||||
self.save_responses()
|
||||
|
||||
embed = discord.Embed(title="✅ Auto-response Removed.", color=0xFF6B6B)
|
||||
|
203
garfpy/weather.py
Normal file
203
garfpy/weather.py
Normal file
@ -0,0 +1,203 @@
|
||||
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:
|
||||
# Check if last part looks like country code
|
||||
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
|
Reference in New Issue
Block a user