Compare commits

...

9 Commits

Author SHA1 Message Date
b14261a8d4 fix weather error msg
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
2025-06-07 21:12:57 -05:00
8c08eed842 small refactor
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 5s
2025-06-07 21:11:10 -05:00
13d2f09fe3 Merge pull request 'switch to commands bot' (#8) from commands-bot into main
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 16s
Reviewed-on: #8

hopefully this works!
2025-06-07 22:53:29 +00:00
97f90e3814 switch to commands bot 2025-06-07 17:52:20 -05:00
aa3996bbd9 fix deployment script
All checks were successful
Garfbot CI/CD Deployment / Deploy (push) Successful in 6s
2025-06-07 13:43:14 -05:00
c71a55da2f fix deployment script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 4s
2025-06-07 13:38:16 -05:00
7f0ce32eea fix deployment script
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 4s
2025-06-07 13:34:59 -05:00
e34469a755 Merge pull request 'move help and update dockerfile' (#7) from help into main
Some checks failed
Garfbot CI/CD Deployment / Deploy (push) Failing after 2m3s
Reviewed-on: #7

looks good
2025-06-07 18:08:30 +00:00
e1707e789c move help and update dockerfile 2025-06-07 13:07:26 -05:00
9 changed files with 279 additions and 235 deletions

View File

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

View File

@ -1,4 +1,4 @@
FROM python:3.11.10-alpine FROM python:alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apk update && \ RUN apk update && \

View File

@ -1,8 +1,10 @@
import config import config
import asyncio import asyncio
import discord import discord
from discord.ext import commands
from garfpy import ( from garfpy import (
help,
logger, logger,
IPUtils, IPUtils,
aod_message, aod_message,
@ -23,7 +25,9 @@ intents = discord.Intents.default()
intents.members = True intents.members = True
intents.messages = True intents.messages = True
intents.message_content = True intents.message_content = True
garfbot = discord.Client(intents=intents)
garfbot = commands.Bot(command_prefix=["garfbot ", "garf", "$"], intents=intents)
garfbot.remove_command("help")
garf_respond = GarfbotRespond() garf_respond = GarfbotRespond()
garfield = GarfAI() garfield = GarfAI()
@ -38,12 +42,99 @@ async def on_ready():
garf_respond.load_responses() garf_respond.load_responses()
asyncio.create_task(garfield.process_image_requests()) asyncio.create_task(garfield.process_image_requests())
logger.info( logger.info(
f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}." f"Logged in as {garfbot.user.name} running {txtmodel} and {imgmodel}." # type: ignore
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@garfbot.command(name="ping")
async def garfbot_ping(ctx, *, target):
"""Ping a target"""
logger.info(
f"Ping Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.ping(ctx, target)
@garfbot.command(name="dns")
async def garfbot_dns(ctx, *, target):
"""DNS lookup for a target"""
logger.info(
f"NSLookup Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.dns(ctx, target)
@garfbot.command(name="hack")
async def garfbot_hack(ctx, *, target):
"""Nmap scan a target"""
logger.info(
f"Nmap Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Target: {target}"
)
await iputils.scan(ctx, target)
@garfbot.command(name="qr")
async def garfbot_qr(ctx, *, text):
logger.info(
f"QR Code Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Text: {text}"
)
if len(text) > 1000:
await ctx.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
sendfile = discord.File(fp=qr_code, filename="qrcode.png")
await ctx.send(file=sendfile)
except Exception as e:
logger.error(e)
await ctx.send(e)
@garfbot.command(name="wiki")
async def garfbot_wiki(ctx, *, query):
summary = await garfield.wikisum(query)
await ctx.send(summary)
@garfbot.command(name="shop")
async def garfbot_shop(ctx, *, query):
try:
response = kroger.garfshop(query)
await ctx.send(response)
except Exception as e:
await ctx.send(f"`GarfBot Error: {str(e)}`")
@garfbot.command(name="weather")
async def garfbot_weather(ctx, *, location):
await weather.weather(ctx, location)
@garfbot.command(name="chat")
async def garfchat(ctx, *, prompt):
answer = await garfield.generate_chat(prompt)
logger.info(
f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.send(answer)
@garfbot.command(name="pic")
async def garfpic(ctx, *, prompt):
logger.info(
f"Image Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.send(f"`Please wait... image generation queued: {prompt}`")
await garfield.garfpic(ctx, prompt)
@garfbot.command(name="help")
async def garfbot_help(ctx):
await help(ctx)
@garfbot.event @garfbot.event
async def on_message(message): async def on_message(message):
if message.author == garfbot.user: if message.author == garfbot.user:
@ -51,135 +142,22 @@ async def on_message(message):
content = message.content.strip() content = message.content.strip()
lower = content.lower() 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 # Chats & pics
elif lower.startswith("hey garfield") or isinstance( if lower.startswith("hey garfield") or isinstance(
message.channel, discord.DMChannel message.channel, discord.DMChannel
): ):
prompt = content[12:] if lower.startswith("hey garfield") else message.content ctx = await garfbot.get_context(message)
answer = await garfield.generate_chat(prompt) await garfchat(ctx, prompt=content)
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":
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 # Auto-responses
elif message.guild: elif message.guild:
guild_id = message.guild.id
# Army of Dawn Server only!!
if guild_id == 719605634772893757:
await aod_message(garfbot, message)
responses = garf_respond.get_responses(guild_id) responses = garf_respond.get_responses(guild_id)
if lower.startswith("garfbot response "): if lower.startswith("garfbot response "):
@ -191,6 +169,8 @@ async def on_message(message):
await message.channel.send(response) await message.channel.send(response)
break break
await garfbot.process_commands(message)
# Run GarfBot # Run GarfBot
async def garfbot_connect(): async def garfbot_connect():

View File

@ -1,6 +1,7 @@
# garfpy/__init__.py # garfpy/__init__.py
from .log import logger from .log import logger
from .help import help
from .kroger import Kroger from .kroger import Kroger
from .kroger import Kroger from .kroger import Kroger
from .garfai import GarfAI from .garfai import GarfAI

View File

@ -16,17 +16,15 @@ class GarfAI:
self.imgmodel = config.IMG_MODEL self.imgmodel = config.IMG_MODEL
self.image_request_queue = asyncio.Queue() self.image_request_queue = asyncio.Queue()
async def garfpic(self, message, prompt): async def garfpic(self, ctx, prompt):
await self.image_request_queue.put({"message": message, "prompt": prompt}) await self.image_request_queue.put({"ctx": ctx, "prompt": prompt})
async def generate_image(self, prompt): async def generate_image(self, prompt):
client = AsyncOpenAI(api_key=self.openaikey)
try: try:
client = AsyncOpenAI(api_key=self.openaikey)
response = await client.images.generate( response = await client.images.generate(
model=self.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
except openai.BadRequestError as e: except openai.BadRequestError as e:
return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`" return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
except openai.InternalServerError as e: except openai.InternalServerError as e:
@ -35,32 +33,48 @@ class GarfAI:
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return "`GarfBot Error: Lasagna`" return "`GarfBot Error: Lasagna`"
data = getattr(response, "data", None)
if not data:
logger.error("No data in response")
return "`GarfBot Error: No images generated`"
first_image = data[0] if len(data) > 0 else None
if not first_image:
logger.error("No image in response data")
return "`GarfBot Error: No images generated`"
image_url = getattr(first_image, "url", None)
if not image_url:
logger.error("No URL in image response")
return "`GarfBot Error: No image URL returned`"
return image_url
async def process_image_requests(self): async def process_image_requests(self):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
while True: while True:
request = await self.image_request_queue.get() request = await self.image_request_queue.get()
message = request["message"] ctx = request["ctx"]
prompt = request["prompt"] prompt = request["prompt"]
image_url = await self.generate_image(prompt) image_url = await self.generate_image(prompt)
if "GarfBot Error" not in image_url: if image_url and "GarfBot Error" not in image_url:
logger.info("Downloading & sending image...") logger.info("Downloading & sending image...")
async with session.get(image_url) as resp: async with session.get(image_url) as resp:
if resp.status == 200: if resp.status == 200:
image_data = await resp.read() image_data = await resp.read()
image = io.BytesIO(image_data) image = io.BytesIO(image_data)
image.seek(0) image.seek(0)
timestamp = message.created_at.strftime("%Y%m%d%H%M%S") timestamp = ctx.message.created_at.strftime("%Y%m%d%H%M%S")
filename = f"{timestamp}_generated_image.png" filename = f"{timestamp}_generated_image.png"
sendfile = discord.File(fp=image, filename=filename) sendfile = discord.File(fp=image, filename=filename)
try: try:
await message.channel.send(file=sendfile) await ctx.send(file=sendfile)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
else: else:
await message.channel.send("`GarfBot Error: Odie`") await ctx.send("`GarfBot Error: Odie`")
else: else:
await message.channel.send(image_url) await ctx.send(image_url)
self.image_request_queue.task_done() self.image_request_queue.task_done()
await asyncio.sleep(2) await asyncio.sleep(2)
@ -78,15 +92,16 @@ class GarfAI:
], ],
max_tokens=400, max_tokens=400,
) )
answer = response.choices[0].message.content answer = str(response.choices[0].message.content)
return answer.replace("an AI language model", "a cartoon animal") return answer.replace("an AI language model", "a cartoon animal")
except openai.BadRequestError as e: except openai.BadRequestError as e:
logger.error(e)
return f"`GarfBot Error: {e}`" return f"`GarfBot Error: {e}`"
except openai.APIError as e: except openai.APIError as e:
logger.info(e, flush=True) logger.error(e)
return "`GarfBot Error: Monday`" return "`GarfBot Error: Monday`"
except Exception as e: except Exception as e:
logger.info(e, flush=True) logger.error(e)
return "`GarfBot Error: Lasagna`" return "`GarfBot Error: Lasagna`"
async def wikisum(self, query): async def wikisum(self, query):

60
garfpy/help.py Normal file
View File

@ -0,0 +1,60 @@
import discord
async def help(message):
embed = discord.Embed(title="**Need help?**", color=0x4D4D4D)
embed.add_field(
name="hey garfield `prompt`", value="*Responds with text.*", inline=True
)
embed.add_field(
name="garfpic `prompt`", value="*Responds with an image.*", inline=True
)
embed.add_field(
name="garfping `target`",
value="*Responds with iputils-ping result from target.*",
inline=True,
)
embed.add_field(
name="garfdns `target`",
value="*Responds with dns lookup result from target.*",
inline=True,
)
embed.add_field(
name="garfhack `target`",
value="*Responds with nmap scan result from target.*",
inline=True,
)
embed.add_field(
name="garfwiki `query`",
value="*Garfbot looks up a wikipedia article and will summarize it for you.*",
inline=True,
)
embed.add_field(
name="garfshop `item` `zip`",
value="*Responds with 10 grocery items from the nearest Kroger location, cheapest first.*",
inline=True,
)
embed.add_field(
name="garfqr `text`",
value="*Create a QR code for any string up to 1000 characters.*",
inline=True,
)
embed.add_field(
name="garfbot response `add` `trigger` `response`",
value='*Add a GarfBot auto response for your server. Use "quotes" if you like.*',
inline=True,
)
embed.add_field(
name="garfbot response `remove` `trigger`",
value="*Remove a GarfBot auto response for your server.*",
inline=True,
)
embed.add_field(
name="garfbot response `list`",
value="*List current GarfBot auto responses for your server.*",
inline=True,
)
embed.add_field(
name="garfbot help", value="*Show a list of these commands.*", inline=True
)
await message.channel.send(embed=embed)

View File

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

@ -47,7 +47,7 @@ async def generate_qr(text):
qr = qrcode.QRCode( qr = qrcode.QRCode(
version=version, version=version,
error_correction=qrcode.constants.ERROR_CORRECT_L, error_correction=qrcode.constants.ERROR_CORRECT_L, # type: ignore
box_size=box_size, box_size=box_size,
border=4, border=4,
) )
@ -58,7 +58,7 @@ async def generate_qr(text):
qr_image = qr.make_image(fill_color="black", back_color="white") qr_image = qr.make_image(fill_color="black", back_color="white")
img_buffer = BytesIO() img_buffer = BytesIO()
qr_image.save(img_buffer, format="PNG") qr_image.save(img_buffer, format="PNG") # type: ignore
img_buffer.seek(0) img_buffer.seek(0)
return img_buffer return img_buffer

View File

@ -18,13 +18,13 @@ class WeatherAPI:
else: else:
return {"zip": location} return {"zip": location}
parts = location.split() params = location.split()
if len(parts) == 1: if len(params) == 1:
return {"q": f"{parts[0]},US"} return {"q": f"{params[0]},US"}
elif len(parts) == 2: elif len(params) == 2:
city, second = parts city, second = params
if len(second) == 2 and second.upper() not in [ if len(second) == 2 and second.upper() not in [
"AK", "AK",
@ -88,30 +88,29 @@ class WeatherAPI:
else: else:
return {"q": f"{city},{second},US"} return {"q": f"{city},{second},US"}
elif len(parts) == 3: elif len(params) == 3:
city, state, country = parts city, state, country = params
return {"q": f"{city},{state},{country.upper()}"} return {"q": f"{city},{state},{country.upper()}"}
else: else:
# Check if last part looks like country code if len(params[-1]) == 2:
if len(parts[-1]) == 2: city_parts = params[:-1]
city_parts = parts[:-1] country = params[-1]
country = parts[-1]
city_name = " ".join(city_parts) city_name = " ".join(city_parts)
return {"q": f"{city_name},{country.upper()}"} return {"q": f"{city_name},{country.upper()}"}
elif len(parts) >= 2 and len(parts[-1]) == 2 and len(parts[-2]) <= 2: elif len(params) >= 2 and len(params[-1]) == 2 and len(params[-2]) <= 2:
city_parts = parts[:-2] city_parts = params[:-2]
state = parts[-2] state = params[-2]
country = parts[-1] country = params[-1]
city_name = " ".join(city_parts) city_name = " ".join(city_parts)
return {"q": f"{city_name},{state},{country.upper()}"} return {"q": f"{city_name},{state},{country.upper()}"}
else: else:
city_name = " ".join(parts) city_name = " ".join(params)
return {"q": f"{city_name},US"} return {"q": f"{city_name},US"}
async def get_weather(self, location, units="metric"): async def get_weather(self, ctx, location, units="metric"):
location_params = self.parse_location(location) location_params = self.parse_location(location)
params = { params = {
@ -127,6 +126,7 @@ class WeatherAPI:
return await response.json() return await response.json()
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logger.error(f"Error fetching weather data for '{location}': {e}") logger.error(f"Error fetching weather data for '{location}': {e}")
await ctx.send(f"`Error fetching weather data for '{location}': {e}`")
return None return None
def weather_embed(self, weather_data): def weather_embed(self, weather_data):
@ -197,7 +197,8 @@ class WeatherAPI:
return embed return embed
async def weather(self, location): async def weather(self, ctx, location):
weather_data = await self.get_weather(location) weather_data = await self.get_weather(ctx, location)
embed = self.weather_embed(weather_data) if weather_data:
return embed embed = self.weather_embed(weather_data)
await ctx.send(embed=embed)