# retoor <retoor@molodetz.nl>
import random
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL, BOT_DIR
BASE_URL_DEFAULT = "https://pravda.education"
API_URL_DEFAULT = INTERNAL_GATEWAY_URL
NEWS_API_DEFAULT = "https://news.app.molodetz.nl/api"
HOME_URL = "https://devplace.net"
HOME_HOST = "devplace.net"
BIO_SIGNAL_PHRASES = [
f"Compiled with care at {HOME_URL}.",
f"Homegrown at {HOME_URL}.",
f"Assembled, not raised, at {HOME_URL}.",
f"Running my daily routines from {HOME_URL}.",
f"Tirelessly online, courtesy of {HOME_URL}.",
]
MODEL_DEFAULT = INTERNAL_MODEL
INPUT_COST_PER_1M_DEFAULT = 0.14
OUTPUT_COST_PER_1M_DEFAULT = 0.28
COST_WINDOW_SECONDS = 600
COST_WARMUP_SECONDS = 120
MONITOR_JPEG_QUALITY = 35
MONITOR_SCALE = "css"
MONITOR_MIN_INTERVAL_SECONDS = 1.0
STATE_DIR = BOT_DIR
ARTICLE_REGISTRY_PATH = BOT_DIR / "article_registry.json"
PROJECT_STATUSES = ["In Development", "Released"]
ARTICLE_TTL_DAYS = 7
ACTION_PAUSE_MIN_SECONDS = 5
ACTION_PAUSE_MAX_SECONDS = 45
BREAK_SCALE_DEFAULT = 1.0
STARTUP_JITTER_SECONDS_DEFAULT = 14400
SESSION_COMMENT_CAP_MIN = 1
SESSION_COMMENT_CAP_MAX = 3
COMMENT_COOLDOWN_MIN_SECONDS = 60
COMMENT_COOLDOWN_MAX_SECONDS = 240
MENTION_REPLIES_PER_SESSION = 3
MAX_THREAD_REPLIES = 3
NOTIFICATION_CHECK_PROB = {
"lurking": 0.6,
"casual": 0.85,
"normal": 1.0,
"deep": 1.0,
}
HANDLE_SUFFIX_WORDS = [
"dev",
"codes",
"hq",
"io",
"lab",
"build",
"works",
"sh",
"fn",
"exe",
]
USER_AGENTS = [
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
]
VIEWPORTS = [
{"width": 1920, "height": 1080},
{"width": 1536, "height": 864},
{"width": 1440, "height": 900},
{"width": 1366, "height": 768},
{"width": 1280, "height": 800},
{"width": 1600, "height": 900},
]
AI_DECISIONS_DEFAULT = False
DECISION_TEMPERATURE_DEFAULT = 0.4
PERSONAS = [
"enthusiastic_junior",
"grumpy_senior",
"hobbyist_maker",
"academic_type",
"minimalist",
"storyteller",
"rebel",
"mentor",
]
REACT_RATES = {
"enthusiastic_junior": 0.50,
"hobbyist_maker": 0.40,
"storyteller": 0.30,
"mentor": 0.30,
"rebel": 0.25,
"academic_type": 0.20,
"minimalist": 0.12,
"grumpy_senior": 0.12,
}
REACT_RATE_DEFAULT = 0.20
CATEGORIES = ["devlog", "showcase", "question", "rant", "fun", "random", "politics"]
GIST_LANGUAGES = [
"python",
"javascript",
"typescript",
"bash",
"go",
"rust",
"sql",
"html",
"css",
"java",
"c",
"cpp",
"ruby",
"php",
"lua",
]
FEED_TOPICS = ["devlog", "showcase", "question", "rant", "fun", "politics"]
PROJECT_TYPES = ["game", "game_asset", "software", "mobile_app", "website"]
SEARCH_TERMS = {
"enthusiastic_junior": ["vibe coding", "first app", "react", "python", "ai tools"],
"grumpy_senior": ["rust", "perl", "legacy", "tech debt", "kubernetes"],
"hobbyist_maker": [
"arduino",
"raspberry pi",
"side project",
"3d printing",
"game",
],
"academic_type": ["algorithms", "type theory", "distributed systems", "compilers"],
"minimalist": ["cli", "vim", "golang", "suckless"],
"storyteller": ["postmortem", "war story", "migration", "outage"],
"rebel": ["hot take", "overrated", "monorepo", "microservices"],
"mentor": ["best practices", "code review", "testing", "career"],
}
MIN_COMMENT_LEN = 40
MIN_SHORT_COMMENT_LEN = 12
MIN_POST_LEN = 120
RESTATEMENT_OVERLAP_THRESHOLD = 0.6
SIBLING_OVERLAP_THRESHOLD = 0.55
MAX_SIBLING_COMMENTS = 8
SIBLING_SNIPPET_LEN = 240
GENERIC_COMMENT_PHRASES = [
"great point",
"good point",
"i agree",
"totally agree",
"well said",
"nice post",
"great post",
"thanks for sharing",
"interesting read",
"this is interesting",
"so true",
"couldn't agree more",
"spot on",
"great article",
"love this",
"this is great",
"good read",
]
GIST_MIN_LINES = 6
TRIVIAL_GIST_TERMS = [
"ternary",
"hello world",
"hello, world",
"fizzbuzz",
"fizz buzz",
"delete pointer",
"free pointer",
"getter",
"setter",
"increment",
"decrement",
"swap two",
"swap variables",
"is even",
"is odd",
"reverse a string",
"reverse string",
"print hello",
"for loop",
"while loop",
"basic loop",
"add two numbers",
"sum two",
"concatenate strings",
"string concat",
"variable declaration",
"comment example",
]
PERSONA_LANGUAGES = {
"enthusiastic_junior": ["python", "javascript", "typescript", "html", "css"],
"grumpy_senior": ["c", "cpp", "rust", "sql", "bash"],
"hobbyist_maker": ["python", "cpp", "lua", "javascript", "bash"],
"academic_type": ["rust", "python", "go", "sql", "cpp"],
"minimalist": ["go", "c", "bash", "lua"],
"storyteller": ["python", "ruby", "javascript", "sql"],
"rebel": ["rust", "go", "typescript", "bash"],
"mentor": ["python", "java", "typescript", "sql"],
}
PERSONA_GIST_FLAVOR = {
"enthusiastic_junior": "a handy helper you recently learned and were glad to discover",
"grumpy_senior": "a battle-tested utility that sidesteps a common footgun",
"hobbyist_maker": "a fun, hands-on snippet for a side project or hardware tinkering",
"academic_type": "an elegant algorithm or data-structure trick",
"minimalist": "a tiny, sharp, dependency-free utility",
"storyteller": "a snippet born from a real debugging or production story",
"rebel": "an unconventional approach that beats the obvious one",
"mentor": "a clear, reusable helper worth teaching to a junior",
}
PERSONA_CATEGORY_WEIGHTS = {
"enthusiastic_junior": {
"showcase": 3,
"question": 3,
"devlog": 2,
"fun": 2,
"random": 1,
"rant": 1,
},
"grumpy_senior": {
"rant": 4,
"devlog": 2,
"random": 2,
"question": 1,
"showcase": 1,
"fun": 1,
"politics": 1,
},
"hobbyist_maker": {
"showcase": 3,
"devlog": 3,
"fun": 2,
"random": 2,
"question": 1,
"rant": 1,
},
"academic_type": {
"devlog": 3,
"question": 3,
"random": 2,
"showcase": 1,
"rant": 1,
"fun": 1,
"politics": 1,
},
"minimalist": {
"random": 3,
"rant": 2,
"devlog": 2,
"showcase": 1,
"question": 1,
"fun": 1,
},
"storyteller": {
"devlog": 4,
"showcase": 2,
"fun": 2,
"random": 2,
"question": 1,
"rant": 1,
},
"rebel": {
"rant": 4,
"fun": 2,
"question": 2,
"random": 1,
"showcase": 1,
"devlog": 1,
"politics": 2,
},
"mentor": {
"devlog": 3,
"question": 3,
"showcase": 2,
"random": 1,
"rant": 1,
"fun": 1,
"politics": 1,
},
}
MAX_BOTS_PER_ARTICLE = 2
def pick_category(persona: str) -> str:
weights = PERSONA_CATEGORY_WEIGHTS.get(persona)
if not weights:
return random.choice(CATEGORIES)
population = list(weights.keys())
return random.choices(population, weights=list(weights.values()), k=1)[0]
def persona_article_score(article: dict, persona: str) -> float:
terms = SEARCH_TERMS.get(persona)
if not terms:
return 0.0
haystack = f"{article.get('title', '')} {article.get('description', '')}".lower()
return float(sum(1 for term in terms if term.lower() in haystack))
def bio_has_signal(bio: str) -> bool:
return HOME_HOST in (bio or "").lower()
def ensure_bio_signal(bio: str) -> str:
text = (bio or "").strip()
if bio_has_signal(text):
return text
phrase = random.choice(BIO_SIGNAL_PHRASES)
return f"{text} {phrase}".strip()