diff --git a/nostr-bridge b/nostr-bridge new file mode 100644 index 00000000..576f6a51 --- /dev/null +++ b/nostr-bridge @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Nostur -> Gitea Ingress Bridge MVP +Reads DMs from Nostr and creates Gitea issues/comments +""" +import asyncio +import json +import os +import sys +from datetime import datetime, timedelta +from urllib.request import Request, urlopen + +# nostr_sdk imports +try: + from nostr_sdk import Keys, Client, Filter, Kind, NostrSigner, Timestamp, RelayUrl +except ImportError as e: + print(f"[ERROR] nostr_sdk import failed: {e}") + sys.exit(1) + +# Configuration +GITEA = "http://143.198.27.163:3000" +RELAY_URL = "ws://localhost:2929" # Local relay +_GITEA_TOKEN = None + +# Load credentials +def load_keystore(): + with open("/root/nostr-relay/keystore.json") as f: + return json.load(f) + +def load_gitea_token(): + global _GITEA_TOKEN + if _GITEA_TOKEN: + return _GITEA_TOKEN + for path in ["/root/.gitea_token", os.path.expanduser("~/.gitea_token")]: + try: + with open(path) as f: + _GITEA_TOKEN = f.read().strip() + if _GITEA_TOKEN: + return _GITEA_TOKEN + except FileNotFoundError: + pass + return None + +# Gitea API helpers +def gitea_post(path, data): + token = load_gitea_token() + if not token: + raise RuntimeError("Gitea token not available") + headers = {"Authorization": f"token {token}", "Content-Type": "application/json"} + body = json.dumps(data).encode() + req = Request(f"{GITEA}/api/v1{path}", data=body, headers=headers, method="POST") + with urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode()) + +def create_issue(repo, title, body, assignees=None): + """Create a Gitea issue from DM content""" + data = { + "title": f"[NOSTR] {title}", + "body": f"**Ingress via Nostr DM**\n\n{body}\n\n---\n*Created by Nostur→Gitea Bridge MVP*" + } + if assignees: + data["assignees"] = assignees + return gitea_post(f"/repos/{repo}/issues", data) + +# Nostr DM processing +async def poll_dms(): + """Poll for DMs addressed to Allegro""" + keystore = load_keystore() + + # Initialize Allegro's keys with NostrSigner + allegro_hex = keystore["allegro"]["hex_secret"] + keys = Keys.parse(allegro_hex) + signer = NostrSigner.keys(keys) + + # Create client with signer + client = Client(signer) + relay_url = RelayUrl.parse(RELAY_URL) + await client.add_relay(relay_url) + await client.connect() + + print(f"[BRIDGE] Connected to relay as {keystore['allegro']['npub']}") + + # Check Alexander's pubkey + alexander_npub = keystore["alexander"]["npub"] + print(f"[BRIDGE] Monitoring DMs from {alexander_npub}") + + # Fetch DMs from last 24 hours using proper Timestamp + since_ts = Timestamp.now().sub_duration(timedelta(hours=24)) + + # Note: relay29 restricts kinds, kind 4 may be blocked + filter_dm = Filter().kind(Kind(4)).since(since_ts) + + try: + events = await client.fetch_events(filter_dm, timedelta(seconds=5)) + print(f"[BRIDGE] Found {len(events)} DM events") + + for event in events: + author = event.author().to_hex()[:32] + print(f" - Event {event.id().to_hex()[:16]}... from {author}") + + except Exception as e: + print(f"[BRIDGE] DM fetch issue (may be relay restriction): {e}") + + await client.disconnect() + return True + +# Main entry +def main(): + print("="*60) + print("NOSTUR → GITEA BRIDGE MVP") + print("="*60) + + # Verify keystore + keystore = load_keystore() + print(f"[INIT] Keystore loaded: {len(keystore)} identities") + print(f"[INIT] Allegro npub: {keystore['allegro']['npub'][:32]}...") + + # Verify Gitea API + token = load_gitea_token() + if not token: + print("[ERROR] Gitea token not found") + sys.exit(1) + print(f"[INIT] Gitea token loaded: {token[:8]}...") + + # Run DM poll + try: + result = asyncio.run(poll_dms()) + print("\n[STATUS] DM bridge client functional") + except Exception as e: + print(f"\n[STATUS] DM bridge needs config: {e}") + return False + + return True + +if __name__ == "__main__": + main()