1 Commits

Author SHA1 Message Date
4ddcd2b374 env vars testing 2026-01-04 05:36:35 -06:00
15 changed files with 162 additions and 451 deletions

View File

@@ -22,7 +22,8 @@ jobs:
if [ "$(docker compose ps -q)" ]; then if [ "$(docker compose ps -q)" ]; then
docker compose down docker compose down
fi fi
docker compose up -d --build docker build -t git.crate.zip/crate/garfbot:latest .
docker compose up -d
else else
docker restart garfbot docker restart garfbot
fi fi

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ config.py
__pycache__/ __pycache__/
garfpy/__pycache__/ garfpy/__pycache__/
*.venv* *.venv*
.idea
*.json *.json
*.old *.old
*.log* *.log*

View File

@@ -18,7 +18,6 @@ RUN apk update && \
tcl-dev tcl-dev
RUN pip3 install --no-cache-dir \ RUN pip3 install --no-cache-dir \
config \
discord \ discord \
openai \ openai \
aiohttp \ aiohttp \

View File

@@ -1,6 +1,6 @@
MIT No Attribution MIT No Attribution
Copyright 2026 Aaron Crate Copyright 2025 Aaron Crate
Permission is hereby granted, free of charge, to any person obtaining a copy of this Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software software and associated documentation files (the "Software"), to deal in the Software

View File

@@ -2,19 +2,14 @@ services:
garfbot: garfbot:
image: git.crate.zip/crate/garfbot:latest image: git.crate.zip/crate/garfbot:latest
container_name: garfbot container_name: garfbot
build: environment:
context: . - TXT_MODEL=${TXT_MODEL}
dockerfile: Dockerfile - IMG_MODEL=${IMG_MODEL}
- GARFBOT_TOKEN=${GARFBOT_TOKEN}
- OPENAI_TOKEN=${OPENAI_TOKEN}
- WEATHER_TOKEN=${WEATHER_TOKEN}
- KROGER_ID=${KROGER_ID}
- KROGER_SECRET=${KROGER_SECRET}
restart: always restart: always
volumes: volumes:
- /home/crate/garfbot:/usr/src/app - /home/crate/garfbot:/usr/src/app
jonbot:
image: git.crate.zip/crate/jonbot:latest
container_name: jonbot
build:
context: ./jonbot
dockerfile: Dockerfile
restart: always
volumes:
- /home/crate/garfbot/jonbot:/usr/src/app

53
etc/jonbot.py Normal file
View File

