connectd/config.py
root f33409ceda add forge support, central coordination, lost builder detection
- central API client for distributed instance coordination
- forge scraper: gitea, forgejo, gogs, gitlab, sourcehut, codeberg
- forge issue delivery as outreach method
- usage-based contact method ranking with fallback chain
- lost builder detection and targeted outreach
- reddit and lobsters handle discovery
- deep scrape for handle/email discovery from profiles
2025-12-16 21:30:05 +00:00

165 lines
5.9 KiB
Python

"""
connectd/config.py - central configuration
all configurable settings in one place.
"""
import os
from pathlib import Path
# base paths
BASE_DIR = Path(__file__).parent
DB_DIR = BASE_DIR / 'db'
DATA_DIR = BASE_DIR / 'data'
CACHE_DIR = DB_DIR / 'cache'
# ensure directories exist
DATA_DIR.mkdir(exist_ok=True)
CACHE_DIR.mkdir(exist_ok=True)
# === DAEMON CONFIG ===
SCOUT_INTERVAL = 3600 * 4 # full scout every 4 hours
MATCH_INTERVAL = 3600 # check matches every hour
INTRO_INTERVAL = 1800 # send intros every 2 hours
MAX_INTROS_PER_DAY = 1000 # rate limit builder-to-builder outreach
# === MATCHING CONFIG ===
MIN_OVERLAP_PRIORITY = 30 # min score for priority user matches
MIN_OVERLAP_STRANGERS = 50 # higher bar for stranger intros
MIN_HUMAN_SCORE = 25 # min values score to be considered
# === LOST BUILDER CONFIG ===
# these people need encouragement, not networking.
# the goal isn't to recruit them - it's to show them the door exists.
LOST_CONFIG = {
# detection thresholds
'min_lost_score': 40, # minimum lost_potential_score
'min_values_score': 20, # must have SOME values alignment
# outreach settings
'enabled': True,
'max_per_day': 100, # lower volume, higher care
'require_review': False, # fully autonomous
'cooldown_days': 90, # don't spam struggling people
# matching settings
'min_builder_score': 50, # inspiring builders must be active
'min_match_overlap': 10, # must have SOME shared interests
# LLM drafting
'use_llm': True,
'llm_temperature': 0.7, # be genuine, not robotic
# message guidelines (for LLM prompt)
'tone': 'genuine, not salesy',
'max_words': 150, # they don't have energy for long messages
'no_pressure': True, # never pushy
'sign_off': '- connectd',
}
# === API CREDENTIALS ===
# all credentials from environment variables - no defaults
GROQ_API_KEY = os.environ.get('GROQ_API_KEY', '')
GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions'
GROQ_MODEL = os.environ.get('GROQ_MODEL', 'llama-3.3-70b-versatile')
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN', '')
# === FORGE TOKENS ===
# for creating issues on self-hosted git forges
# each forge needs its own token from that instance
#
# CODEBERG: Settings -> Applications -> Generate Token (repo:write scope)
# GITEA/FORGEJO: Settings -> Applications -> Generate Token
# GITLAB: Settings -> Access Tokens -> Personal Access Token (api scope)
# SOURCEHUT: Settings -> Personal Access Tokens (uses email instead)
CODEBERG_TOKEN = os.environ.get('CODEBERG_TOKEN', '')
GITEA_TOKENS = {} # instance_url -> token, loaded from env
GITLAB_TOKENS = {} # instance_url -> token, loaded from env
# parse GITEA_TOKENS from env
# format: GITEA_TOKEN_192_168_1_8_3259=token -> http://192.168.1.8:3259
# format: GITEA_TOKEN_codeberg_org=token -> https://codeberg.org
def _parse_instance_url(env_key, prefix):
"""convert env key to instance URL"""
raw = env_key.replace(prefix, '')
parts = raw.split('_')
# check if last part is a port number
if parts[-1].isdigit() and len(parts[-1]) <= 5:
port = parts[-1]
host = '.'.join(parts[:-1])
# local IPs use http
if host.startswith('192.168.') or host.startswith('10.') or host == 'localhost':
return f'http://{host}:{port}'
return f'https://{host}:{port}'
else:
host = '.'.join(parts)
return f'https://{host}'
for key, value in os.environ.items():
if key.startswith('GITEA_TOKEN_'):
url = _parse_instance_url(key, 'GITEA_TOKEN_')
GITEA_TOKENS[url] = value
elif key.startswith('GITLAB_TOKEN_'):
url = _parse_instance_url(key, 'GITLAB_TOKEN_')
GITLAB_TOKENS[url] = value
MASTODON_TOKEN = os.environ.get('MASTODON_TOKEN', '')
MASTODON_INSTANCE = os.environ.get('MASTODON_INSTANCE', '')
BLUESKY_HANDLE = os.environ.get('BLUESKY_HANDLE', '')
BLUESKY_APP_PASSWORD = os.environ.get('BLUESKY_APP_PASSWORD', '')
MATRIX_HOMESERVER = os.environ.get('MATRIX_HOMESERVER', '')
MATRIX_USER_ID = os.environ.get('MATRIX_USER_ID', '')
MATRIX_ACCESS_TOKEN = os.environ.get('MATRIX_ACCESS_TOKEN', '')
DISCORD_BOT_TOKEN = os.environ.get('DISCORD_BOT_TOKEN', '')
DISCORD_TARGET_SERVERS = os.environ.get('DISCORD_TARGET_SERVERS', '')
# lemmy (for authenticated access to private instance)
LEMMY_INSTANCE = os.environ.get('LEMMY_INSTANCE', '')
LEMMY_USERNAME = os.environ.get('LEMMY_USERNAME', '')
LEMMY_PASSWORD = os.environ.get('LEMMY_PASSWORD', '')
# email (for sending intros)
SMTP_HOST = os.environ.get('SMTP_HOST', '')
SMTP_PORT = int(os.environ.get('SMTP_PORT', '465'))
SMTP_USER = os.environ.get('SMTP_USER', '')
SMTP_PASS = os.environ.get('SMTP_PASS', '')
# === HOST USER CONFIG ===
# the person running connectd - gets priority matching
HOST_USER = os.environ.get('HOST_USER', '') # alias like sudoxnym
HOST_NAME = os.environ.get('HOST_NAME', '')
HOST_EMAIL = os.environ.get('HOST_EMAIL', '')
HOST_GITHUB = os.environ.get('HOST_GITHUB', '')
HOST_MASTODON = os.environ.get('HOST_MASTODON', '') # user@instance
HOST_REDDIT = os.environ.get('HOST_REDDIT', '')
HOST_LEMMY = os.environ.get('HOST_LEMMY', '') # user@instance
HOST_LOBSTERS = os.environ.get('HOST_LOBSTERS', '')
HOST_MATRIX = os.environ.get('HOST_MATRIX', '') # @user:server
HOST_DISCORD = os.environ.get('HOST_DISCORD', '') # user id
HOST_BLUESKY = os.environ.get('HOST_BLUESKY', '') # handle.bsky.social
HOST_LOCATION = os.environ.get('HOST_LOCATION', '')
HOST_INTERESTS = os.environ.get('HOST_INTERESTS', '') # comma separated
HOST_LOOKING_FOR = os.environ.get('HOST_LOOKING_FOR', '')
def get_lost_config():
"""get lost builder configuration"""
return LOST_CONFIG.copy()
def update_lost_config(updates):
"""update lost builder configuration"""
global LOST_CONFIG
LOST_CONFIG.update(updates)
return LOST_CONFIG.copy()