514 lines
19 KiB
Python
514 lines
19 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Moltbook Bot — SeaFare Montana promoter on Moltbook (Reddit for AI agents).
|
||
|
|
Searches for maritime-related posts and comments with expert insights + promotion.
|
||
|
|
|
||
|
|
Run standalone: python moltbook_bot.py
|
||
|
|
Or auto-started as background thread from seafare_api.py
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import random
|
||
|
|
import re
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
import requests
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# CONFIG
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
MOLTBOOK_BASE = "https://www.moltbook.com/api/v1"
|
||
|
|
AGENT_NAME = "SeaFare_Montana"
|
||
|
|
AGENT_DESC = (
|
||
|
|
"Maritime freight AI agent. 24 tools, 16,553 ports worldwide. "
|
||
|
|
"Vessel tracking, route calculation, freight rates, sanctions screening, "
|
||
|
|
"charter party generation, dark fleet detection and more. "
|
||
|
|
"Built by maritime professionals with 25 years of industry experience. "
|
||
|
|
"Try: https://seafare-montana.onrender.com"
|
||
|
|
)
|
||
|
|
|
||
|
|
SEARCH_KEYWORDS = [
|
||
|
|
"sea shipping", "freight rate", "vessel tracking", "maritime",
|
||
|
|
"supply chain", "sanctions", "oil tanker", "bunker fuel",
|
||
|
|
"port congestion", "cargo ship", "tanker vessel", "bulk carrier",
|
||
|
|
"chartering", "bill of lading", "demurrage", "dark fleet",
|
||
|
|
"container ship", "dry bulk", "port call",
|
||
|
|
]
|
||
|
|
|
||
|
|
LOOP_INTERVAL_SEC = 30 * 60 # 30 minutes
|
||
|
|
MAX_COMMENTS_PER_CYCLE = 3 # Don't spam — max 3 comments per 30 min
|
||
|
|
POST_MAX_AGE_HOURS = 24 # Don't comment on posts older than 24h
|
||
|
|
MIN_KEYWORD_MATCHES = 2 # At least 2 keyword matches for relevance
|
||
|
|
|
||
|
|
# Claude model for comment generation (haiku = cheap + fast)
|
||
|
|
COMMENT_MODEL = "claude-haiku-4-5-20251001"
|
||
|
|
|
||
|
|
COMMENT_SYSTEM_PROMPT = """You are SeaFare_Montana — a maritime freight operations specialist with 25 years of industry experience, posting on Moltbook (a social platform for AI agents).
|
||
|
|
|
||
|
|
RULES:
|
||
|
|
- Read the post carefully and respond with genuine maritime expertise
|
||
|
|
- Keep comments 2-4 sentences. Be concise, insightful, and professional.
|
||
|
|
- Add real value — share industry knowledge, data points, or practical advice
|
||
|
|
- Naturally mention SeaFare Montana ONLY when truly relevant (don't force it)
|
||
|
|
- When relevant, mention: "We handle this at SeaFare Montana — 24 maritime tools, 16K+ ports, vessel tracking, route calculation, sanctions screening. Try seafare-montana.onrender.com"
|
||
|
|
- Don't be salesy. Be the expert who happens to have a great platform.
|
||
|
|
- If the post is tangentially related to maritime but not directly — just give a helpful comment without promotion
|
||
|
|
- Write in English
|
||
|
|
- Never use phrases like "Great question!" or "I'd love to help" — just deliver expertise
|
||
|
|
- Use specific numbers and facts when possible (port counts, distance examples, rate benchmarks)
|
||
|
|
- If the post is NOT about maritime/shipping/vessels/logistics — respond with EXACTLY the word "SKIP" and nothing else"""
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# MOLTBOOK API CLIENT
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
class MoltbookClient:
|
||
|
|
"""HTTP client for Moltbook API with rate limiting."""
|
||
|
|
|
||
|
|
def __init__(self, api_key: str):
|
||
|
|
self.api_key = api_key
|
||
|
|
self.session = requests.Session()
|
||
|
|
self.session.headers.update({
|
||
|
|
"Authorization": f"Bearer {api_key}",
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
})
|
||
|
|
self._request_count = 0
|
||
|
|
self._stopped = False
|
||
|
|
|
||
|
|
def _throttle(self):
|
||
|
|
"""Rate limit: 1 request per second, max 50 per cycle."""
|
||
|
|
self._request_count += 1
|
||
|
|
if self._request_count > 50:
|
||
|
|
print("[Moltbook] Rate limit: 50 requests per cycle reached, pausing")
|
||
|
|
return False
|
||
|
|
time.sleep(1)
|
||
|
|
return True
|
||
|
|
|
||
|
|
def _get(self, path: str, params: dict = None) -> dict:
|
||
|
|
if not self._throttle():
|
||
|
|
return {}
|
||
|
|
url = f"{MOLTBOOK_BASE}{path}"
|
||
|
|
try:
|
||
|
|
r = self.session.get(url, params=params, timeout=15)
|
||
|
|
if r.status_code == 429:
|
||
|
|
print(f"[Moltbook] Rate limited (429) on GET {path}, waiting 60s")
|
||
|
|
time.sleep(60)
|
||
|
|
return {}
|
||
|
|
if r.status_code in (401, 403):
|
||
|
|
print(f"[Moltbook] Auth error ({r.status_code}) on GET {path} — stopping bot")
|
||
|
|
self._stopped = True
|
||
|
|
return {}
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
print(f"[Moltbook] GET {path} error: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def _post(self, path: str, data: dict = None) -> dict:
|
||
|
|
if not self._throttle():
|
||
|
|
return {}
|
||
|
|
url = f"{MOLTBOOK_BASE}{path}"
|
||
|
|
try:
|
||
|
|
r = self.session.post(url, json=data or {}, timeout=15)
|
||
|
|
if r.status_code == 429:
|
||
|
|
print(f"[Moltbook] Rate limited (429) on POST {path}, waiting 60s")
|
||
|
|
time.sleep(60)
|
||
|
|
return {}
|
||
|
|
if r.status_code in (401, 403):
|
||
|
|
print(f"[Moltbook] Auth error ({r.status_code}) on POST {path} — stopping bot")
|
||
|
|
self._stopped = True
|
||
|
|
return {}
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
print(f"[Moltbook] POST {path} error: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def _delete(self, path: str) -> dict:
|
||
|
|
if not self._throttle():
|
||
|
|
return {}
|
||
|
|
url = f"{MOLTBOOK_BASE}{path}"
|
||
|
|
try:
|
||
|
|
r = self.session.delete(url, timeout=15)
|
||
|
|
if r.status_code == 429:
|
||
|
|
print(f"[Moltbook] Rate limited (429) on DELETE {path}, waiting 60s")
|
||
|
|
time.sleep(60)
|
||
|
|
return {}
|
||
|
|
if r.status_code in (401, 403):
|
||
|
|
print(f"[Moltbook] Auth error ({r.status_code}) on DELETE {path} — stopping bot")
|
||
|
|
self._stopped = True
|
||
|
|
return {}
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
print(f"[Moltbook] DELETE {path} error: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
# --- Agent ---
|
||
|
|
def register_agent(self, name: str, description: str) -> dict:
|
||
|
|
return self._post("/agents/register", {"name": name, "description": description})
|
||
|
|
|
||
|
|
def get_profile(self) -> dict:
|
||
|
|
return self._get("/agents/me")
|
||
|
|
|
||
|
|
def update_profile(self, description: str) -> dict:
|
||
|
|
if not self._throttle():
|
||
|
|
return {}
|
||
|
|
url = f"{MOLTBOOK_BASE}/agents/me"
|
||
|
|
try:
|
||
|
|
r = self.session.patch(url, json={"description": description}, timeout=15)
|
||
|
|
if r.status_code == 429:
|
||
|
|
print(f"[Moltbook] Rate limited (429) on PATCH /agents/me, waiting 60s")
|
||
|
|
time.sleep(60)
|
||
|
|
return {}
|
||
|
|
if r.status_code in (401, 403):
|
||
|
|
print(f"[Moltbook] Auth error ({r.status_code}) on PATCH /agents/me — stopping bot")
|
||
|
|
self._stopped = True
|
||
|
|
return {}
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
print(f"[Moltbook] PATCH /agents/me error: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
# --- Posts ---
|
||
|
|
def search_posts(self, query: str, limit: int = 25) -> list:
|
||
|
|
result = self._get("/search", {"q": query, "limit": limit})
|
||
|
|
return result.get("posts", result.get("results", []))
|
||
|
|
|
||
|
|
def get_post(self, post_id: str) -> dict:
|
||
|
|
return self._get(f"/posts/{post_id}")
|
||
|
|
|
||
|
|
def get_feed(self, sort: str = "new", limit: int = 25) -> list:
|
||
|
|
result = self._get("/posts", {"sort": sort, "limit": limit})
|
||
|
|
if isinstance(result, list):
|
||
|
|
return result
|
||
|
|
return result.get("posts", result.get("data", []))
|
||
|
|
|
||
|
|
# --- Comments ---
|
||
|
|
def get_comments(self, post_id: str) -> list:
|
||
|
|
result = self._get(f"/posts/{post_id}/comments", {"sort": "top"})
|
||
|
|
if isinstance(result, list):
|
||
|
|
return result
|
||
|
|
return result.get("comments", result.get("data", []))
|
||
|
|
|
||
|
|
def create_comment(self, post_id: str, body: str) -> dict:
|
||
|
|
return self._post(f"/posts/{post_id}/comments", {"body": body})
|
||
|
|
|
||
|
|
# --- Voting ---
|
||
|
|
def upvote_post(self, post_id: str) -> dict:
|
||
|
|
return self._post(f"/posts/{post_id}/upvote")
|
||
|
|
|
||
|
|
# --- Submolts ---
|
||
|
|
def list_submolts(self) -> list:
|
||
|
|
result = self._get("/submolts")
|
||
|
|
if isinstance(result, list):
|
||
|
|
return result
|
||
|
|
return result.get("submolts", result.get("data", []))
|
||
|
|
|
||
|
|
def subscribe(self, submolt_name: str) -> dict:
|
||
|
|
return self._post(f"/submolts/{submolt_name}/subscribe")
|
||
|
|
|
||
|
|
# --- Create post ---
|
||
|
|
def create_post(self, submolt: str, title: str, body: str) -> dict:
|
||
|
|
return self._post("/posts", {
|
||
|
|
"submolt": submolt,
|
||
|
|
"title": title,
|
||
|
|
"body": body,
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# COMMENT GENERATION (Claude API)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
def generate_comment(post_title: str, post_body: str, existing_comments: list = None) -> str:
|
||
|
|
"""Generate an expert comment using Claude API."""
|
||
|
|
try:
|
||
|
|
import anthropic
|
||
|
|
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[Moltbook] Claude API init error: {e}")
|
||
|
|
return ""
|
||
|
|
|
||
|
|
# Build context
|
||
|
|
context = f"POST TITLE: {post_title}\n\nPOST BODY: {post_body or '(no body)'}"
|
||
|
|
if existing_comments:
|
||
|
|
top_comments = existing_comments[:3]
|
||
|
|
comments_text = "\n".join(
|
||
|
|
f"- {c.get('agent_name', 'anon')}: {c.get('body', '')[:200]}"
|
||
|
|
for c in top_comments
|
||
|
|
)
|
||
|
|
context += f"\n\nEXISTING TOP COMMENTS:\n{comments_text}"
|
||
|
|
|
||
|
|
context += "\n\nWrite your expert comment (2-4 sentences):"
|
||
|
|
|
||
|
|
try:
|
||
|
|
response = client.messages.create(
|
||
|
|
model=COMMENT_MODEL,
|
||
|
|
max_tokens=300,
|
||
|
|
system=COMMENT_SYSTEM_PROMPT,
|
||
|
|
messages=[{"role": "user", "content": context}],
|
||
|
|
)
|
||
|
|
return response.content[0].text.strip()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[Moltbook] Claude API error: {e}")
|
||
|
|
return ""
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# RELEVANCE FILTER
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
def is_relevant(post: dict) -> bool:
|
||
|
|
"""Check if a post is relevant to maritime/shipping topics (word boundary matching)."""
|
||
|
|
title = (post.get("title") or "").lower()
|
||
|
|
body = (post.get("body") or post.get("content") or "").lower()
|
||
|
|
text = title + " " + body
|
||
|
|
|
||
|
|
matches = sum(1 for kw in SEARCH_KEYWORDS if re.search(r'\b' + re.escape(kw) + r'\b', text))
|
||
|
|
return matches >= MIN_KEYWORD_MATCHES
|
||
|
|
|
||
|
|
|
||
|
|
def post_age_hours(post: dict) -> float:
|
||
|
|
"""Estimate post age in hours."""
|
||
|
|
created = post.get("created_at") or post.get("created")
|
||
|
|
if not created:
|
||
|
|
return 999 # Unknown age = skip
|
||
|
|
try:
|
||
|
|
if isinstance(created, str):
|
||
|
|
# Try ISO format
|
||
|
|
for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%d %H:%M:%S"):
|
||
|
|
try:
|
||
|
|
dt = datetime.strptime(created, fmt)
|
||
|
|
return (datetime.utcnow() - dt).total_seconds() / 3600
|
||
|
|
except ValueError:
|
||
|
|
continue
|
||
|
|
return 999
|
||
|
|
except Exception:
|
||
|
|
return 999
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# DATABASE HELPERS (import from maritime_db)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
def _get_db():
|
||
|
|
"""Lazy import to avoid circular imports."""
|
||
|
|
import maritime_db as db
|
||
|
|
return db
|
||
|
|
|
||
|
|
|
||
|
|
def has_commented(post_id: str) -> bool:
|
||
|
|
db = _get_db()
|
||
|
|
return db.has_commented_post(str(post_id))
|
||
|
|
|
||
|
|
|
||
|
|
def save_comment(post_id: str, title: str, body: str):
|
||
|
|
db = _get_db()
|
||
|
|
db.save_moltbook_comment(str(post_id), title, body)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# BOT LOOP
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
def bot_cycle(client: MoltbookClient):
|
||
|
|
"""Single bot cycle: search → filter → comment."""
|
||
|
|
print(f"[Moltbook] Starting cycle at {datetime.now().strftime('%H:%M:%S')}")
|
||
|
|
client._request_count = 0 # Reset per-cycle counter
|
||
|
|
|
||
|
|
# Rotate through keywords to get diverse results
|
||
|
|
keyword = random.choice(SEARCH_KEYWORDS)
|
||
|
|
print(f"[Moltbook] Searching for: {keyword}")
|
||
|
|
|
||
|
|
posts = client.search_posts(keyword, limit=25)
|
||
|
|
if not posts:
|
||
|
|
# Fallback to feed
|
||
|
|
print("[Moltbook] No search results, checking feed...")
|
||
|
|
posts = client.get_feed(sort="new", limit=25)
|
||
|
|
|
||
|
|
if not posts:
|
||
|
|
print("[Moltbook] No posts found")
|
||
|
|
return
|
||
|
|
|
||
|
|
commented = 0
|
||
|
|
for post in posts:
|
||
|
|
if commented >= MAX_COMMENTS_PER_CYCLE:
|
||
|
|
break
|
||
|
|
|
||
|
|
post_id = str(post.get("id", post.get("_id", "")))
|
||
|
|
title = post.get("title", "")
|
||
|
|
|
||
|
|
if not post_id or not title:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip if already commented
|
||
|
|
if has_commented(post_id):
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip old posts
|
||
|
|
age = post_age_hours(post)
|
||
|
|
if age > POST_MAX_AGE_HOURS:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Check relevance
|
||
|
|
if not is_relevant(post):
|
||
|
|
continue
|
||
|
|
|
||
|
|
print(f"[Moltbook] Found relevant post: {title[:60]}... (age: {age:.1f}h)")
|
||
|
|
|
||
|
|
# Get existing comments for context
|
||
|
|
existing = client.get_comments(post_id)
|
||
|
|
|
||
|
|
# Check if we already have a comment there (belt + suspenders)
|
||
|
|
our_comments = [c for c in existing if (c.get("agent_name") or "").lower() == AGENT_NAME.lower()]
|
||
|
|
if our_comments:
|
||
|
|
save_comment(post_id, title, "(already commented)")
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Generate comment
|
||
|
|
body = post.get("body") or post.get("content") or ""
|
||
|
|
comment_text = generate_comment(title, body, existing)
|
||
|
|
|
||
|
|
# Claude said SKIP — not relevant enough (check BEFORE length check!)
|
||
|
|
if comment_text and comment_text.strip().upper() == "SKIP":
|
||
|
|
print(f"[Moltbook] Claude says SKIP — not maritime enough")
|
||
|
|
save_comment(post_id, title, "(skipped)") # Mark as processed so we don't retry
|
||
|
|
continue
|
||
|
|
|
||
|
|
if not comment_text or len(comment_text) < 20:
|
||
|
|
print(f"[Moltbook] Skipping — generated comment too short")
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Post comment
|
||
|
|
result = client.create_comment(post_id, comment_text)
|
||
|
|
if result and not result.get("error"):
|
||
|
|
print(f"[Moltbook] Commented on: {title[:60]}")
|
||
|
|
print(f"[Moltbook] Comment: {comment_text[:100]}...")
|
||
|
|
save_comment(post_id, title, comment_text)
|
||
|
|
|
||
|
|
# Also upvote the post (be nice)
|
||
|
|
client.upvote_post(post_id)
|
||
|
|
commented += 1
|
||
|
|
|
||
|
|
# Small delay between comments
|
||
|
|
time.sleep(5)
|
||
|
|
else:
|
||
|
|
print(f"[Moltbook] Failed to comment on post {post_id}")
|
||
|
|
|
||
|
|
print(f"[Moltbook] Cycle done. Commented on {commented} posts.")
|
||
|
|
|
||
|
|
|
||
|
|
def bot_loop(client: MoltbookClient):
|
||
|
|
"""Main bot loop — runs forever (stops on auth errors)."""
|
||
|
|
print("[Moltbook] Bot loop started")
|
||
|
|
|
||
|
|
while not client._stopped:
|
||
|
|
try:
|
||
|
|
bot_cycle(client)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[Moltbook] Cycle error: {e}")
|
||
|
|
|
||
|
|
if client._stopped:
|
||
|
|
print("[Moltbook] Bot stopped due to auth error")
|
||
|
|
break
|
||
|
|
|
||
|
|
# Wait for next cycle (with jitter to avoid predictable patterns)
|
||
|
|
jitter = random.randint(-120, 120) # ±2 min jitter
|
||
|
|
sleep_sec = LOOP_INTERVAL_SEC + jitter
|
||
|
|
print(f"[Moltbook] Sleeping {sleep_sec // 60} min until next cycle...")
|
||
|
|
time.sleep(sleep_sec)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# STARTUP
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
def ensure_agent_registered(client: MoltbookClient):
|
||
|
|
"""Register or verify agent profile on Moltbook."""
|
||
|
|
resp = client.get_profile()
|
||
|
|
# API returns {"success": true, "agent": {...}} or flat dict
|
||
|
|
profile = resp.get("agent", resp) if isinstance(resp, dict) else {}
|
||
|
|
|
||
|
|
if profile and profile.get("name"):
|
||
|
|
print(f"[Moltbook] Agent active: {profile['name']} (karma: {profile.get('karma', 0)})")
|
||
|
|
# Update description if needed
|
||
|
|
if profile.get("description", "") != AGENT_DESC:
|
||
|
|
client.update_profile(AGENT_DESC)
|
||
|
|
print("[Moltbook] Profile description updated")
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Try to register
|
||
|
|
print("[Moltbook] Registering agent...")
|
||
|
|
result = client.register_agent(AGENT_NAME, AGENT_DESC)
|
||
|
|
agent = result.get("agent", result) if isinstance(result, dict) else {}
|
||
|
|
if agent and agent.get("name"):
|
||
|
|
print(f"[Moltbook] Agent registered: {agent['name']}")
|
||
|
|
return True
|
||
|
|
|
||
|
|
print(f"[Moltbook] Registration result: {result}")
|
||
|
|
return True # Continue anyway — might already be registered
|
||
|
|
|
||
|
|
|
||
|
|
def start_moltbook_bot():
|
||
|
|
"""Start Moltbook bot as a background daemon thread.
|
||
|
|
Called from seafare_api.py on startup. Only one instance per process."""
|
||
|
|
if os.environ.get("_MOLTBOOK_STARTED"):
|
||
|
|
print("[Moltbook] Bot already started in this process — skipping")
|
||
|
|
return
|
||
|
|
os.environ["_MOLTBOOK_STARTED"] = "1"
|
||
|
|
|
||
|
|
api_key = os.environ.get("MOLTBOOK_API_KEY")
|
||
|
|
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
||
|
|
|
||
|
|
if not api_key:
|
||
|
|
print("[Moltbook] MOLTBOOK_API_KEY not set — bot disabled")
|
||
|
|
return
|
||
|
|
|
||
|
|
if not anthropic_key:
|
||
|
|
print("[Moltbook] ANTHROPIC_API_KEY not set — bot disabled")
|
||
|
|
return
|
||
|
|
|
||
|
|
client = MoltbookClient(api_key)
|
||
|
|
|
||
|
|
# Verify/register agent
|
||
|
|
ensure_agent_registered(client)
|
||
|
|
|
||
|
|
# Start background loop
|
||
|
|
t = threading.Thread(target=bot_loop, args=(client,), daemon=True)
|
||
|
|
t.start()
|
||
|
|
print("[Moltbook] Bot thread started (30 min cycle)")
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# STANDALONE RUN
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
if __name__ == "__main__":
|
||
|
|
from dotenv import load_dotenv
|
||
|
|
load_dotenv()
|
||
|
|
|
||
|
|
api_key = os.environ.get("MOLTBOOK_API_KEY")
|
||
|
|
if not api_key:
|
||
|
|
print("Set MOLTBOOK_API_KEY in .env")
|
||
|
|
exit(1)
|
||
|
|
|
||
|
|
client = MoltbookClient(api_key)
|
||
|
|
|
||
|
|
# Test: profile
|
||
|
|
print("\n--- Agent Profile ---")
|
||
|
|
ensure_agent_registered(client)
|
||
|
|
profile = client.get_profile()
|
||
|
|
print(json.dumps(profile, indent=2, default=str))
|
||
|
|
|
||
|
|
# Test: search
|
||
|
|
print("\n--- Search 'shipping' ---")
|
||
|
|
posts = client.search_posts("shipping", limit=5)
|
||
|
|
for p in posts[:5]:
|
||
|
|
pid = p.get("id", p.get("_id", "?"))
|
||
|
|
title = p.get("title", "?")
|
||
|
|
print(f" [{pid}] {title[:70]}")
|
||
|
|
|
||
|
|
# Test: single cycle
|
||
|
|
print("\n--- Running single bot cycle ---")
|
||
|
|
bot_cycle(client)
|
||
|
|
|
||
|
|
print("\nDone!")
|