mirror of
https://github.com/sudoxnym/connectd.git
synced 2026-04-14 11:37:42 +00:00
add lemmy authentication and DM delivery support
- add LEMMY_INSTANCE, LEMMY_USERNAME, LEMMY_PASSWORD env vars - add send_lemmy_dm() for private message delivery - add lemmy as activity-based contact method - user's instance prioritized in scraping 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
582475457c
commit
a5d7970b29
5 changed files with 138 additions and 2 deletions
|
|
@ -26,6 +26,11 @@ MATRIX_ACCESS_TOKEN=
|
|||
DISCORD_BOT_TOKEN=
|
||||
DISCORD_TARGET_SERVERS= # comma separated server IDs
|
||||
|
||||
# lemmy (for authenticated access to your instance)
|
||||
LEMMY_INSTANCE=
|
||||
LEMMY_USERNAME=
|
||||
LEMMY_PASSWORD=
|
||||
|
||||
# === EMAIL DELIVERY ===
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=465
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ 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'))
|
||||
|
|
|
|||
|
|
@ -395,6 +395,10 @@ def deliver_intro(match_data, intro_draft, dry_run=False):
|
|||
from scoutd.discord import send_discord_dm
|
||||
success, error = send_discord_dm(contact_info, intro_draft, dry_run)
|
||||
|
||||
elif method == 'lemmy':
|
||||
from scoutd.lemmy import send_lemmy_dm
|
||||
success, error = send_lemmy_dm(contact_info, intro_draft, dry_run)
|
||||
|
||||
elif method == 'github_issue':
|
||||
owner = contact_info.get('owner')
|
||||
repo = contact_info.get('repo')
|
||||
|
|
|
|||
|
|
@ -187,6 +187,32 @@ def determine_contact_method(human):
|
|||
if matrix_score > 0:
|
||||
activity_scores['matrix'] = {'score': matrix_score, 'info': matrix_id}
|
||||
|
||||
# lemmy activity (fediverse)
|
||||
lemmy_username = human.get('username') if human.get('platform') == 'lemmy' else extra.get('lemmy')
|
||||
if lemmy_username:
|
||||
lemmy_score = 0
|
||||
|
||||
# lemmy is fediverse - high values alignment
|
||||
lemmy_score += 20 # fediverse platform bonus
|
||||
|
||||
post_count = extra.get('post_count', 0)
|
||||
comment_count = extra.get('comment_count', 0)
|
||||
|
||||
if post_count > 100:
|
||||
lemmy_score += 15
|
||||
elif post_count > 50:
|
||||
lemmy_score += 10
|
||||
elif post_count > 10:
|
||||
lemmy_score += 5
|
||||
|
||||
if comment_count > 500:
|
||||
lemmy_score += 10
|
||||
elif comment_count > 100:
|
||||
lemmy_score += 5
|
||||
|
||||
if lemmy_score > 0:
|
||||
activity_scores['lemmy'] = {'score': lemmy_score, 'info': lemmy_username}
|
||||
|
||||
# pick highest activity platform
|
||||
if activity_scores:
|
||||
best_platform = max(activity_scores.items(), key=lambda x: x[1]['score'])
|
||||
|
|
|
|||
100
scoutd/lemmy.py
100
scoutd/lemmy.py
|
|
@ -7,7 +7,7 @@ great for finding lost builders in communities like:
|
|||
- /c/antiwork, /c/workreform (lost builders!)
|
||||
- /c/selfhosted, /c/privacy, /c/opensource
|
||||
|
||||
no auth needed for public posts.
|
||||
supports authenticated access for private instances and DM delivery.
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
|
@ -24,6 +24,14 @@ from .lost import (
|
|||
classify_user,
|
||||
)
|
||||
|
||||
# auth config from environment
|
||||
LEMMY_INSTANCE = os.environ.get('LEMMY_INSTANCE', '')
|
||||
LEMMY_USERNAME = os.environ.get('LEMMY_USERNAME', '')
|
||||
LEMMY_PASSWORD = os.environ.get('LEMMY_PASSWORD', '')
|
||||
|
||||
# auth token cache
|
||||
_auth_token = None
|
||||
|
||||
# popular lemmy instances
|
||||
LEMMY_INSTANCES = [
|
||||
'lemmy.ml',
|
||||
|
|
@ -60,6 +68,89 @@ CACHE_DIR = Path(__file__).parent.parent / 'db' / 'cache' / 'lemmy'
|
|||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_auth_token(instance=None):
|
||||
"""get auth token for lemmy instance"""
|
||||
global _auth_token
|
||||
|
||||
if _auth_token:
|
||||
return _auth_token
|
||||
|
||||
instance = instance or LEMMY_INSTANCE
|
||||
if not all([instance, LEMMY_USERNAME, LEMMY_PASSWORD]):
|
||||
return None
|
||||
|
||||
try:
|
||||
url = f"https://{instance}/api/v3/user/login"
|
||||
resp = requests.post(url, json={
|
||||
'username_or_email': LEMMY_USERNAME,
|
||||
'password': LEMMY_PASSWORD,
|
||||
}, timeout=30)
|
||||
|
||||
if resp.status_code == 200:
|
||||
_auth_token = resp.json().get('jwt')
|
||||
return _auth_token
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"lemmy auth error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def send_lemmy_dm(recipient_username, message, dry_run=False):
|
||||
"""send a private message via lemmy"""
|
||||
if not LEMMY_INSTANCE:
|
||||
return False, "LEMMY_INSTANCE not configured"
|
||||
|
||||
if dry_run:
|
||||
print(f"[dry run] would send lemmy DM to {recipient_username}")
|
||||
return True, None
|
||||
|
||||
token = get_auth_token()
|
||||
if not token:
|
||||
return False, "failed to authenticate with lemmy"
|
||||
|
||||
try:
|
||||
# parse recipient - could be username@instance or just username
|
||||
if '@' in recipient_username:
|
||||
username, instance = recipient_username.split('@', 1)
|
||||
else:
|
||||
username = recipient_username
|
||||
instance = LEMMY_INSTANCE
|
||||
|
||||
# get recipient user id
|
||||
user_url = f"https://{LEMMY_INSTANCE}/api/v3/user"
|
||||
resp = requests.get(user_url, params={'username': f"{username}@{instance}"}, timeout=30)
|
||||
|
||||
if resp.status_code != 200:
|
||||
# try without instance suffix for local users
|
||||
resp = requests.get(user_url, params={'username': username}, timeout=30)
|
||||
|
||||
if resp.status_code != 200:
|
||||
return False, f"could not find user {recipient_username}"
|
||||
|
||||
recipient_id = resp.json().get('person_view', {}).get('person', {}).get('id')
|
||||
if not recipient_id:
|
||||
return False, "could not get recipient id"
|
||||
|
||||
# send DM
|
||||
dm_url = f"https://{LEMMY_INSTANCE}/api/v3/private_message"
|
||||
resp = requests.post(dm_url,
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
json={
|
||||
'content': message,
|
||||
'recipient_id': recipient_id,
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
return True, None
|
||||
else:
|
||||
return False, f"lemmy DM error: {resp.status_code} - {resp.text}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"lemmy DM error: {str(e)}"
|
||||
|
||||
|
||||
def get_community_posts(instance, community, limit=50, sort='New'):
|
||||
"""get posts from a lemmy community"""
|
||||
try:
|
||||
|
|
@ -167,7 +258,12 @@ def scrape_lemmy(db, limit_per_community=30):
|
|||
lost_found = 0
|
||||
seen_users = set()
|
||||
|
||||
for instance in LEMMY_INSTANCES:
|
||||
# build instance list - user's instance first if configured
|
||||
instances = list(LEMMY_INSTANCES)
|
||||
if LEMMY_INSTANCE and LEMMY_INSTANCE not in instances:
|
||||
instances.insert(0, LEMMY_INSTANCE)
|
||||
|
||||
for instance in instances:
|
||||
print(f" instance: {instance}")
|
||||
|
||||
for community in TARGET_COMMUNITIES:
|
||||
|
|
|
|||
Loading…
Reference in a new issue