@@ -0,0 +1,53 @@
import config
import openai
import discord
import asyncio
import os
openai.api_key = config.OPENAI_TOKEN
jonkey = config.JONBOT_TOKEN
model = config.TXT_MODEL
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Logged in as {client.user.name} running {model}.", flush=True)
@client.event
async def on_message(message):
if message.author == client.user:
return
if message.content.lower().startswith("hey jon") or isinstance(message.channel, discord.DMChannel):
question = message.content[7:] if message.content.lower().startswith("hey jon") else message.content
try:
response = openai.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": "Pretend you are friendly Jon Arbuckle."},
{"role": "user", "content": f"{question}"}
],
max_tokens=400
)
answer = response['choices'][0]['message']['content']
answer = answer.replace("AI language model", "American citizen")
answer = answer.replace("language model AI", "citizen of the United States")
await message.channel.send(answer)
except Exception as e:
e = str(e)
await message.channel.send(f"`JonBot Error: {e}`")
async def jonbot_connect():
while True:
try:
await client.start(jonkey)
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,8 +1,7 @@
import config import os
import asyncio import asyncio
import discord import discord
from discord.ext import commands from discord.ext import commands
from urllib.parse import urlparse, parse_qs
from garfpy import ( from garfpy import (
help, help,
@@ -17,10 +16,9 @@ from garfpy import (
) )
# gapikey = config.GIF_TOKEN garfkey = os.getenv("GARFBOT_TOKEN")
garfkey = config.GARFBOT_TOKEN txtmodel = os.getenv("TXT_MODEL")
txtmodel = config.TXT_MODEL imgmodel = os.getenv("IMG_MODEL")
imgmodel = config.IMG_MODEL
intents = discord.Intents.default() intents = discord.Intents.default()
intents.members = True intents.members = True
@@ -41,36 +39,6 @@ kroger = Kroger()
weather = WeatherAPI() weather = WeatherAPI()
URL_PATTERNS = [
r'https?://(?:www\.)?youtube\.com/watch\?[^\s]*',
r'https?://youtu\.be/[^\s]*',
r'https?://(?:open\.)?spotify\.com/[^\s]*',
]
def clean_url(url):
try:
parsed = urlparse(url)
if 'youtube.com' in parsed.hostname:
params = parse_qs(parsed.query)
video_id = params.get('v', [None])[0]
if not video_id:
return None
# timestamp = params.get('t', [None])[0]
# if timestamp:
# return f"https://www.youtube.com/watch?v={video_id}&t={timestamp}"
return f"https://www.youtube.com/watch?v={video_id}"
if 'youtu.be' in parsed.hostname:
return f"https://youtu.be{parsed.path}"
if 'spotify.com' in parsed.hostname:
return f"https://open.spotify.com{parsed.path}"
except Exception:
return None
@garfbot.event @garfbot.event
async def on_ready(): async def on_ready():
try: try:
@@ -116,30 +84,30 @@ async def garfbot_qr(ctx, *, text):
f"QR Code Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Text: {text}" f"QR Code Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Text: {text}"
) )
if len(text) > 1000: if len(text) > 1000:
await ctx.reply("❌ Text too long! Maximum 1000 characters.") await ctx.send("❌ Text too long! Maximum 1000 characters.")
else: else:
try: try:
qr_code = await generate_qr(text) qr_code = await generate_qr(text)
sendfile = discord.File(fp=qr_code, filename="qrcode.png") sendfile = discord.File(fp=qr_code, filename="qrcode.png")
await ctx.reply(file=sendfile) await ctx.send(file=sendfile)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
await ctx.reply(e) await ctx.send(e)
@garfbot.command(name="wiki") @garfbot.command(name="wiki")
async def garfbot_wiki(ctx, *, query): async def garfbot_wiki(ctx, *, query):
summary = await garfield.wikisum(query) summary = await garfield.wikisum(query)
await ctx.reply(summary) await ctx.send(summary)
@garfbot.command(name="shop") @garfbot.command(name="shop")
async def garfbot_shop(ctx, *, query): async def garfbot_shop(ctx, *, query):
try: try:
response = kroger.garfshop(query) response = kroger.garfshop(query)
await ctx.reply(response) await ctx.send(response)
except Exception as e: except Exception as e:
await ctx.reply(f"`GarfBot Error: {str(e)}`") await ctx.send(f"`GarfBot Error: {str(e)}`")
@garfbot.command(name="weather") @garfbot.command(name="weather")
@@ -149,15 +117,11 @@ async def garfbot_weather(ctx, *, location):
@garfbot.command(name="chat") @garfbot.command(name="chat")
async def garfchat(ctx, *, prompt): async def garfchat(ctx, *, prompt):
if "is this true" in prompt.lower():
messages = [msg async for msg in ctx.channel.history(limit=2)]
prompt = messages[1].content
prompt = f"Is this true: {prompt}"
answer = await garfield.generate_chat(prompt) answer = await garfield.generate_chat(prompt)
logger.info( logger.info(
f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}" f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
) )
await ctx.reply(answer) await ctx.send(answer)
@garfbot.command(name="pic") @garfbot.command(name="pic")
@@ -165,7 +129,7 @@ async def garfpic(ctx, *, prompt):
logger.info( logger.info(
f"Image Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}" f"Image Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
) )
await ctx.reply(f"`Please wait... image generation queued: {prompt}`") await ctx.send(f"`Please wait... image generation queued: {prompt}`")
await garfield.garfpic(ctx, prompt) await garfield.garfpic(ctx, prompt)
@@ -183,19 +147,6 @@ async def on_message(message):
content = message.content.strip() content = message.content.strip()
lower = content.lower() lower = content.lower()
# # Remove tracking stuff from youtube and spotify links
# cleaned_urls = []
# for pattern in URL_PATTERNS:
# for match in re.finditer(pattern, message.content):
# cleaned = clean_url(match.group(0))
# if cleaned and cleaned != match.group(0):
# cleaned_urls.append(cleaned)
# if cleaned_urls:
# links = '\n'.join(cleaned_urls)
# await message.reply(f"🔗 Cleaned link{'s' if len(cleaned_urls) > 1 else ''}:\n{links}")
# Chats & pics # Chats & pics
if lower.startswith("hey garfield") or isinstance( if lower.startswith("hey garfield") or isinstance(
message.channel, discord.DMChannel message.channel, discord.DMChannel
@@ -229,7 +180,7 @@ async def on_message(message):
async def garfbot_connect(): async def garfbot_connect():
while True: while True:
try: try:
await garfbot.start(garfkey) await garfbot.start(garfkey) # type: ignore
except Exception as e: except Exception as e:
e = str(e) e = str(e)
logger.error(f"Garfbot couldn't connect! {e}") logger.error(f"Garfbot couldn't connect! {e}")

View File

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

View File

@@ -121,17 +121,3 @@ async def aod_message(garfbot, message):
for field, values in zip(table_fields, table_columns): for field, values in zip(table_fields, table_columns):
stats_embed.add_field(name=field, value="\n".join(values), inline=True) stats_embed.add_field(name=field, value="\n".join(values), inline=True)
await message.channel.send(embed=stats_embed) await message.channel.send(embed=stats_embed)
# # Boy You Said It
# words = re.findall(r"[a-zA-Z']+", message.content.lower())
# stops = {"a", "an", "the", "and", "or", "but", "is", "it", "in", "on", "at", "to", "of"}
# words = [w for w in words if w not in stops]
# if words:
# firsts = [w[0] for w in words]
# commons = max(set(firsts), key=firsts.count)
# count = firsts.count(commons)
# if count >= 3 or (len(words) >= 2 and count / len(words) >= 0.75):
# await message.channel.send("Boy, you said it!")

View File

@@ -1,8 +1,6 @@
import io import io
import time import os
import uuid
import openai import openai
import config
import aiohttp import aiohttp
import asyncio import asyncio
import discord import discord
@@ -11,212 +9,90 @@ from openai import AsyncOpenAI
from garfpy import logger from garfpy import logger
INVOKEAI_BASE = config.INVOKEAI_URL
_MODEL_KEY = "0eb50094-5c9b-431b-ba01-87e145edb849"
_VAE_KEY = "dde3627c-8a45-4088-93d1-66c44acbb337"
_ENCODER_KEY = "7ba22542-4687-4946-a52e-c92f925f4b75"
_MODEL_REF = {"key": _MODEL_KEY, "hash": "blake3:c3ee838d71d99497db01fae6f304eafd9e734e935f3b783e968d50febb56be2c", "name": "FLUX.2 Klein 4B (GGUF Q4)", "base": "flux2", "type": "main"}
_VAE_REF = {"key": _VAE_KEY, "hash": "blake3:531855de70db993d0f6181f82cde27d15411d58b7ffa3b2fdce2b9434c0173c2", "name": "FLUX.2 VAE", "base": "flux2", "type": "vae"}
_ENCODER_REF = {"key": _ENCODER_KEY, "hash": "blake3:af5840e6770dc99f678e69867949c8b9264835915eb82a990e940fa6e4fa6c81", "name": "FLUX.2 Klein Qwen3 4B Encoder", "base": "any", "type": "qwen3_encoder"}
_POLL_INTERVAL = 2
_POLL_ATTEMPTS = 60
_MIN_IMAGE_BYTES = 1000
def _node_id(prefix: str) -> str:
return f"{prefix}:{uuid.uuid4().hex[:10]}"
def _build_graph(prompt: str) -> dict:
seed = int(time.time() * 1000) % (2 ** 31)
p = _node_id("positive_prompt")
ml = _node_id("flux2_klein_model_loader")
te = _node_id("flux2_klein_text_encoder")
dn = _node_id("flux2_denoise")
out = _node_id("canvas_output")
nodes = {
p: {"id": p, "is_intermediate": True, "use_cache": True, "value": prompt, "type": "string"},
ml: {"id": ml, "is_intermediate": True, "use_cache": True, "type": "flux2_klein_model_loader",
"model": _MODEL_REF, "vae_model": _VAE_REF, "qwen3_encoder_model": _ENCODER_REF},
te: {"id": te, "is_intermediate": True, "use_cache": True, "type": "flux2_klein_text_encoder"},
dn: {"id": dn, "is_intermediate": True, "use_cache": True, "type": "flux2_denoise", "seed": seed},
out: {"id": out, "is_intermediate": False, "use_cache": False, "type": "flux2_vae_decode"},
}
edges = [
{"source": {"node_id": ml, "field": "qwen3_encoder"}, "destination": {"node_id": te, "field": "qwen3_encoder"}},
{"source": {"node_id": ml, "field": "max_seq_len"}, "destination": {"node_id": te, "field": "max_seq_len"}},
{"source": {"node_id": p, "field": "value"}, "destination": {"node_id": te, "field": "prompt"}},
{"source": {"node_id": ml, "field": "transformer"}, "destination": {"node_id": dn, "field": "transformer"}},
{"source": {"node_id": ml, "field": "vae"}, "destination": {"node_id": dn, "field": "vae"}},
{"source": {"node_id": te, "field": "conditioning"}, "destination": {"node_id": dn, "field": "positive_text_conditioning"}},
{"source": {"node_id": ml, "field": "vae"}, "destination": {"node_id": out, "field": "vae"}},
{"source": {"node_id": dn, "field": "latents"}, "destination": {"node_id": out, "field": "latents"}},
]
return {"nodes": nodes, "edges": edges}
async def _poll_batch(session: aiohttp.ClientSession, base: str, batch_id: str) -> bool:
"""Poll batch status until completed, failed, or timed out. Returns True on success."""
for _ in range(_POLL_ATTEMPTS):
await asyncio.sleep(_POLL_INTERVAL)
try:
async with session.get(f"{base}/api/v1/queue/default/b/{batch_id}/status") as resp:
if not resp.ok:
continue
s = await resp.json(content_type=None)
total = s.get("total", 0)
completed = s.get("completed", 0)
failed = s.get("failed", 0)
if total > 0 and failed >= total:
logger.error(f"Batch {batch_id} failed")
return False
if total > 0 and completed >= total:
return True
except Exception as e:
logger.error(f"InvokeAI poll error: {e}")
return False
async def _get_image_name(session: aiohttp.ClientSession, base: str, batch_id: str) -> str | None:
try:
async with session.get(f"{base}/api/v1/queue/default/i/{batch_id}") as resp:
if resp.ok:
data = await resp.json(content_type=None)
for node_result in data.get("session", {}).get("results", {}).values():
if node_result.get("type") == "image_output":
return node_result["image"]["image_name"]
except Exception as e:
logger.error(f"Item fetch error: {e}")
try:
async with session.get(
f"{base}/api/v1/images/",
params={"limit": 1, "order_by": "created_at", "direction": "DESC"},
) as resp:
if resp.ok:
items = (await resp.json(content_type=None)).get("items", [])
if items:
return items[0]["image_name"]
except Exception as e:
logger.error(f"Image list fallback error: {e}")
return None
async def _fetch_image_bytes(session: aiohttp.ClientSession, base: str, name: str) -> bytes | None:
"""Try the full image endpoint, then fall back to thumbnail."""
urls = [
f"{base}/api/v1/images/i/{name}/full",
f"{base}/api/v1/images/i/{name}/thumbnail",
]
for url in urls:
try:
async with session.get(url) as resp:
ct = resp.headers.get("Content-Type", "")
data = await resp.read()
logger.info(f"Image fetch {url}: status={resp.status} content-type={ct} size={len(data)}")
if "html" not in ct and len(data) >= _MIN_IMAGE_BYTES:
return data
except Exception as e:
logger.error(f"InvokeAI image fetch error ({url}): {e}")
return None
class GarfAI: class GarfAI:
def __init__(self): def __init__(self):
self.baseurl = config.BASE_URL self.openaikey = os.getenv('OPENAI_TOKEN')
self.sysprompt = config.SYSTEM_PROMPT self.txtmodel = os.getenv('TXT_MODEL')
self.txtmodel = config.TXT_MODEL self.imgmodel = os.getenv('IMG_MODEL')
self.imgmodel = config.IMG_MODEL
self._oai = AsyncOpenAI(
api_key=config.OPENAI_TOKEN,
base_url=config.BASE_URL,
)
self.image_request_queue = asyncio.Queue() self.image_request_queue = asyncio.Queue()
async def garfpic(self, ctx, prompt): async def garfpic(self, ctx, prompt):
await self.image_request_queue.put({"ctx": ctx, "prompt": prompt}) await self.image_request_queue.put({"ctx": ctx, "prompt": prompt})
async def generate_image(self, session: aiohttp.ClientSession, prompt: str) -> bytes | str: async def generate_image(self, prompt):
base = INVOKEAI_BASE client = AsyncOpenAI(api_key=self.openaikey)
try: try:
async with session.post( response = await client.images.generate(
f"{base}/api/v1/queue/default/enqueue_batch", model=self.imgmodel, prompt=prompt, n=1, size="1024x1024"
json={"batch": {"graph": _build_graph(prompt), "runs": 1}}, )
) as resp: except openai.BadRequestError as e:
if not resp.ok: return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
text = await resp.text() except openai.InternalServerError as e:
logger.error(f"InvokeAI enqueue failed {resp.status}: {text}") logger.error(e)
return "`GarfBot Error: InvokeAI rejected the request`" return f"`GarfBot Error: ({e.status_code}) - Monday`"
data = await resp.json(content_type=None)
batch_id = data["batch"]["batch_id"]
except Exception as e: except Exception as e:
logger.error(f"InvokeAI enqueue error: {e}") logger.error(e)
return "`GarfBot Error: Couldn't reach InvokeAI`" return "`GarfBot Error: Lasagna`"
data = getattr(response, "data", None)
if not data:
logger.error("No data in response")
return "`GarfBot Error: No images generated`"
logger.info(f"InvokeAI batch queued: {batch_id}") 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`"
if not await _poll_batch(session, base, batch_id): image_url = getattr(first_image, "url", None)
return "`GarfBot Error: InvokeAI generation failed or timed out`" if not image_url:
logger.error("No URL in image response")
return "`GarfBot Error: No image URL returned`"
image_name = await _get_image_name(session, base, batch_id) return image_url
if not image_name:
return "`GarfBot Error: Could not resolve image name`"
logger.info(f"Got image: {image_name}")
data = await _fetch_image_bytes(session, base, image_name)
if data:
return data
logger.error("All image download attempts failed")
return "`GarfBot Error: Odie`"
async def process_image_requests(self): async def process_image_requests(self):
async with aiohttp.ClientSession(headers={"Accept": "application/json"}) 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()
ctx = request["ctx"] ctx = request["ctx"]
prompt = request["prompt"] prompt = request["prompt"]
image_url = await self.generate_image(prompt)
result = await self.generate_image(session, prompt) if image_url and "GarfBot Error" not in image_url:
logger.info("Downloading & sending image...")
if isinstance(result, bytes): async with session.get(image_url) as resp:
logger.info("Sending image...") if resp.status == 200:
image = io.BytesIO(result) image_data = await resp.read()
timestamp = ctx.message.created_at.strftime("%Y%m%d%H%M%S") image = io.BytesIO(image_data)
filename = f"{timestamp}_generated_image.png" image.seek(0)
try: timestamp = ctx.message.created_at.strftime("%Y%m%d%H%M%S")
await ctx.reply(file=discord.File(fp=image, filename=filename)) filename = f"{timestamp}_generated_image.png"
except Exception as e: sendfile = discord.File(fp=image, filename=filename)
logger.error(e) try:
await ctx.send(file=sendfile)
except Exception as e:
logger.error(e)
else:
await ctx.send("`GarfBot Error: Odie`")
else: else:
await ctx.reply(result) 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)
async def generate_chat(self, question: str) -> str: async def generate_chat(self, question):
try: try:
response = await self._oai.chat.completions.create( client = AsyncOpenAI(api_key=self.openaikey)
model=self.txtmodel, response = await client.chat.completions.create(
model=self.txtmodel, # type: ignore
messages=[ messages=[
{"role": "system", "content": self.sysprompt}, {
{"role": "user", "content": question}, "role": "system",
"content": "Pretend you are sarcastic Garfield.",
},
{"role": "user", "content": f"{question}"},
], ],
max_tokens=400, max_tokens=400,
temperature=1.2,
) )
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) logger.error(e)
@@ -228,15 +104,12 @@ class GarfAI:
logger.error(e) logger.error(e)
return "`GarfBot Error: Lasagna`" return "`GarfBot Error: Lasagna`"
async def wikisum(self, query: str) -> str: async def wikisum(self, query):
try: try:
summary = wikipedia.summary(query) summary = wikipedia.summary(query)
return await self.generate_chat(f"Please summarize in your own words: {summary}") garfsum = await self.generate_chat(
except wikipedia.exceptions.DisambiguationError as e: f"Please summarize in your own words: {summary}"
options = ", ".join(e.options[:3]) )
return f"`GarfBot Error: Ambiguous query — did you mean: {options}?`" return garfsum
except wikipedia.exceptions.PageError:
return "`GarfBot Error: No Wikipedia page found for that query`"
except Exception as e: except Exception as e:
logger.error(e) return e
return f"`GarfBot Error: {e}`"

View File

@@ -6,9 +6,9 @@ async def help(message):
embed.add_field( embed.add_field(
name="hey garfield `prompt`", value="*Responds with text.*", inline=True name="hey garfield `prompt`", value="*Responds with text.*", inline=True
) )
# embed.add_field( embed.add_field(
# name="garfpic `prompt`", value="*Responds with an image.*", inline=True name="garfpic `prompt`", value="*Responds with an image.*", inline=True
# ) )
embed.add_field( embed.add_field(
name="garfping `target`", name="garfping `target`",
value="*Responds with iputils-ping result from target.*", value="*Responds with iputils-ping result from target.*",

View File

@@ -1,4 +1,4 @@
import config import os
import requests import requests
from base64 import b64encode from base64 import b64encode
from garfpy import logger from garfpy import logger
@@ -6,8 +6,8 @@ from garfpy import logger
class Kroger: class Kroger:
def __init__(self): def __init__(self):
self.client_id = config.CLIENT_ID self.client_id = os.getenv('CLIENT_ID')
self.client_secret = config.CLIENT_SECRET self.client_secret = os.getenv('CLIENT_SECRET')
self.auth = b64encode( self.auth = b64encode(
f"{self.client_id}:{self.client_secret}".encode() f"{self.client_id}:{self.client_secret}".encode()
).decode() ).decode()

View File

@@ -1,5 +1,5 @@
import os
import re import re
import config
import discord import discord
import aiohttp import aiohttp
from garfpy import logger from garfpy import logger
@@ -7,7 +7,7 @@ from garfpy import logger
class WeatherAPI: class WeatherAPI:
def __init__(self, api_key=None): def __init__(self, api_key=None):
self.api_key = api_key or config.WEATHER_TOKEN self.api_key = api_key or os.getenv("WEATHER_TOKEN")
self.base_url = "https://api.openweathermap.org/data/2.5/weather" self.base_url = "https://api.openweathermap.org/data/2.5/weather"
def parse_location(self, location): def parse_location(self, location):
@@ -126,7 +126,7 @@ class WeatherAPI:
response.raise_for_status() response.raise_for_status()
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}'")
await ctx.send(f"`Error fetching weather data for '{location}'`") await ctx.send(f"`Error fetching weather data for '{location}'`")
return None return None

View File

@@ -1,30 +0,0 @@
FROM python:alpine
WORKDIR /usr/src/app
RUN apk update && \
apk add --no-cache \
iputils \
bind-tools \
nmap \
gcc \
musl-dev \
jpeg-dev \
zlib-dev \
freetype-dev \
lcms2-dev \
openjpeg-dev \
tiff-dev \
tk-dev \
tcl-dev
RUN pip3 install --no-cache-dir \
config \
discord \
openai \
aiohttp \
requests \
wikipedia \
pillow \
qrcode
CMD [ "python", "jonbot.py" ]

View File

@@ -1,119 +0,0 @@
import config
import openai
from openai import AsyncOpenAI
import discord
from discord.ext import commands
import asyncio
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(
"jonbot.log",
when="midnight",
interval=1,
backupCount=7,
delay=True,
)
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)
openai.api_key = config.OPENAI_TOKEN
jonkey = config.JONBOT_TOKEN
txtmodel = config.TXT_MODEL
sysprompt = config.SYSTEM_PROMPT
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
client = commands.Bot(
command_prefix=["Jonbot ", "jonbot ", "Jon", "jon"],
case_insensitive=True,
intents=intents,
)
@client.event
async def on_ready():
print(f"Logged in as {client.user.name} running {txtmodel}.", flush=True)
@client.command(name="chat")
async def jonchat(ctx, *, prompt):
if "is this true" in prompt.lower():
messages = [msg async for msg in ctx.channel.history(limit=2)]
prompt = messages[1].content
prompt = f"Is this true: {prompt}"
answer = await generate_chat(prompt)
logger.info(
f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.reply(answer)
@client.event
async def on_message(message):
if message.author == client.user:
return
content = message.content.strip()
lower = content.lower()
if lower.startswith("hey jon") or isinstance(
message.channel, discord.DMChannel
):
ctx = await client.get_context(message)
await jonchat(ctx, prompt=content)
oai = AsyncOpenAI(
api_key=config.OPENAI_TOKEN,
base_url=config.BASE_URL,
)
async def generate_chat(question: str) -> str:
try:
response = await oai.chat.completions.create(
model=txtmodel,
messages=[
{"role": "system", "content": sysprompt},
{"role": "user", "content": question},
],
max_tokens=400,
temperature=1.2,
)
answer = response.choices[0].message.content
return answer
except openai.BadRequestError as e:
logger.error(e)
return f"`JonBot Error: {e}`"
except openai.APIError as e:
logger.error(e)
return "`JonBot Error: Odie`"
except Exception as e:
logger.error(e)
return "`JonBot Error: Liz`"
async def jonbot_connect():
while True:
try:
await client.start(jonkey)
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())