mirror of
https://github.com/sudoxnym/connectd.git
synced 2026-04-14 19:46:30 +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_BOT_TOKEN=
|
||||||
DISCORD_TARGET_SERVERS= # comma separated server IDs
|
DISCORD_TARGET_SERVERS= # comma separated server IDs
|
||||||
|
|
||||||
|
# lemmy (for authenticated access to your instance)
|
||||||
|
LEMMY_INSTANCE=
|
||||||
|
LEMMY_USERNAME=
|
||||||
|
LEMMY_PASSWORD=
|
||||||
|
|
||||||
# === EMAIL DELIVERY ===
|
# === EMAIL DELIVERY ===
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=465
|
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_BOT_TOKEN = os.environ.get('DISCORD_BOT_TOKEN', '')
|
||||||
DISCORD_TARGET_SERVERS = os.environ.get('DISCORD_TARGET_SERVERS', '')
|
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)
|
# email (for sending intros)
|
||||||
SMTP_HOST = os.environ.get('SMTP_HOST', '')
|
SMTP_HOST = os.environ.get('SMTP_HOST', '')
|
||||||
SMTP_PORT = int(os.environ.get('SMTP_PORT', '465'))
|
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
|
from scoutd.discord import send_discord_dm
|
||||||
success, error = send_discord_dm(contact_info, intro_draft, dry_run)
|
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':
|
elif method == 'github_issue':
|
||||||
owner = contact_info.get('owner')
|
owner = contact_info.get('owner')
|
||||||
repo = contact_info.get('repo')
|
repo = contact_info.get('repo')
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,32 @@ def determine_contact_method(human):
|
||||||
if matrix_score > 0:
|
if matrix_score > 0:
|
||||||
activity_scores['matrix'] = {'score': matrix_score, 'info': matrix_id}
|
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
|
# pick highest activity platform
|
||||||
if activity_scores:
|
if activity_scores:
|
||||||
best_platform = max(activity_scores.items(), key=lambda x: x[1]['score'])
|
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/antiwork, /c/workreform (lost builders!)
|
||||||
- /c/selfhosted, /c/privacy, /c/opensource
|
- /c/selfhosted, /c/privacy, /c/opensource
|
||||||
|
|
||||||
no auth needed for public posts.
|
supports authenticated access for private instances and DM delivery.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -24,6 +24,14 @@ from .lost import (
|
||||||
classify_user,
|
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
|
# popular lemmy instances
|
||||||
LEMMY_INSTANCES = [
|
LEMMY_INSTANCES = [
|
||||||
'lemmy.ml',
|
'lemmy.ml',
|
||||||
|
|
@ -60,6 +68,89 @@ CACHE_DIR = Path(__file__).parent.parent / 'db' / 'cache' / 'lemmy'
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
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'):
|
def get_community_posts(instance, community, limit=50, sort='New'):
|
||||||
"""get posts from a lemmy community"""
|
"""get posts from a lemmy community"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -167,7 +258,12 @@ def scrape_lemmy(db, limit_per_community=30):
|
||||||
lost_found = 0
|
lost_found = 0
|
||||||
seen_users = set()
|
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}")
|
print(f" instance: {instance}")
|
||||||
|
|
||||||
for community in TARGET_COMMUNITIES:
|
for community in TARGET_COMMUNITIES:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue