mirror of
https://github.com/sudoxnym/connectd.git
synced 2026-04-14 11:37:42 +00:00
- determine_contact_method now recognizes mastodon/bluesky users by platform - username IS the handle for platform-native users - fixed orphaned matches table issue - wave 1 intros sent successfully
216 lines
5.8 KiB
Python
216 lines
5.8 KiB
Python
"""
|
|
introd/send.py - actually deliver intros via appropriate channel
|
|
"""
|
|
|
|
import smtplib
|
|
import requests
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from datetime import datetime
|
|
import os
|
|
|
|
# email config (from env)
|
|
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', '')
|
|
FROM_EMAIL = os.environ.get('FROM_EMAIL', '')
|
|
|
|
|
|
def send_email(to_email, subject, body):
|
|
"""send email via SMTP"""
|
|
msg = MIMEMultipart()
|
|
msg['From'] = FROM_EMAIL
|
|
msg['To'] = to_email
|
|
msg['Subject'] = subject
|
|
|
|
msg.attach(MIMEText(body, 'plain'))
|
|
|
|
try:
|
|
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
|
|
server.login(SMTP_USER, SMTP_PASS)
|
|
server.send_message(msg)
|
|
return True, None
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
|
|
def send_github_issue(repo_url, title, body):
|
|
"""
|
|
create a github issue (requires GITHUB_TOKEN)
|
|
note: only works if you have write access to the repo
|
|
typically won't work for random users - fallback to manual
|
|
"""
|
|
# extract owner/repo from url
|
|
# https://github.com/owner/repo -> owner/repo
|
|
parts = repo_url.rstrip('/').split('/')
|
|
if len(parts) < 2:
|
|
return False, "invalid github url"
|
|
|
|
owner = parts[-2]
|
|
repo = parts[-1]
|
|
|
|
token = os.environ.get('GITHUB_TOKEN')
|
|
if not token:
|
|
return False, "no github token"
|
|
|
|
# would create issue via API - but this is invasive
|
|
# better to just output the info for manual action
|
|
return False, "github issues not automated - use manual outreach"
|
|
|
|
|
|
def send_mastodon_dm(instance, username, message):
|
|
"""
|
|
send mastodon DM (requires account credentials)
|
|
not implemented - requires oauth setup
|
|
"""
|
|
return False, "mastodon DMs not automated - use manual outreach"
|
|
|
|
|
|
def send_reddit_message(username, subject, body):
|
|
"""
|
|
send reddit message (requires account credentials)
|
|
not implemented - requires oauth setup
|
|
"""
|
|
return False, "reddit messages not automated - use manual outreach"
|
|
|
|
|
|
def send_intro(db, intro_id):
|
|
"""
|
|
send an approved intro
|
|
|
|
returns: (success, error_message)
|
|
"""
|
|
# get intro from db
|
|
c = db.conn.cursor()
|
|
c.execute('SELECT * FROM intros WHERE id = ?', (intro_id,))
|
|
row = c.fetchone()
|
|
|
|
if not row:
|
|
return False, "intro not found"
|
|
|
|
intro = dict(row)
|
|
|
|
if intro['status'] != 'approved':
|
|
return False, f"intro not approved (status: {intro['status']})"
|
|
|
|
channel = intro.get('channel')
|
|
draft = intro.get('draft')
|
|
|
|
# get recipient info
|
|
recipient = db.get_human_by_id(intro['recipient_human_id'])
|
|
if not recipient:
|
|
return False, "recipient not found"
|
|
|
|
success = False
|
|
error = None
|
|
|
|
if channel == 'email':
|
|
# get email from contact
|
|
import json
|
|
contact = recipient.get('contact', {})
|
|
if isinstance(contact, str):
|
|
contact = json.loads(contact)
|
|
|
|
email = contact.get('email')
|
|
if email:
|
|
success, error = send_email(
|
|
email,
|
|
"connection: aligned builder intro",
|
|
draft
|
|
)
|
|
else:
|
|
error = "no email address"
|
|
|
|
elif channel == 'github':
|
|
success, error = send_github_issue(
|
|
recipient.get('url'),
|
|
"connection: aligned builder intro",
|
|
draft
|
|
)
|
|
|
|
elif channel == 'mastodon':
|
|
success, error = send_mastodon_dm(
|
|
recipient.get('instance'),
|
|
recipient.get('username'),
|
|
draft
|
|
)
|
|
|
|
elif channel == 'reddit':
|
|
success, error = send_reddit_message(
|
|
recipient.get('username'),
|
|
"connection: aligned builder intro",
|
|
draft
|
|
)
|
|
|
|
else:
|
|
error = f"unknown channel: {channel}"
|
|
|
|
# update status
|
|
if success:
|
|
db.mark_intro_sent(intro_id)
|
|
print(f"introd: sent intro {intro_id} via {channel}")
|
|
else:
|
|
# mark as needs manual sending
|
|
c.execute('''UPDATE intros SET status = 'manual_needed',
|
|
approved_at = ? WHERE id = ?''',
|
|
(datetime.now().isoformat(), intro_id))
|
|
db.conn.commit()
|
|
print(f"introd: intro {intro_id} needs manual send ({error})")
|
|
|
|
return success, error
|
|
|
|
|
|
def send_all_approved(db):
|
|
"""
|
|
send all approved intros
|
|
"""
|
|
c = db.conn.cursor()
|
|
c.execute('SELECT id FROM intros WHERE status = "approved"')
|
|
rows = c.fetchall()
|
|
|
|
if not rows:
|
|
print("no approved intros to send")
|
|
return
|
|
|
|
print(f"sending {len(rows)} approved intros...")
|
|
|
|
sent = 0
|
|
failed = 0
|
|
|
|
for row in rows:
|
|
success, error = send_intro(db, row['id'])
|
|
if success:
|
|
sent += 1
|
|
else:
|
|
failed += 1
|
|
|
|
print(f"sent: {sent}, failed/manual: {failed}")
|
|
|
|
|
|
def export_manual_intros(db, output_file='manual_intros.txt'):
|
|
"""
|
|
export intros that need manual sending to a text file
|
|
"""
|
|
c = db.conn.cursor()
|
|
c.execute('''SELECT i.*, h.username, h.platform, h.url
|
|
FROM intros i
|
|
JOIN humans h ON i.recipient_human_id = h.id
|
|
WHERE i.status IN ('approved', 'manual_needed')''')
|
|
rows = c.fetchall()
|
|
|
|
if not rows:
|
|
print("no intros to export")
|
|
return
|
|
|
|
with open(output_file, 'w') as f:
|
|
for row in rows:
|
|
f.write("=" * 60 + "\n")
|
|
f.write(f"TO: {row['username']} ({row['platform']})\n")
|
|
f.write(f"URL: {row['url']}\n")
|
|
f.write(f"CHANNEL: {row['channel']}\n")
|
|
f.write("-" * 60 + "\n")
|
|
f.write(row['draft'] + "\n")
|
|
f.write("\n")
|
|
|
|
print(f"exported {len(rows)} intros to {output_file}")
|