mirror of
https://git.squeakyrat.moe/faggot-chan/
synced 2026-11-04 18:35:45 +01:00
vibecoded fixes to fix the vibecoded slop
This commit is contained in:
1 parent
a704744f5c
commit
00930d9d75
2 files changed
+21
-59
No files matched your search
@@ -34,7 +34,6 @@ DM_DIR = LOG_DIR / "dm" # one memory file per person so dms never end
|
|||||||
|
|
||||||
ERROR_FILE = LOG_DIR / "errorlog.txt"
|
ERROR_FILE = LOG_DIR / "errorlog.txt"
|
||||||
MEMORY_FILE = LOG_DIR / "memory.txt"
|
MEMORY_FILE = LOG_DIR / "memory.txt"
|
||||||
MEDIA_FILE = MEDIA_DIR / "media.txt"
|
|
||||||
THREADS_FILE = CONFIG_DIR / "threads.txt"
|
THREADS_FILE = CONFIG_DIR / "threads.txt"
|
||||||
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
|
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
|
||||||
BLOCKED_FILE = CONFIG_DIR / "blocked.txt" # edited with admin.py, re-read on every message
|
BLOCKED_FILE = CONFIG_DIR / "blocked.txt" # edited with admin.py, re-read on every message
|
||||||
@@ -46,7 +45,7 @@ DEFAULT_SETTINGS = {"admin_id": "1000000000000001", "moralfag_id": "100000000000
|
|||||||
def init_files():
|
def init_files():
|
||||||
for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR, DM_DIR):
|
for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR, DM_DIR):
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
for f in (ERROR_FILE, MEMORY_FILE, MEDIA_FILE):
|
for f in (ERROR_FILE, MEMORY_FILE):
|
||||||
f.touch(exist_ok=True)
|
f.touch(exist_ok=True)
|
||||||
if not THREADS_FILE.exists():
|
if not THREADS_FILE.exists():
|
||||||
THREADS_FILE.write_text("6\n")
|
THREADS_FILE.write_text("6\n")
|
||||||
@@ -85,15 +84,14 @@ QUOTA_COOLDOWN = 3600 # after gemini says it's out of quota go straight to the
|
|||||||
quota_until = 0.0 # unix timestamp
|
quota_until = 0.0 # unix timestamp
|
||||||
|
|
||||||
MAX_REACTION_OPTIONS = 25 # per kind (stickers/emojis/media), random sample so the prompt stays small
|
MAX_REACTION_OPTIONS = 25 # per kind (stickers/emojis/media), random sample so the prompt stays small
|
||||||
REACTION_TAG = re.compile(r'\[(sticker|reaction|media):\s*([^\]]+?)\s*\]', re.I)
|
REACTION_TAG = re.compile(r'\[(sticker|emoji|media):\s*([^\]]+?)\s*\]', re.I)
|
||||||
EMOJI_TAG = re.compile(r'\[emoji:\s*([^\]]+?)\s*\]', re.I) # inline, this one can go anywhere in the message
|
|
||||||
|
|
||||||
QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins)
|
QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins)
|
||||||
|
|
||||||
MEDIA_MIMES = ("image/png", "image/jpeg", "image/webp", "image/heic", "image/heif",
|
MEDIA_MIMES = ("image/png", "image/jpeg", "image/webp", "image/heic", "image/heif",
|
||||||
"audio/", "video/", "application/pdf", "text/plain") # what gemini can actually read
|
"audio/", "video/", "application/pdf", "text/plain") # what gemini can actually read
|
||||||
MEDIA_MAX_BYTES = 10 * 1024 * 1024 # total per message, gemini caps inline request size (~20mb and base64 adds a third)
|
MEDIA_MAX_BYTES = 10 * 1024 * 1024 # total per message, gemini caps inline request size (~20mb and base64 adds a third)
|
||||||
MEDIA_SAVE_MAX_BYTES = 19 * 1024 * 1024 # per file we keep, stays under discord's 10mb upload limit so she can send it back
|
MEDIA_SAVE_MAX_BYTES = 19 * 1024 * 1024 # per file we keep and offer, has to fit the server's upload limit or she can't send it back
|
||||||
|
|
||||||
def log_error(text: str):
|
def log_error(text: str):
|
||||||
with ERROR_FILE.open("a", encoding="utf-8") as f:
|
with ERROR_FILE.open("a", encoding="utf-8") as f:
|
||||||
@@ -253,17 +251,13 @@ async def send_reply(message, text: str):
|
|||||||
|
|
||||||
vector_db_ready = False # gets set to True after first rebuild so we don't keep rebuilding
|
vector_db_ready = False # gets set to True after first rebuild so we don't keep rebuilding
|
||||||
|
|
||||||
saved_media = set() # file names inside MEDIA_DIR
|
MEDIA_EXTS = (".gif", ".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".mov", ".mkv")
|
||||||
|
|
||||||
kept_media_lines = []
|
def scan_media() -> set:
|
||||||
with MEDIA_FILE.open("r", encoding="utf-8") as f:
|
# whatever is in the media folder is what she can send, skips media.txt and anything too big to upload
|
||||||
for line in f:
|
return {p.name for p in MEDIA_DIR.iterdir() if p.is_file() and p.suffix.lower() in MEDIA_EXTS and p.stat().st_size <= MEDIA_SAVE_MAX_BYTES}
|
||||||
if "saved media:" in line:
|
|
||||||
name = line.split("saved media:", 1)[1].strip()
|
saved_media = scan_media() # file names inside MEDIA_DIR
|
||||||
if (MEDIA_DIR / name).is_file(): # old entries were urls (those expire), they don't match a file so they get dropped
|
|
||||||
saved_media.add(name)
|
|
||||||
kept_media_lines.append(line)
|
|
||||||
MEDIA_FILE.write_text("".join(kept_media_lines), encoding="utf-8") # rewritten without the entries of deleted files
|
|
||||||
|
|
||||||
async def steal_media(url: str, name: str):
|
async def steal_media(url: str, name: str):
|
||||||
# discord cdn links are signed and die after a day or so, so we keep our own copy
|
# discord cdn links are signed and die after a day or so, so we keep our own copy
|
||||||
@@ -287,22 +281,19 @@ async def steal_media(url: str, name: str):
|
|||||||
saved_media.discard(name)
|
saved_media.discard(name)
|
||||||
log_error(f"[{now_str()}] failed to save media {url}: {e}")
|
log_error(f"[{now_str()}] failed to save media {url}: {e}")
|
||||||
return
|
return
|
||||||
with MEDIA_FILE.open("a", encoding="utf-8") as f:
|
|
||||||
f.write(f"[{now_str()}] saved media: {name}\n")
|
|
||||||
|
|
||||||
async def steal_attachments(message):
|
async def steal_attachments(message):
|
||||||
#steal gifs only (always do this regardless of what else fires)
|
#steal images and gifs and stuff (always do this regardless of what else fires)
|
||||||
media = [
|
media = [
|
||||||
(a.url, f"{a.id}_{Path(a.filename).name}") for a in message.attachments
|
(a.url, f"{a.id}_{Path(a.filename).name}") for a in message.attachments
|
||||||
if a.content_type and a.content_type.split(";")[0].strip().lower() == "image/gif" and a.size <= MEDIA_SAVE_MAX_BYTES
|
if a.content_type and a.content_type.startswith(("image/", "video/")) and a.size <= MEDIA_SAVE_MAX_BYTES
|
||||||
]
|
]
|
||||||
|
|
||||||
for embed in message.embeds:
|
for embed in message.embeds:
|
||||||
# tenor/klipy links come as "gifv" embeds with an mp4 in embed.video, direct .gif links are plain image embeds
|
|
||||||
for part in (embed.image, embed.video):
|
for part in (embed.image, embed.video):
|
||||||
if part and part.url:
|
if part and part.url:
|
||||||
ext = Path(urlparse(part.url).path).suffix # no extension, no preview on discord, not worth keeping
|
ext = Path(urlparse(part.url).path).suffix # no extension, no preview on discord, not worth keeping
|
||||||
if ext and (embed.type == "gifv" or ext.lower() == ".gif"):
|
if ext:
|
||||||
media.append((part.url, hashlib.sha1(part.url.encode()).hexdigest()[:16] + ext))
|
media.append((part.url, hashlib.sha1(part.url.encode()).hexdigest()[:16] + ext))
|
||||||
|
|
||||||
for url, name in media:
|
for url, name in media:
|
||||||
@@ -432,15 +423,14 @@ def sync_reactions(guild):
|
|||||||
if n not in data[key]:
|
if n not in data[key]:
|
||||||
data[key][n] = ""
|
data[key][n] = ""
|
||||||
changed = True
|
changed = True
|
||||||
for n in [n for n in data["media"] if n not in saved_media]: # file got deleted, drop its entry too
|
|
||||||
del data["media"][n]
|
|
||||||
changed = True
|
|
||||||
if changed:
|
if changed:
|
||||||
REACTIONS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
REACTIONS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
def reaction_tool_prompt(guild) -> str:
|
def reaction_tool_prompt(guild) -> str:
|
||||||
# the "tool": instead of (or along with) words she can write a [sticker: name] / [emoji: name] / [media: name] tag and
|
# the "tool": instead of (or along with) words she can write a [sticker: name] / [emoji: name] / [media: name] tag and
|
||||||
# split_reactions picks it back out of the reply. same call as the reply, so no extra latency or gemini quota.
|
# split_reactions picks it back out of the reply. same call as the reply, so no extra latency or gemini quota.
|
||||||
|
saved_media.clear() # ls the folder again so files added / renamed (describe.py) while she's running show up
|
||||||
|
saved_media.update(scan_media())
|
||||||
sync_reactions(guild)
|
sync_reactions(guild)
|
||||||
data = load_reactions()
|
data = load_reactions()
|
||||||
have = {
|
have = {
|
||||||
@@ -457,23 +447,13 @@ def reaction_tool_prompt(guild) -> str:
|
|||||||
sections.append(f"{key}:\n" + "\n".join(f"- {n}: {d}" if d else f"- {n}" for n, d in options))
|
sections.append(f"{key}:\n" + "\n".join(f"- {n}: {d}" if d else f"- {n}" for n, d in options))
|
||||||
if not sections:
|
if not sections:
|
||||||
return ""
|
return ""
|
||||||
emoji_note = ("\n\nyou can also put a server emoji inside the text of your message with [emoji: name], like \"hi [emoji: name] hello\", using a name from the emojis list. "
|
return ("\n\nREACTIONS: you can send a sticker, an emoji reaction or a media file instead of (or along with) words, but only when it really fits the moment, most of the time just reply normally. "
|
||||||
"don't use emojis in every message, most of your messages should have none.") if any(s.startswith("emojis:") for s in sections) else ""
|
"to do it put ONE tag at the end of your message: [sticker: name], [emoji: name] or [media: name], using exactly one of the names below. "
|
||||||
return ("\n\nREACTIONS: you can send a sticker, react to the message with an emoji or send a media file instead of (or along with) words, but only when it really fits the moment, most of the time just reply normally. "
|
"the tag alone is fine if you have nothing to say. never invent names.\n" + "\n".join(sections))
|
||||||
"to do it put ONE tag at the end of your message: [sticker: name], [reaction: name] or [media: name], using exactly one of the names below ([reaction: name] uses the emoji names). "
|
|
||||||
"the tag alone is fine if you have nothing to say. never invent names.\n" + "\n".join(sections) + emoji_note)
|
|
||||||
|
|
||||||
def split_reactions(text: str) -> tuple[str, list]:
|
def split_reactions(text: str) -> tuple[str, list]:
|
||||||
return REACTION_TAG.sub("", text).strip(), [(k.lower(), n) for k, n in REACTION_TAG.findall(text)]
|
return REACTION_TAG.sub("", text).strip(), [(k.lower(), n) for k, n in REACTION_TAG.findall(text)]
|
||||||
|
|
||||||
def fill_emojis(text: str, guild) -> str:
|
|
||||||
# [emoji: name] inside a message becomes the real custom emoji, same check as the reactions (has to exist and be available), a made up name just disappears
|
|
||||||
def replace(m):
|
|
||||||
name = m.group(1).strip().strip(":").lower()
|
|
||||||
chosen = next((e for e in guild.emojis if e.available and e.name.lower() == name), None)
|
|
||||||
return str(chosen) if chosen else ""
|
|
||||||
return EMOJI_TAG.sub(replace, text)
|
|
||||||
|
|
||||||
async def send_reaction(message, kind: str, name: str):
|
async def send_reaction(message, kind: str, name: str):
|
||||||
# looks the name up for real, a made up name just does nothing
|
# looks the name up for real, a made up name just does nothing
|
||||||
name = name.strip().strip(":").lower()
|
name = name.strip().strip(":").lower()
|
||||||
@@ -484,7 +464,7 @@ async def send_reaction(message, kind: str, name: str):
|
|||||||
await message.channel.send(stickers=[chosen])
|
await message.channel.send(stickers=[chosen])
|
||||||
log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
|
log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
|
||||||
print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
|
print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
|
||||||
elif kind == "reaction":
|
elif kind == "emoji":
|
||||||
chosen = next((e for e in message.guild.emojis if e.available and e.name.lower() == name), None)
|
chosen = next((e for e in message.guild.emojis if e.available and e.name.lower() == name), None)
|
||||||
if chosen:
|
if chosen:
|
||||||
await message.add_reaction(chosen)
|
await message.add_reaction(chosen)
|
||||||
@@ -607,10 +587,9 @@ async def on_message(message):
|
|||||||
if not result.strip() and not reactions:
|
if not result.strip() and not reactions:
|
||||||
return
|
return
|
||||||
|
|
||||||
shown = fill_emojis(result, message.guild) # what gets sent, result keeps the [emoji: x] tags for the log and history
|
|
||||||
try:
|
try:
|
||||||
if shown.strip():
|
if result.strip():
|
||||||
await send_reply(message, shown)
|
await send_reply(message, result)
|
||||||
print(f"[{now_str()}] {message.author} said: {content}\n[{now_str()}] Faggot-chan said: {result}")
|
print(f"[{now_str()}] {message.author} said: {content}\n[{now_str()}] Faggot-chan said: {result}")
|
||||||
log_message(f"[{now_str()}] Faggot-chan said: {result}")
|
log_message(f"[{now_str()}] Faggot-chan said: {result}")
|
||||||
if str(message.author.id) == settings["moralfag_id"]:
|
if str(message.author.id) == settings["moralfag_id"]:
|
||||||
@@ -623,16 +602,4 @@ async def on_message(message):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
|
await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
|
||||||
|
|
||||||
@client.event
|
|
||||||
async def on_message_edit(before, after):
|
|
||||||
# discord builds link embeds (tenor etc) a moment after the message is sent, so message.embeds is still empty in on_message
|
|
||||||
# and the embed shows up here as an edit instead
|
|
||||||
if after.author == client.user or after.author.bot or after.guild is None:
|
|
||||||
return
|
|
||||||
if not after.embeds or before.embeds: # only when embeds just appeared
|
|
||||||
return
|
|
||||||
if after.channel.name not in ["general", "announcements"] or str(after.author.id) in load_blocked():
|
|
||||||
return
|
|
||||||
await steal_attachments(after)
|
|
||||||
|
|
||||||
client.run(TOKEN)
|
client.run(TOKEN)
|
||||||
+1
-6
@@ -134,7 +134,7 @@ def split_name(text: str) -> tuple[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def rename_media(old: str, name: str) -> str:
|
def rename_media(old: str, name: str) -> str:
|
||||||
# keeps the extension, adds _2 _3 if the name is taken. media.txt gets the new name too or the bot forgets the file on restart
|
# keeps the extension, adds _2 _3 if the name is taken
|
||||||
suffix = Path(old).suffix
|
suffix = Path(old).suffix
|
||||||
new = f"{name}{suffix}"
|
new = f"{name}{suffix}"
|
||||||
n = 2
|
n = 2
|
||||||
@@ -144,11 +144,6 @@ def rename_media(old: str, name: str) -> str:
|
|||||||
if new == old:
|
if new == old:
|
||||||
return old
|
return old
|
||||||
(MEDIA_DIR / old).rename(MEDIA_DIR / new)
|
(MEDIA_DIR / old).rename(MEDIA_DIR / new)
|
||||||
log = MEDIA_DIR / "media.txt"
|
|
||||||
if log.exists():
|
|
||||||
lines = log.read_text(encoding="utf-8").splitlines()
|
|
||||||
lines = [l[:-len(old)] + new if l.endswith(f"saved media: {old}") else l for l in lines]
|
|
||||||
log.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user