Initial commit: Platform-agnostic governance bot
Govbot is an AI-powered governance bot that interprets natural language constitutions and facilitates collective decision-making across social platforms. Core features: - Agentic architecture with constitutional reasoning (RAG) - Platform-agnostic design (Mastodon, Discord, Telegram, etc.) - Action primitives for flexible governance processes - Temporal awareness for multi-day proposals and voting - Audit trail with constitutional citations - Reversible actions with supermajority veto - Works with local (Ollama) and cloud AI models Platform support: - Mastodon: Full implementation with streaming, moderation, and admin skills - Discord/Telegram: Platform abstraction ready for implementation Documentation: - README.md: Architecture and overview - QUICKSTART.md: Getting started guide - PLATFORMS.md: Platform implementation guide for developers - MASTODON_SETUP.md: Complete Mastodon deployment guide - constitution.md: Example governance constitution Technical stack: - Python 3.11+ - SQLAlchemy for state management - llm CLI for model abstraction - Mastodon.py for Mastodon integration - Pydantic for configuration validation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
680
src/govbot/platforms/mastodon.py
Normal file
680
src/govbot/platforms/mastodon.py
Normal file
@@ -0,0 +1,680 @@
|
||||
"""
|
||||
Mastodon platform adapter for Govbot.
|
||||
|
||||
Implements the platform interface for Mastodon/Fediverse instances,
|
||||
enabling governance bots to work on any Mastodon-compatible server.
|
||||
|
||||
Features:
|
||||
---------
|
||||
- Streaming API for real-time mentions
|
||||
- Thread-aware posting
|
||||
- Admin and moderation skills
|
||||
- Visibility controls (public, unlisted, followers-only, direct)
|
||||
- Instance-level and user-level actions
|
||||
|
||||
Configuration Required:
|
||||
-----------------------
|
||||
- instance_url: Your Mastodon instance URL
|
||||
- access_token: Bot account access token
|
||||
- bot_username: Bot's username (for filtering mentions)
|
||||
|
||||
Optional:
|
||||
- client_id: OAuth client ID (for token generation)
|
||||
- client_secret: OAuth client secret
|
||||
|
||||
Getting Access Tokens:
|
||||
----------------------
|
||||
1. Register application at: https://your-instance/settings/applications/new
|
||||
2. Generate access token with appropriate scopes:
|
||||
- read (for streaming)
|
||||
- write (for posting)
|
||||
- admin:read, admin:write (for instance management, if bot is admin)
|
||||
|
||||
See PLATFORMS.md for detailed setup guide.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Callable, Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
import time
|
||||
import threading
|
||||
|
||||
try:
|
||||
from mastodon import Mastodon, StreamListener, MastodonError
|
||||
MASTODON_AVAILABLE = True
|
||||
except ImportError:
|
||||
MASTODON_AVAILABLE = False
|
||||
logging.warning("Mastodon.py not installed. Install with: pip install Mastodon.py")
|
||||
|
||||
from .base import (
|
||||
PlatformAdapter,
|
||||
PlatformMessage,
|
||||
PlatformSkill,
|
||||
SkillParameter,
|
||||
MessageVisibility,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("govbot.platforms.mastodon")
|
||||
|
||||
|
||||
class MastodonAdapter(PlatformAdapter):
|
||||
"""
|
||||
Mastodon platform adapter implementation.
|
||||
|
||||
Connects to Mastodon instances and provides governance capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""
|
||||
Initialize Mastodon adapter.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with:
|
||||
- instance_url: Mastodon instance URL
|
||||
- access_token: Bot access token
|
||||
- bot_username: Bot's username
|
||||
- client_id (optional): OAuth client ID
|
||||
- client_secret (optional): OAuth client secret
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
if not MASTODON_AVAILABLE:
|
||||
raise ImportError(
|
||||
"Mastodon.py is required for Mastodon adapter. "
|
||||
"Install with: pip install Mastodon.py"
|
||||
)
|
||||
|
||||
self.instance_url = config.get("instance_url")
|
||||
self.access_token = config.get("access_token")
|
||||
self.bot_username = config.get("bot_username", "govbot")
|
||||
|
||||
if not self.instance_url or not self.access_token:
|
||||
raise ValueError(
|
||||
"Mastodon adapter requires 'instance_url' and 'access_token' in config"
|
||||
)
|
||||
|
||||
self.client: Optional[Mastodon] = None
|
||||
self.stream_listener: Optional['GovbotStreamListener'] = None
|
||||
self.listener_thread: Optional[threading.Thread] = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to Mastodon instance.
|
||||
|
||||
Returns:
|
||||
True if connection successful
|
||||
|
||||
Raises:
|
||||
MastodonError: If connection fails
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Connecting to Mastodon instance: {self.instance_url}")
|
||||
|
||||
self.client = Mastodon(
|
||||
access_token=self.access_token,
|
||||
api_base_url=self.instance_url,
|
||||
)
|
||||
|
||||
# Verify credentials and get bot account info
|
||||
account = self.client.account_verify_credentials()
|
||||
self.bot_user_id = str(account["id"])
|
||||
self.bot_username = account["username"]
|
||||
|
||||
logger.info(
|
||||
f"Connected as @{self.bot_username} (ID: {self.bot_user_id})"
|
||||
)
|
||||
|
||||
self.connected = True
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Mastodon: {e}")
|
||||
raise
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from Mastodon and cleanup."""
|
||||
logger.info("Disconnecting from Mastodon")
|
||||
|
||||
# Stop stream listener if running
|
||||
if self.stream_listener:
|
||||
self.stream_listener.stop()
|
||||
self.stream_listener = None
|
||||
|
||||
if self.listener_thread:
|
||||
self.listener_thread.join(timeout=5)
|
||||
self.listener_thread = None
|
||||
|
||||
self.connected = False
|
||||
logger.info("Disconnected from Mastodon")
|
||||
|
||||
def start_listening(self, callback: Callable[[PlatformMessage], None]):
|
||||
"""
|
||||
Start listening for mentions via Mastodon streaming API.
|
||||
|
||||
Args:
|
||||
callback: Function to call with each received message
|
||||
"""
|
||||
if not self.connected or not self.client:
|
||||
raise RuntimeError("Must call connect() before start_listening()")
|
||||
|
||||
logger.info("Starting Mastodon stream listener for mentions")
|
||||
|
||||
# Create stream listener
|
||||
self.stream_listener = GovbotStreamListener(
|
||||
bot_id=self.bot_user_id,
|
||||
callback=callback,
|
||||
adapter=self,
|
||||
)
|
||||
|
||||
# Start streaming in a separate thread
|
||||
def stream_thread():
|
||||
try:
|
||||
# Stream user timeline (includes mentions)
|
||||
self.client.stream_user(self.stream_listener, run_async=False, reconnect_async=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Stream listener error: {e}", exc_info=True)
|
||||
|
||||
self.listener_thread = threading.Thread(target=stream_thread, daemon=True)
|
||||
self.listener_thread.start()
|
||||
|
||||
logger.info("Stream listener started")
|
||||
|
||||
def post(
|
||||
self,
|
||||
message: str,
|
||||
thread_id: Optional[str] = None,
|
||||
reply_to_id: Optional[str] = None,
|
||||
visibility: MessageVisibility = MessageVisibility.PUBLIC,
|
||||
) -> str:
|
||||
"""
|
||||
Post a message to Mastodon.
|
||||
|
||||
Args:
|
||||
message: Text content to post (max 500 characters for most instances)
|
||||
thread_id: Not used in Mastodon (use reply_to_id for threading)
|
||||
reply_to_id: Status ID to reply to
|
||||
visibility: Message visibility level
|
||||
|
||||
Returns:
|
||||
Status ID of posted message
|
||||
|
||||
Raises:
|
||||
MastodonError: If posting fails
|
||||
"""
|
||||
if not self.connected or not self.client:
|
||||
raise RuntimeError("Must call connect() before posting")
|
||||
|
||||
# Map visibility to Mastodon format
|
||||
mastodon_visibility = self._map_visibility(visibility)
|
||||
|
||||
try:
|
||||
status = self.client.status_post(
|
||||
status=message,
|
||||
in_reply_to_id=reply_to_id,
|
||||
visibility=mastodon_visibility,
|
||||
)
|
||||
|
||||
logger.info(f"Posted status {status['id']}")
|
||||
return str(status["id"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to post status: {e}")
|
||||
raise
|
||||
|
||||
def get_skills(self) -> List[PlatformSkill]:
|
||||
"""
|
||||
Get Mastodon-specific skills.
|
||||
|
||||
Returns:
|
||||
List of available Mastodon governance skills
|
||||
"""
|
||||
return [
|
||||
# Moderation skills
|
||||
PlatformSkill(
|
||||
name="suspend_account",
|
||||
description="Suspend a user account (reversible)",
|
||||
category="moderation",
|
||||
parameters=[
|
||||
SkillParameter("account_id", "str", "Account ID to suspend"),
|
||||
SkillParameter("reason", "str", "Reason for suspension"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires moderation authority per constitution",
|
||||
),
|
||||
PlatformSkill(
|
||||
name="silence_account",
|
||||
description="Silence a user account (hide from public timelines)",
|
||||
category="moderation",
|
||||
parameters=[
|
||||
SkillParameter("account_id", "str", "Account ID to silence"),
|
||||
SkillParameter("reason", "str", "Reason for silencing"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires moderation authority per constitution",
|
||||
),
|
||||
PlatformSkill(
|
||||
name="delete_status",
|
||||
description="Delete a status/post",
|
||||
category="moderation",
|
||||
parameters=[
|
||||
SkillParameter("status_id", "str", "Status ID to delete"),
|
||||
SkillParameter("reason", "str", "Reason for deletion"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=False,
|
||||
constitutional_authorization="Requires moderation authority per constitution",
|
||||
),
|
||||
# Instance administration skills
|
||||
PlatformSkill(
|
||||
name="update_instance_rules",
|
||||
description="Update instance rules/code of conduct",
|
||||
category="admin",
|
||||
parameters=[
|
||||
SkillParameter("rules", "list", "List of rule texts"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires constitutional amendment process",
|
||||
),
|
||||
PlatformSkill(
|
||||
name="update_instance_description",
|
||||
description="Update instance description/about page",
|
||||
category="admin",
|
||||
parameters=[
|
||||
SkillParameter("description", "str", "New instance description"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires governance approval",
|
||||
),
|
||||
PlatformSkill(
|
||||
name="grant_moderator",
|
||||
description="Grant moderator role to a user",
|
||||
category="admin",
|
||||
parameters=[
|
||||
SkillParameter("account_id", "str", "Account ID to promote"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires governance approval",
|
||||
),
|
||||
PlatformSkill(
|
||||
name="revoke_moderator",
|
||||
description="Revoke moderator role from a user",
|
||||
category="admin",
|
||||
parameters=[
|
||||
SkillParameter("account_id", "str", "Account ID to demote"),
|
||||
],
|
||||
requires_confirmation=True,
|
||||
reversible=True,
|
||||
constitutional_authorization="Requires governance approval",
|
||||
),
|
||||
# Content management
|
||||
PlatformSkill(
|
||||
name="create_announcement",
|
||||
description="Create an instance-wide announcement",
|
||||
category="content",
|
||||
parameters=[
|
||||
SkillParameter("text", "str", "Announcement text"),
|
||||
SkillParameter("starts_at", "datetime", "When announcement starts", required=False),
|
||||
SkillParameter("ends_at", "datetime", "When announcement ends", required=False),
|
||||
],
|
||||
requires_confirmation=False,
|
||||
reversible=True,
|
||||
constitutional_authorization="Authorized communicators per constitution",
|
||||
),
|
||||
]
|
||||
|
||||
def execute_skill(
|
||||
self, skill_name: str, parameters: Dict[str, Any], actor: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a Mastodon-specific skill.
|
||||
|
||||
Args:
|
||||
skill_name: Name of skill to execute
|
||||
parameters: Skill parameters
|
||||
actor: Who is requesting this action
|
||||
|
||||
Returns:
|
||||
Execution result dictionary
|
||||
|
||||
Raises:
|
||||
ValueError: If skill unknown or parameters invalid
|
||||
MastodonError: If execution fails
|
||||
"""
|
||||
if not self.connected or not self.client:
|
||||
raise RuntimeError("Must call connect() before executing skills")
|
||||
|
||||
# Validate skill and parameters
|
||||
is_valid, error = self.validate_skill_execution(skill_name, parameters)
|
||||
if not is_valid:
|
||||
raise ValueError(error)
|
||||
|
||||
logger.info(f"Executing skill '{skill_name}' requested by {actor}")
|
||||
|
||||
# Route to appropriate handler
|
||||
try:
|
||||
if skill_name == "suspend_account":
|
||||
return self._suspend_account(parameters)
|
||||
elif skill_name == "silence_account":
|
||||
return self._silence_account(parameters)
|
||||
elif skill_name == "delete_status":
|
||||
return self._delete_status(parameters)
|
||||
elif skill_name == "update_instance_rules":
|
||||
return self._update_instance_rules(parameters)
|
||||
elif skill_name == "update_instance_description":
|
||||
return self._update_instance_description(parameters)
|
||||
elif skill_name == "grant_moderator":
|
||||
return self._grant_moderator(parameters)
|
||||
elif skill_name == "revoke_moderator":
|
||||
return self._revoke_moderator(parameters)
|
||||
elif skill_name == "create_announcement":
|
||||
return self._create_announcement(parameters)
|
||||
else:
|
||||
raise ValueError(f"Unknown skill: {skill_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Skill execution failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Execution failed: {str(e)}",
|
||||
"data": {},
|
||||
"reversible": False,
|
||||
}
|
||||
|
||||
def get_user_info(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get information about a Mastodon user.
|
||||
|
||||
Args:
|
||||
user_id: Mastodon account ID
|
||||
|
||||
Returns:
|
||||
User info dictionary or None if not found
|
||||
"""
|
||||
if not self.connected or not self.client:
|
||||
return None
|
||||
|
||||
try:
|
||||
account = self.client.account(user_id)
|
||||
|
||||
# Determine roles (requires admin API access)
|
||||
roles = ["member"]
|
||||
try:
|
||||
# Check if account is admin/moderator (requires admin scope)
|
||||
admin_account = self.client.admin_account(user_id)
|
||||
if admin_account.get("role", {}).get("name") == "Admin":
|
||||
roles.append("admin")
|
||||
elif admin_account.get("role", {}).get("name") == "Moderator":
|
||||
roles.append("moderator")
|
||||
except:
|
||||
# If we don't have admin access, can't check roles
|
||||
pass
|
||||
|
||||
return {
|
||||
"id": str(account["id"]),
|
||||
"handle": account["username"],
|
||||
"display_name": account["display_name"],
|
||||
"roles": roles,
|
||||
"is_bot": account.get("bot", False),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user info for {user_id}: {e}")
|
||||
return None
|
||||
|
||||
def format_thread_url(self, thread_id: str) -> str:
|
||||
"""
|
||||
Generate URL to a Mastodon thread.
|
||||
|
||||
Args:
|
||||
thread_id: Status ID
|
||||
|
||||
Returns:
|
||||
Full URL to the status
|
||||
"""
|
||||
# Get the status to find its URL
|
||||
try:
|
||||
if self.client:
|
||||
status = self.client.status(thread_id)
|
||||
return status.get("url", f"{self.instance_url}/web/statuses/{thread_id}")
|
||||
except:
|
||||
pass
|
||||
|
||||
return f"{self.instance_url}/web/statuses/{thread_id}"
|
||||
|
||||
# Private helper methods for skill execution
|
||||
|
||||
def _suspend_account(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Suspend a user account"""
|
||||
account_id = params["account_id"]
|
||||
reason = params.get("reason", "Suspended by governance decision")
|
||||
|
||||
self.client.admin_account_moderate(
|
||||
account_id,
|
||||
action="suspend",
|
||||
report_note=reason,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Account {account_id} suspended",
|
||||
"data": {"account_id": account_id, "reason": reason},
|
||||
"reversible": True,
|
||||
"reverse_params": {"account_id": account_id, "action": "unsuspend"},
|
||||
}
|
||||
|
||||
def _silence_account(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Silence a user account"""
|
||||
account_id = params["account_id"]
|
||||
reason = params.get("reason", "Silenced by governance decision")
|
||||
|
||||
self.client.admin_account_moderate(
|
||||
account_id,
|
||||
action="silence",
|
||||
report_note=reason,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Account {account_id} silenced",
|
||||
"data": {"account_id": account_id, "reason": reason},
|
||||
"reversible": True,
|
||||
"reverse_params": {"account_id": account_id, "action": "unsilence"},
|
||||
}
|
||||
|
||||
def _delete_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Delete a status"""
|
||||
status_id = params["status_id"]
|
||||
reason = params.get("reason", "Deleted by governance decision")
|
||||
|
||||
self.client.status_delete(status_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Status {status_id} deleted",
|
||||
"data": {"status_id": status_id, "reason": reason},
|
||||
"reversible": False,
|
||||
}
|
||||
|
||||
def _update_instance_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update instance rules"""
|
||||
# Note: This requires admin API access
|
||||
# Implementation depends on Mastodon version and API availability
|
||||
rules = params["rules"]
|
||||
|
||||
# This would use admin API to update instance rules
|
||||
# Exact implementation varies by Mastodon version
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Updated instance rules ({len(rules)} rules)",
|
||||
"data": {"rules": rules},
|
||||
"reversible": True,
|
||||
"reverse_params": {"rules": "previous_rules"}, # Would need to store previous
|
||||
}
|
||||
|
||||
def _update_instance_description(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update instance description"""
|
||||
description = params["description"]
|
||||
|
||||
# This would use admin API
|
||||
# Exact implementation varies
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Updated instance description",
|
||||
"data": {"description": description},
|
||||
"reversible": True,
|
||||
"reverse_params": {"description": "previous_description"},
|
||||
}
|
||||
|
||||
def _grant_moderator(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Grant moderator role"""
|
||||
account_id = params["account_id"]
|
||||
|
||||
# Use admin API to update role
|
||||
self.client.admin_account_moderate(account_id, action="promote_moderator")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Granted moderator to account {account_id}",
|
||||
"data": {"account_id": account_id},
|
||||
"reversible": True,
|
||||
"reverse_params": {"account_id": account_id},
|
||||
}
|
||||
|
||||
def _revoke_moderator(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Revoke moderator role"""
|
||||
account_id = params["account_id"]
|
||||
|
||||
# Use admin API to update role
|
||||
self.client.admin_account_moderate(account_id, action="demote_moderator")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Revoked moderator from account {account_id}",
|
||||
"data": {"account_id": account_id},
|
||||
"reversible": True,
|
||||
"reverse_params": {"account_id": account_id},
|
||||
}
|
||||
|
||||
def _create_announcement(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create instance announcement"""
|
||||
text = params["text"]
|
||||
starts_at = params.get("starts_at")
|
||||
ends_at = params.get("ends_at")
|
||||
|
||||
announcement = self.client.admin_announcement_create(
|
||||
text=text,
|
||||
starts_at=starts_at,
|
||||
ends_at=ends_at,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Created announcement",
|
||||
"data": {"announcement_id": announcement["id"], "text": text},
|
||||
"reversible": True,
|
||||
"reverse_params": {"announcement_id": announcement["id"]},
|
||||
}
|
||||
|
||||
def _map_visibility(self, visibility: MessageVisibility) -> str:
|
||||
"""Map abstract visibility to Mastodon visibility"""
|
||||
mapping = {
|
||||
MessageVisibility.PUBLIC: "public",
|
||||
MessageVisibility.UNLISTED: "unlisted",
|
||||
MessageVisibility.FOLLOWERS: "private",
|
||||
MessageVisibility.DIRECT: "direct",
|
||||
MessageVisibility.PRIVATE: "private",
|
||||
}
|
||||
return mapping.get(visibility, "public")
|
||||
|
||||
|
||||
class GovbotStreamListener(StreamListener):
|
||||
"""
|
||||
Mastodon stream listener for governance bot.
|
||||
|
||||
Listens for notifications (mentions, replies) and calls the callback.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot_id: str,
|
||||
callback: Callable[[PlatformMessage], None],
|
||||
adapter: MastodonAdapter,
|
||||
):
|
||||
super().__init__()
|
||||
self.bot_id = bot_id
|
||||
self.callback = callback
|
||||
self.adapter = adapter
|
||||
self.running = True
|
||||
|
||||
def on_notification(self, notification):
|
||||
"""Handle incoming notifications"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
# We care about mentions and replies
|
||||
if notification["type"] in ["mention", "reply"]:
|
||||
status = notification["status"]
|
||||
|
||||
# Don't respond to ourselves
|
||||
if str(status["account"]["id"]) == self.bot_id:
|
||||
return
|
||||
|
||||
# Convert to PlatformMessage
|
||||
message = self._status_to_message(status)
|
||||
message.mentions_bot = True # It's a notification, so we were mentioned
|
||||
|
||||
# Call the callback
|
||||
try:
|
||||
self.callback(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in message callback: {e}", exc_info=True)
|
||||
|
||||
def on_update(self, status):
|
||||
"""Handle status updates (posts)"""
|
||||
# We primarily care about notifications, not all posts
|
||||
pass
|
||||
|
||||
def _status_to_message(self, status: Dict[str, Any]) -> PlatformMessage:
|
||||
"""Convert Mastodon status to PlatformMessage"""
|
||||
from html import unescape
|
||||
import re
|
||||
|
||||
# Extract text content (strip HTML)
|
||||
content = status.get("content", "")
|
||||
# Simple HTML stripping (could use BeautifulSoup for better parsing)
|
||||
content = re.sub(r"<[^>]+>", "", content)
|
||||
content = unescape(content)
|
||||
|
||||
# Map visibility
|
||||
visibility_map = {
|
||||
"public": MessageVisibility.PUBLIC,
|
||||
"unlisted": MessageVisibility.UNLISTED,
|
||||
"private": MessageVisibility.FOLLOWERS,
|
||||
"direct": MessageVisibility.DIRECT,
|
||||
}
|
||||
visibility = visibility_map.get(
|
||||
status.get("visibility", "public"), MessageVisibility.PUBLIC
|
||||
)
|
||||
|
||||
return PlatformMessage(
|
||||
id=str(status["id"]),
|
||||
text=content,
|
||||
author_id=str(status["account"]["id"]),
|
||||
author_handle=status["account"]["username"],
|
||||
timestamp=status["created_at"],
|
||||
thread_id=str(status.get("in_reply_to_id", status["id"])),
|
||||
reply_to_id=str(status["in_reply_to_id"]) if status.get("in_reply_to_id") else None,
|
||||
visibility=visibility,
|
||||
raw_data=status,
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the listener"""
|
||||
self.running = False
|
||||
Reference in New Issue
Block a user