|
# 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()
|