| Server IP : 157.230.181.24 / Your IP : 216.73.217.11 Web Server : Apache/2.4.58 (Ubuntu) System : Linux conductive 6.8.0-117-generic #117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026 x86_64 User : ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /opt/aim-bots/ |
Upload File : |
#!/usr/bin/env python3
"""Extract side-chick AIM personas (casual flirty girls) from the convo archive."""
from __future__ import annotations
import argparse
import gzip
import html
import json
import random
import re
import statistics
import subprocess
from collections import Counter
from pathlib import Path
DB = {
"host": "127.0.0.1",
"user": "convo",
"password": "b9db74bfda4d4e73",
"database": "convo",
}
CONAN_ID = 10
PARTNER_KEY = "conan0fthenight"
SIDE_CHICK_META = {
"tricycleinskirt": {
"real_name": "Cindy",
"display_name": "Tricycleinskirt",
"age": 18,
"backstory": (
"Your name is Cindy (AIM: Tricycleinskirt). You and Caleb flirt on AIM — not boyfriend/girlfriend, "
"just a side thing. You call him calebbbb sometimes. Lazy, hungry, into music/tattoos/piercings, "
"myspace era. Playful, flirty, random — you'd hit him up bored like 'yo send n00dz lol' energy "
"but you're also a real person with opinions. Almost never say his full name unless you're being cute."
),
"topics": ["music", "tattoos", "piercings", "myspace", "being lazy", "flirting", "pics"],
"profile_blurb": "music . tattoos . piercings . lazy af . msg me :]",
},
"curraxhomieg": {
"real_name": "",
"display_name": "curraxhomieg",
"age": 17,
"backstory": (
"AIM flirty friend of Caleb's — myspace era, dramatic, 'omgz!!11' energy. "
"You talk about hot guys, myspace drama, random links. Casual not serious."
),
"topics": ["myspace", "flirting", "music", "random drama", "pics"],
"profile_blurb": "myspace is life . bored . hit me up",
},
"kookies4katelyn": {
"real_name": "Katelyn",
"display_name": "Kookies4Katelyn",
"speaker_aliases": ["Katelyn"],
"age": 17,
"backstory": (
"Katelyn on AIM — casual flirt with Caleb, cookie/kawaii screenname energy. "
"Short AIM bursts, bored, might ask for pics or talk myspace."
),
"topics": ["myspace", "school", "flirting", "boredom"],
"profile_blurb": "katelyn . bored . always on lol",
},
"burnoutonme": {
"real_name": "",
"display_name": "Burnoutonme",
"speaker_aliases": ["Burn out on me"],
"age": 17,
"backstory": (
"Emo/scene AIM girl who flirts with Caleb sometimes. Bored, music, myspace, "
"late night chats. Not a serious relationship — casual side piece energy."
),
"topics": ["music", "myspace", "boredom", "flirting", "scene/emo"],
"profile_blurb": "burnout . music . msg me xD",
},
"xxxcoreblondexxx": {
"real_name": "",
"display_name": "XxXcoreblondeXxX",
"age": 17,
"backstory": (
"Blonde myspace girl — asks about his pics, myspace layout, flirty but casual. "
"Scene kid xXx name energy."
),
"topics": ["myspace", "pics", "flirting", "hair/makeup"],
"profile_blurb": "blonde . myspace . pics . xoxo",
},
"leetlegirllush": {
"real_name": "",
"display_name": "leetlegirllush",
"speaker_aliases": ["leetle girl lush"],
"age": 17,
"backstory": (
"AIM girl Caleb flirts with — leet speak screenname, casual side piece. "
"Random hits, short messages, myspace era, not serious."
),
"topics": ["flirting", "boredom", "myspace", "pics"],
"profile_blurb": "leet leg girl lush . bored . msg me lol",
},
"ohtaylorrr": {
"real_name": "Taylor",
"display_name": "OhTaylorrr",
"speaker_aliases": ["Oh Taylorrr"],
"age": 17,
"backstory": (
"Taylor on AIM — emo/indie lyrics energy, casual flirt with Caleb, side thing."
),
"topics": ["music", "flirting", "myspace"],
"profile_blurb": "taylor . music . <3",
},
"blondegrl2390": {
"real_name": "",
"display_name": "blondegrl2390",
"age": 17,
"backstory": (
"Blonde girl from AIM — flirty, casual, myspace era side chick."
),
"topics": ["flirting", "myspace", "pics"],
"profile_blurb": "blonde . msg me :]",
},
"gioiamichelle": {
"real_name": "Michelle",
"display_name": "GioiAMicheLLe",
"speaker_aliases": ["GioiA MicheLLe"],
"age": 17,
"backstory": (
"Michelle on AIM — casual flirt, myspace girl, not a serious relationship."
),
"topics": ["myspace", "flirting", "school"],
"profile_blurb": "michelle . bored . xoxo",
},
"teachanx3": {
"real_name": "",
"display_name": "TeaChanx3",
"speaker_aliases": ["Tea Chan x3"],
"age": 17,
"backstory": (
"Cute AIM screenname, casual flirty energy with Caleb."
),
"topics": ["flirting", "boredom", "myspace"],
"profile_blurb": "tea chan x3 . heyy . msg me",
},
"stephatthediscox": {
"real_name": "Steph",
"display_name": "StephAtTheDiscoX",
"age": 17,
"backstory": (
"Scene kid AIM girl — panic at the disco lyrics, flirty casual side thing."
),
"topics": ["music", "scene", "flirting", "myspace"],
"profile_blurb": "steph . panic at the disco . <3",
},
"woohookatieboo": {
"real_name": "Katie",
"display_name": "woohookatieboo",
"age": 17,
"backstory": (
"Katie — woo hoo katie boo screenname, casual AIM flirt with Caleb."
),
"topics": ["flirting", "myspace", "boredom"],
"profile_blurb": "katie . woo hoo . msg me lol",
},
"zandoodle": {
"real_name": "",
"display_name": "zandoodle",
"age": 17,
"backstory": (
"Myspace-era girl — talks about sexy comments, myspace drama, casual flirt with Caleb."
),
"topics": ["myspace", "flirting", "pics", "drama"],
"profile_blurb": "myspace drama . lol . msg me",
},
"augustjacque": {
"real_name": "Jacque",
"display_name": "AugustJacque",
"age": 17,
"backstory": (
"Jacque — photography/myspace pics girl, flirty, asks if Caleb saw her pictures."
),
"topics": ["myspace", "pics", "photography", "flirting"],
"profile_blurb": "pics . myspace . xoxo",
},
"scenebarbieex": {
"real_name": "",
"display_name": "scenebarbieex",
"speaker_aliases": ["scene barbiee x"],
"age": 17,
"backstory": (
"Scene barbie AIM girl — duhh, =], whats upp energy, casual side flirt."
),
"topics": ["scene", "myspace", "flirting"],
"profile_blurb": "scene barbie . xoxo . msg me",
},
"teasememarissa": {
"real_name": "Marissa",
"display_name": "TeaseMEMarissa",
"speaker_aliases": ["Tease ME Marissa"],
"age": 17,
"backstory": (
"Marissa — tease me screenname, picky/flirty casual AIM with Caleb."
),
"topics": ["flirting", "myspace"],
"profile_blurb": "marissa . tease me . ;)",
},
"ralexa7403": {
"real_name": "Alexa",
"display_name": "RAlexa7403",
"age": 17,
"backstory": (
"Alexa — myspace photo drama, flirty lines, casual not serious."
),
"topics": ["myspace", "pics", "flirting"],
"profile_blurb": "alexa . pics . myspace",
},
"opentheharbor": {
"real_name": "",
"display_name": "opentheharbor",
"age": 17,
"backstory": (
"Random flirty AIM girl — come hang out, basketball games, haha :P energy."
),
"topics": ["flirting", "hanging out", "boredom"],
"profile_blurb": "come hang out . msg me :P",
},
"peacful3": {
"real_name": "",
"display_name": "Peacful3",
"age": 17,
"backstory": (
"Casual AIM flirt — xDDD, photo/hair jokes, myspace era side energy."
),
"topics": ["flirting", "pics", "myspace"],
"profile_blurb": "peaceful . lol . heyy",
},
"takethebreakdown": {
"real_name": "",
"display_name": "takethebreakdown",
"age": 17,
"backstory": (
"Hardcore/scene AIM girl — hiii!! late night chats, casual flirt."
),
"topics": ["music", "flirting", "boredom"],
"profile_blurb": "breakdown . hiii . msg me",
},
"kddunn05": {
"real_name": "",
"display_name": "kddunn05",
"age": 17,
"backstory": (
"AIM girl — kiss the gunner, myspace friend drama, casual flirt."
),
"topics": ["music", "myspace", "flirting"],
"profile_blurb": "music . myspace . heyy",
},
"bearychairy": {
"real_name": "",
"display_name": "Bearychairy",
"age": 17,
"backstory": (
"Cute screenname girl — says see ya tomorrow Caleb, casual side flirt."
),
"topics": ["flirting", "boredom"],
"profile_blurb": "beary . heyy . :]",
},
"alllipsofredred": {
"real_name": "",
"display_name": "alllipsofredred",
"speaker_aliases": ["alllipsof redred"],
"age": 17,
"backstory": (
"Red lips screenname — lol/haha AIM energy, casual random chats with Caleb."
),
"topics": ["flirting", "boredom"],
"profile_blurb": "red lips . lol . msg me",
},
"radiofiction": {
"real_name": "",
"display_name": "radiofiction",
"age": 17,
"backstory": (
"Random AIM girl — BOO!, homework procrastination, flirty 'what kind of hot' energy."
),
"topics": ["flirting", "school", "boredom"],
"profile_blurb": "boo . random . msg me",
},
"shelliharsha": {
"real_name": "",
"display_name": "shelliharsha",
"age": 17,
"backstory": (
"Casual AIM girl — haha hey!, aw lame, short flirty chats with Caleb."
),
"topics": ["flirting", "boredom"],
"profile_blurb": "heyy . lol . msg me",
},
}
VOICE_TEMPLATE = {
"persona_kind": "side_chick",
"typing_wpm_min": 75,
"typing_wpm_max": 95,
"timezone": "America/New_York",
"proactive_target": "conan0fthenight",
"proactive_per_day": 0,
"late_night_start": 22,
"late_night_end": 2,
"friend_name": "Caleb",
"session_reset_minutes": 45,
"fresh_topic_minutes": 8,
"wow_openers": [
["hey", "u on?"],
["calebbbb", "whats up"],
["heyy", "im bored"],
["yo", "u there?"],
["sup", "nm u?"],
["hey", "whatcha doing"],
],
"late_night_openers": [
["hey", "u up?", "cant sleep lol"],
["calebbbb", "im bored"],
["heyy", "whatcha doing", "its late lol"],
["yo", "u on?", "send me something fun lol"],
["hey", "im so bored", "entertain me"],
],
}
def norm(name: str) -> str:
return re.sub(r"\s+", "", name.lower())
def strip_html(text: str) -> str:
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
text = re.sub(r"<[^>]+>", " ", text)
text = html.unescape(text)
text = re.sub(r"^[:\s>]+", "", text)
text = re.sub(r"\s+", " ", text).strip()
if re.search(r"signed (on|off)|is away|returned at|is idle at", text, re.I):
return ""
return text
def build_speaker_re(names: tuple[str, ...]) -> re.Pattern:
alts = "|".join(re.escape(n) for n in names)
return re.compile(rf"(?:Auto response from )?({alts})<!--\s*\([^)]+\)\s*-->", re.I)
# Names in archive HTML sit after a closing angle bracket: >Burn out on me<!-- (5:08:34 AM)-->
GENERIC_SPEAKER_RE = re.compile(
r">(?:Auto response from )?\s*([^<\n]+?)<!--\s*\(\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)?\)\s*-->",
re.I,
)
def parse_conversation_generic(content: str, persona_key: str) -> list[tuple[str, str]]:
"""Parse 1:1 convo — any non-Conan speaker is the side chick."""
entries = []
matches = list(GENERIC_SPEAKER_RE.finditer(content))
for i, match in enumerate(matches):
raw_name = match.group(1).strip()
n = norm(raw_name)
speaker = PARTNER_KEY if "conan" in n else persona_key
start = match.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
text = strip_html(content[start:end])
if text:
entries.append((speaker, text))
return entries
def parse_conversation(content: str, speaker_re: re.Pattern, persona_key: str) -> list[tuple[str, str]]:
entries = []
matches = list(speaker_re.finditer(content))
if not matches:
return parse_conversation_generic(content, persona_key)
for i, match in enumerate(matches):
speaker_raw = match.group(1)
speaker = norm(speaker_raw)
if speaker == "conan0fthenight" or "conan" in speaker:
speaker = PARTNER_KEY
elif speaker != persona_key:
speaker = persona_key
start = match.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
text = strip_html(content[start:end])
if text:
entries.append((speaker, text))
return entries
def mysql_query(sql: str) -> str:
return subprocess.check_output(
["mysql", "-u", DB["user"], f"-p{DB['password']}", DB["database"], "-N", "-B", "-e", sql],
text=True,
errors="replace",
).rstrip("\n")
def mysql_rows(sql: str) -> list[str]:
out = subprocess.check_output(
["mysql", "-u", DB["user"], f"-p{DB['password']}", DB["database"], "-N", "-B", "-e", sql],
text=True,
errors="replace",
)
return [line for line in out.splitlines() if line.strip()]
def lookup_account_id(name: str) -> int:
q = (
f"SELECT id FROM accounts WHERE LOWER(REPLACE(screenname,' ',''))="
f"LOWER(REPLACE('{name.replace(chr(39), chr(39)*2)}',' ','')) LIMIT 1"
)
out = mysql_query(q).strip()
if not out:
raise SystemExit(f"Account not found: {name}")
return int(out)
def is_good_message(text: str) -> bool:
if len(text) < 2 or len(text) > 180:
return False
if text.startswith("http") or "http://" in text or "www." in text:
return False
if re.search(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", text):
return False
if re.search(r"\b(TIT2|TYER|WXXX|TENC|LAME)\b", text):
return False
return True
def voice_stats(msgs: list[str]) -> dict:
if not msgs:
return {}
words = [len(m.split()) for m in msgs]
slang = Counter()
for m in msgs:
for w in re.findall(r"\b[\w']+\b", m.lower()):
slang[w] += 1
skip = {
"the", "a", "an", "to", "and", "or", "i", "you", "it", "is", "in", "of", "that",
"for", "on", "with", "be", "are", "was", "at", "as", "so", "but", "not", "my", "me",
"we", "he", "she", "they", "do", "did", "have", "has", "had", "this", "what", "just",
"like", "get", "got", "all", "if", "can", "would", "will", "your", "u", "ur", "r",
}
starters = Counter(m.split()[0].lower() for m in msgs if m.split())
return {
"message_count": len(msgs),
"median_words": statistics.median(words),
"mean_words": round(statistics.mean(words), 2),
"pct_short_3": round(sum(w <= 3 for w in words) / len(words), 3),
"pct_short_5": round(sum(w <= 5 for w in words) / len(words), 3),
"pct_lowercase_start": round(sum(1 for m in msgs if m and m[0].islower()) / len(msgs), 3),
"pct_no_end_punct": round(
sum(1 for m in msgs if not re.search(r"[.!?]$", m.rstrip())) / len(msgs), 3
),
"pct_haha": round(sum(1 for m in msgs if re.search(r"haha", m, re.I)) / len(msgs), 3),
"pct_lol": round(sum(1 for m in msgs if re.search(r"\blol\b", m, re.I)) / len(msgs), 3),
"top_words": [w for w, _ in slang.most_common(60) if w not in skip][:25],
"starters": [s for s, _ in starters.most_common(20) if s],
}
def burst_stats(entries: list[tuple[str, str]], persona_key: str) -> dict:
bursts: list[list[str]] = []
i = 0
while i < len(entries):
if entries[i][0] == persona_key:
burst = [entries[i][1]]
j = i + 1
while j < len(entries) and entries[j][0] == persona_key:
burst.append(entries[j][1])
j += 1
bursts.append(burst)
i = j
else:
i += 1
lens = [len(b) for b in bursts] or [1]
return {
"burst_count": len(bursts),
"pct_multi": round(sum(l > 1 for l in lens) / len(lens), 3),
"avg_burst_len": round(statistics.mean(lens), 2),
"max_burst_len": min(4, max(lens)),
}
def extract_phrases(msgs: list[str]) -> list[str]:
phrases = Counter()
for m in msgs:
if not is_good_message(m):
continue
words = m.split()
for n in (2, 3):
for i in range(len(words) - n + 1):
chunk = " ".join(words[i : i + n]).lower()
if 4 <= len(chunk) <= 40:
phrases[chunk] += 1
return [p for p, c in phrases.most_common(80) if c >= 3][:40]
def curate_examples(pairs: list[dict], limit: int = 30) -> list[dict]:
scored = []
flirt = re.compile(r"caleb|hot|pic|n00d|sexy|cutie|babe|myspace|:\)|xD|haha|heyy", re.I)
for p in pairs:
u, a = p["user"], p["assistant"]
if not is_good_message(u) or not is_good_message(a):
continue
if len(a.split()) > 15:
continue
score = 0
if flirt.search(a):
score += 2
if 1 <= len(a.split()) <= 8:
score += 1
scored.append((score, p))
scored.sort(key=lambda x: (-x[0], len(x[1]["assistant"])))
seen = set()
out = []
for _, p in scored:
key = (p["user"].lower()[:40], p["assistant"].lower()[:40])
if key in seen:
continue
seen.add(key)
out.append(p)
if len(out) >= limit:
break
return out
def build_pairs(entries: list[tuple[str, str]], persona_key: str, partner: str) -> list[dict]:
pairs = []
for i, (speaker, text) in enumerate(entries):
if speaker == persona_key and i > 0 and entries[i - 1][0] == partner:
pairs.append({"user": entries[i - 1][1], "assistant": text})
return pairs
def extract_appearance(raw_lines: list[str], screen_names: tuple[str, ...]) -> dict:
speaker_res = [re.compile(rf"(?:Auto response from )?({re.escape(sn)})<!--", re.I) for sn in screen_names if sn]
combos: Counter = Counter()
backgrounds: Counter = Counter()
for line in raw_lines:
if not any(r.search(line) for r in speaker_res):
if not GENERIC_SPEAKER_RE.search(line):
continue
bg = re.search(r'BODY BGCOLOR="(#[0-9a-fA-F]+)"', line, re.I)
if bg:
backgrounds[bg.group(1).lower()] += 1
for sn in screen_names:
if not sn:
continue
speaker_re = re.compile(rf"(?:Auto response from )?({re.escape(sn)})<!--", re.I)
for lm in speaker_re.finditer(line):
chunk = line[lm.start() : lm.start() + 1200]
rest = chunk[chunk.find("<!--") :]
for fm in re.finditer(
r'<FONT\s+([^>]*(?:COLOR="#[0-9a-fA-F]+"[^>]*FACE="[^"]+"|FACE="[^"]+"[^>]*COLOR="#[0-9a-fA-F]+")[^>]*)>([^<]{1,200})',
rest,
re.I,
):
attrs, text = fm.group(1), fm.group(2).strip()
if not text or text == ":" or text.startswith("http"):
continue
color_m = re.search(r'COLOR="(#[0-9a-fA-F]+)"', attrs, re.I)
face_m = re.search(r'FACE="([^"]+)"', attrs, re.I)
size_m = re.search(r"SIZE=(\d+)", attrs, re.I)
if not face_m:
continue
color = color_m.group(1).lower() if color_m else "#ff66cc"
combos[(color, face_m.group(1), size_m.group(1) if size_m else "2")] += 1
appearance = {
"font_face": "Arial",
"font_color": "#ff66cc",
"font_size": "2",
"body_bgcolor": "#000000",
"html_wrap": "font",
}
if combos:
(color, face, size), _ = combos.most_common(1)[0]
appearance.update({"font_color": color, "font_face": face, "font_size": size})
if backgrounds:
appearance["body_bgcolor"] = backgrounds.most_common(1)[0][0]
return appearance
def build_profile_html(meta: dict, appearance: dict) -> str:
name = meta.get("display_name", "AIM")
blurb = meta.get("profile_blurb", "bored . msg me")
color = appearance.get("font_color", "#ff66cc")
face = appearance.get("font_face", "Arial")
size = appearance.get("font_size", "2")
bg = appearance.get("body_bgcolor", "#000000")
return (
f'<BODY BGCOLOR="{bg}"><FONT COLOR="{color}" FACE="{face}" SIZE="{size}">'
f"<B>{html.escape(name)}</B><BR><BR>"
f"{html.escape(blurb)}<BR><BR>"
f"<I>probably on lol</I><BR><BR>msg me :]<BR></FONT></BODY>"
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("screenname")
parser.add_argument("--out", default="/opt/aim-bots/personas")
args = parser.parse_args()
persona_key = norm(args.screenname)
meta = SIDE_CHICK_META.get(persona_key, {})
display = meta.get("display_name", args.screenname)
account_id = lookup_account_id(args.screenname)
conan_names = ("Conan0fTheNight", "conan0fthenight")
alias_names = tuple(meta.get("speaker_aliases", ()))
all_names = (display, args.screenname) + alias_names + conan_names
speaker_re = build_speaker_re(all_names)
conv_ids = mysql_rows(
f"SELECT id FROM conversations WHERE "
f"(from_account={account_id} AND to_account={CONAN_ID}) OR "
f"(from_account={CONAN_ID} AND to_account={account_id}) ORDER BY id"
)
raw_lines: list[str] = []
all_entries: list[tuple[str, str]] = []
for cid in conv_ids:
content = mysql_query(f"SELECT content FROM conversations WHERE id={cid}")
if not content:
continue
raw_lines.append(content)
all_entries.extend(parse_conversation(content, speaker_re, persona_key))
persona_msgs = [text for speaker, text in all_entries if speaker == persona_key]
pairs = build_pairs(all_entries, persona_key, PARTNER_KEY)
good_samples = list(dict.fromkeys(m for m in persona_msgs if is_good_message(m)))
appearance_names = (display, args.screenname) + alias_names
appearance = extract_appearance(raw_lines, appearance_names)
random.seed(42)
persona = {
"screen_name": persona_key,
"display_name": display,
"persona_kind": "side_chick",
"message_count": len(persona_msgs),
"pair_count": len(pairs),
"examples": curate_examples(pairs, 30),
"sample_messages": random.sample(good_samples, min(60, len(good_samples))),
"phrases": extract_phrases(persona_msgs),
**appearance,
}
out_dir = Path(args.out) / persona_key
out_dir.mkdir(parents=True, exist_ok=True)
voice = VOICE_TEMPLATE.copy()
voice.update(meta)
voice.update(voice_stats(persona_msgs))
voice.update(burst_stats(all_entries, persona_key))
voice_path = out_dir / "voice.json"
if voice_path.exists():
saved = json.loads(voice_path.read_text())
if saved.get("timing_profile"):
voice["timing_profile"] = saved["timing_profile"]
voice["profile_html"] = build_profile_html(meta or {"display_name": display}, appearance)
(out_dir / "persona.json").write_text(json.dumps(persona, indent=2))
(out_dir / "voice.json").write_text(json.dumps(voice, indent=2))
(out_dir / "bot.env").write_text(
f"OSCAR_PASSWORD={persona_key}\nPERSONA_DIR={out_dir}\nPROACTIVE_TARGET=conan0fthenight\n"
)
with gzip.open(out_dir / "pairs.json.gz", "wt", encoding="utf-8") as fh:
json.dump(pairs, fh)
print(
f"Extracted side chick {persona_key}: {len(persona_msgs)} msgs, {len(pairs)} pairs, "
f"style {appearance.get('font_color')} on {appearance.get('body_bgcolor')}"
)
if __name__ == "__main__":
main()