Files
agentic-govbot/PLATFORMS.md
Nathan Schneider 54beddb420 Add Slack platform adapter and comprehensive platform skills system
Major Features:
- Implemented full Slack channel-bot adapter with Socket Mode
- Added 35+ Mastodon platform skills across 7 categories
- Created constitution publishing system for Mastodon

Slack Adapter (NEW):
- Full PlatformAdapter implementation (1071 lines)
- Socket Mode for real-time events
- 16 channel-scoped governance skills
- User group management as channel "roles"
- Channel access control and management
- Message pinning and moderation
- Platform limitations documentation
- Comprehensive setup guide (SLACK_SETUP.md)

Mastodon Platform Skills (ENHANCED):
- Account Moderation (9 skills): suspend, silence, disable, mark sensitive, delete status
- Account Management (4 skills): approve, reject, delete data, create account
- Report Management (4 skills): assign, unassign, resolve, reopen
- Federation Management (4 skills): block/unblock domains, allow/disallow federation
- Security Management (4 skills): block IP/email domains
- Constitution Management (2 skills): publish constitution, update profile
- Documented web-only limitations (role management requires tootctl)

Constitution Publishing:
- Publish constitution as pinned Mastodon thread
- Automatic deprecation of previous versions
- Version control with change summaries
- Thread splitting for long documents
- CLI tool: scripts/publish_constitution.py
- Documentation: CONSTITUTION_PUBLISHING.md

Configuration & Integration:
- Added SlackConfig model with bot_token, app_token, channel_id
- Updated PlatformConfig to support type: slack
- Added slack-sdk>=3.33.0 dependency
- Bot.py now routes to Slack adapter
- Updated example config with Slack section

Documentation:
- SLACK_SETUP.md: Complete Slack setup guide
- PLATFORM_SKILLS.md: All 35+ Mastodon skills documented
- CONSTITUTION_PUBLISHING.md: Constitution publishing guide
- Updated README.md: Merged QUICKSTART, added Slack to supported platforms
- Updated PLATFORMS.md: Slack marked as implemented with examples
- Updated .gitignore: Added instance-specific state files

Security:
- All sensitive files properly gitignored
- Instance-specific state (.constitution_post_id) excluded
- Credentials properly handled in config

Breaking Changes: None

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 22:46:48 -07:00

871 lines
25 KiB
Markdown

# Platform Implementation Guide
This guide explains how to implement Govbot adapters for new social/communication platforms.
## Overview
Govbot uses a **platform-agnostic architecture** that separates governance logic from platform-specific code. This allows the same constitutional reasoning and governance processes to work across different platforms.
**Currently Implemented**:
-**Mastodon** - Instance-wide governance with full admin/moderation API
-**Slack** - Channel-scoped governance with Socket Mode
**Planned**:
- 🚧 **Discord** - Server-wide governance (see example skeleton below)
- 🚧 **Telegram** - Group governance
- 🚧 **Matrix** - Room governance
The key abstraction is the **PlatformAdapter** interface, which defines how Govbot interacts with any platform.
## Architecture
```
┌─────────────────────────────────────┐
│ Governance Logic │
│ (Constitution, Agent, Primitives) │ ← Platform-agnostic
└──────────────┬──────────────────────┘
┌──────────────▼──────────────────────┐
│ PlatformAdapter Interface │ ← This guide
└──────────────┬──────────────────────┘
┌──────┴──────┐
│ │
┌────▼────┐ ┌────▼────┐
│Mastodon │ │Your │
│Adapter │ │Adapter │ ← What you'll build
└─────────┘ └─────────┘
```
## Quick Start
To implement a new platform:
1. Create `src/govbot/platforms/yourplatform.py`
2. Subclass `PlatformAdapter`
3. Implement all abstract methods
4. Define platform-specific skills
5. Add configuration support
6. Test with the mock bot
7. Document setup instructions
## Step-by-Step Guide
### Step 1: Create the Adapter File
Create a new file: `src/govbot/platforms/yourplatform.py`
```python
"""
YourPlatform adapter for Govbot.
Brief description of the platform and what governance features this enables.
"""
from typing import Callable, Optional, Dict, Any, List
import logging
from .base import (
PlatformAdapter,
PlatformMessage,
PlatformSkill,
SkillParameter,
MessageVisibility,
)
logger = logging.getLogger("govbot.platforms.yourplatform")
class YourPlatformAdapter(PlatformAdapter):
"""YourPlatform implementation of platform adapter."""
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
# Initialize platform-specific client
# Store necessary credentials
pass
```
### Step 2: Implement Connection Methods
```python
def connect(self) -> bool:
"""
Establish connection to YourPlatform.
Returns:
True if connection successful
Raises:
Exception: If connection fails
"""
try:
# Initialize your platform client
# Authenticate
# Verify credentials
# Store bot user ID and username
self.connected = True
self.bot_user_id = "your_bot_id"
self.bot_username = "your_bot_name"
logger.info(f"Connected to YourPlatform as {self.bot_username}")
return True
except Exception as e:
logger.error(f"Connection failed: {e}")
raise
def disconnect(self):
"""Clean up and disconnect."""
# Close connections
# Stop listeners
# Free resources
self.connected = False
logger.info("Disconnected from YourPlatform")
```
### Step 3: Implement Message Listening
This is the core of how your bot receives messages:
```python
def start_listening(self, callback: Callable[[PlatformMessage], None]):
"""
Start listening for messages mentioning the bot.
Args:
callback: Function to call with each message
"""
if not self.connected:
raise RuntimeError("Must connect() before listening")
logger.info("Starting message listener")
# Platform-specific listening implementation
# Examples:
# - WebSocket/streaming connection
# - Polling API endpoint
# - Webhook receiver
def on_message(native_message):
# Convert platform message to PlatformMessage
normalized = self._normalize_message(native_message)
# Filter for messages mentioning the bot
if self._mentions_bot(normalized):
callback(normalized)
# Start your listener (may be async, threaded, etc.)
# your_platform.listen(on_message)
def _normalize_message(self, native_message) -> PlatformMessage:
"""
Convert platform-native message format to PlatformMessage.
This is crucial for platform abstraction!
"""
return PlatformMessage(
id=native_message["id"],
text=self._extract_text(native_message),
author_id=native_message["author"]["id"],
author_handle=native_message["author"]["username"],
timestamp=native_message["created_at"],
thread_id=native_message.get("thread_id"),
reply_to_id=native_message.get("reply_to"),
visibility=self._map_visibility(native_message),
mentions_bot=True,
raw_data=native_message, # Keep original for reference
)
def _mentions_bot(self, message: PlatformMessage) -> bool:
"""Check if message mentions the bot."""
# Platform-specific mention detection
# Examples:
# - Check mentions list
# - Search for @botname in text
# - Check if in DM
pass
```
### Step 4: Implement Posting
```python
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 the platform.
Args:
message: Text content
thread_id: Thread/channel to post in
reply_to_id: Message to reply to
visibility: Visibility level
Returns:
Message ID of posted message
"""
if not self.connected:
raise RuntimeError("Must connect() before posting")
# Map abstract visibility to platform visibility
platform_visibility = self._map_visibility_to_platform(visibility)
# Use platform API to post
result = your_platform.post_message(
text=message,
channel=thread_id,
reply_to=reply_to_id,
visibility=platform_visibility,
)
logger.info(f"Posted message {result['id']}")
return result["id"]
```
### Step 5: Define Platform Skills
Skills are platform-specific actions the bot can perform. Think about:
- **Moderation**: Ban, mute, delete content
- **Administration**: Change settings, update rules
- **User management**: Roles, permissions
- **Content management**: Pins, announcements
```python
def get_skills(self) -> List[PlatformSkill]:
"""Define YourPlatform-specific governance skills."""
return [
PlatformSkill(
name="ban_user",
description="Ban a user from the server/instance",
category="moderation",
parameters=[
SkillParameter(
"user_id",
"str",
"User ID to ban",
required=True
),
SkillParameter(
"reason",
"str",
"Reason for ban",
required=True
),
SkillParameter(
"duration",
"int",
"Ban duration in days (0 = permanent)",
required=False,
default=0
),
],
requires_confirmation=True,
reversible=True,
constitutional_authorization="Requires moderation authority",
),
PlatformSkill(
name="update_rules",
description="Update server/instance rules",
category="admin",
parameters=[
SkillParameter(
"rules",
"list",
"List of rule texts",
required=True
),
],
requires_confirmation=True,
reversible=True,
constitutional_authorization="Requires constitutional process",
),
# Add more skills specific to your platform
]
```
### Step 6: Implement Skill Execution
```python
def execute_skill(
self,
skill_name: str,
parameters: Dict[str, Any],
actor: str,
) -> Dict[str, Any]:
"""Execute a platform skill."""
if not self.connected:
raise RuntimeError("Must connect() before executing skills")
# Validate
is_valid, error = self.validate_skill_execution(skill_name, parameters)
if not is_valid:
raise ValueError(error)
logger.info(f"Executing {skill_name} for {actor}")
# Route to skill handler
if skill_name == "ban_user":
return self._ban_user(parameters)
elif skill_name == "update_rules":
return self._update_rules(parameters)
else:
raise ValueError(f"Unknown skill: {skill_name}")
def _ban_user(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Ban a user (example skill implementation)."""
user_id = params["user_id"]
reason = params["reason"]
duration = params.get("duration", 0)
# Use platform API to ban user
your_platform.ban(user_id, reason=reason, duration=duration)
return {
"success": True,
"message": f"Banned user {user_id}",
"data": {"user_id": user_id, "reason": reason, "duration": duration},
"reversible": True,
"reverse_params": {"user_id": user_id}, # For unban
}
```
### Step 7: Implement User Info
```python
def get_user_info(self, user_id: str) -> Optional[Dict[str, Any]]:
"""Get information about a user."""
if not self.connected:
return None
try:
user = your_platform.get_user(user_id)
return {
"id": user["id"],
"handle": user["username"],
"display_name": user["display_name"],
"roles": self._get_user_roles(user),
"is_bot": user.get("bot", False),
}
except Exception as e:
logger.error(f"Failed to get user info: {e}")
return None
def _get_user_roles(self, user) -> List[str]:
"""Determine user roles for governance."""
roles = ["member"]
if user.get("is_admin"):
roles.append("admin")
if user.get("is_moderator"):
roles.append("moderator")
return roles
```
### Step 8: Implement Thread URLs
```python
def format_thread_url(self, thread_id: str) -> str:
"""Generate URL to view a thread."""
return f"https://yourplatform.com/thread/{thread_id}"
```
## Platform-Specific Considerations
### Threading/Conversations
Different platforms handle conversations differently:
- **Mastodon**: Reply chains with `in_reply_to_id`
- **Discord**: Channels + threads
- **Telegram**: Groups + reply-to
- **Slack**: Channels + thread_ts
Map your platform's concept to `thread_id` in PlatformMessage.
### Visibility/Privacy
Map your platform's privacy levels to `MessageVisibility`:
```python
def _map_visibility_to_platform(self, visibility: MessageVisibility) -> str:
"""Map abstract visibility to platform-specific."""
mapping = {
MessageVisibility.PUBLIC: "your_platform_public",
MessageVisibility.UNLISTED: "your_platform_unlisted",
MessageVisibility.FOLLOWERS: "your_platform_followers",
MessageVisibility.DIRECT: "your_platform_dm",
MessageVisibility.PRIVATE: "your_platform_private",
}
return mapping.get(visibility, "your_platform_default")
```
### Authentication
Different platforms have different auth:
- **OAuth**: Mastodon, Discord
- **Bot tokens**: Telegram, Discord, Slack
- **API keys**: Custom platforms
Store credentials securely in config:
```python
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
# Get required credentials
self.api_token = config.get("api_token")
self.bot_id = config.get("bot_id")
if not self.api_token:
raise ValueError("YourPlatform requires 'api_token' in config")
```
### Rate Limiting
Handle platform rate limits gracefully:
```python
import time
from functools import wraps
def rate_limited(max_per_minute):
"""Decorator for rate limiting."""
min_interval = 60.0 / max_per_minute
last_called = [0.0]
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
wait_time = min_interval - elapsed
if wait_time > 0:
time.sleep(wait_time)
result = func(*args, **kwargs)
last_called[0] = time.time()
return result
return wrapper
return decorator
@rate_limited(max_per_minute=30)
def post(self, message, ...):
# Your post implementation
pass
```
## Configuration
### Add Config Model
In `src/govbot/utils/config.py`, add your platform config:
```python
class YourPlatformConfig(BaseModel):
"""YourPlatform configuration"""
api_token: str = Field(..., description="API token for bot")
server_id: str = Field(..., description="Server/guild ID")
# Add other required fields
class PlatformConfig(BaseModel):
type: str = Field(...)
mastodon: Optional[MastodonConfig] = None
yourplatform: Optional[YourPlatformConfig] = None # Add this
```
### Update Example Config
In `config/config.example.yaml`:
```yaml
platform:
type: yourplatform
yourplatform:
api_token: your_token_here
server_id: your_server_id
# Other settings
```
### Register in Bot
In `src/govbot/bot.py`, add your adapter:
```python
from .platforms.yourplatform import YourPlatformAdapter
def _create_platform_adapter(self) -> PlatformAdapter:
platform_type = self.config.platform.type.lower()
if platform_type == "mastodon":
return MastodonAdapter(self.config.platform.mastodon.model_dump())
elif platform_type == "yourplatform":
return YourPlatformAdapter(self.config.platform.yourplatform.model_dump())
# ...
```
## Testing Your Adapter
### Unit Tests
Create `tests/test_yourplatform.py`:
```python
import pytest
from src.govbot.platforms.yourplatform import YourPlatformAdapter
from src.govbot.platforms.base import PlatformMessage, MessageVisibility
def test_connection():
"""Test connecting to platform."""
config = {
"api_token": "test_token",
"server_id": "test_server",
}
adapter = YourPlatformAdapter(config)
# Mock the platform connection
# Test that connect() returns True
# Verify bot_user_id and bot_username are set
def test_message_normalization():
"""Test converting platform messages to PlatformMessage."""
adapter = YourPlatformAdapter({...})
native_message = {
# Your platform's message format
}
normalized = adapter._normalize_message(native_message)
assert isinstance(normalized, PlatformMessage)
assert normalized.text == "expected text"
assert normalized.author_handle == "expected_user"
def test_skills():
"""Test that skills are properly defined."""
adapter = YourPlatformAdapter({...})
skills = adapter.get_skills()
assert len(skills) > 0
assert all(skill.name for skill in skills)
assert all(skill.category in ["admin", "moderation", "content", "user_management"]
for skill in skills)
```
### Integration Testing
1. **Use Mock Mode**: Test governance logic without platform
2. **Sandbox Account**: Create test account on your platform
3. **Test Instance**: Use development/test server if available
### Manual Testing Checklist
- [ ] Bot can connect and authenticate
- [ ] Bot receives mentions
- [ ] Bot can post responses
- [ ] Bot can post in threads
- [ ] Visibility levels work correctly
- [ ] Skills execute successfully
- [ ] Rate limiting works
- [ ] Errors are handled gracefully
- [ ] Disconnection is clean
## Common Patterns
### Async vs Sync
If your platform client is async:
```python
import asyncio
import threading
def start_listening(self, callback):
"""Start async listener in separate thread."""
async def async_listen():
async for message in your_platform.stream():
normalized = self._normalize_message(message)
callback(normalized) # Callback is sync
def run_async_loop():
asyncio.run(async_listen())
self.listener_thread = threading.Thread(target=run_async_loop, daemon=True)
self.listener_thread.start()
```
### Webhooks
If your platform uses webhooks instead of streaming:
```python
from flask import Flask, request
class YourPlatformAdapter(PlatformAdapter):
def __init__(self, config):
super().__init__(config)
self.app = Flask(__name__)
self.callback = None
def start_listening(self, callback):
"""Set up webhook receiver."""
self.callback = callback
@self.app.route("/webhook", methods=["POST"])
def webhook():
data = request.json
message = self._normalize_message(data)
if self.callback:
self.callback(message)
return {"status": "ok"}
# Run Flask in separate thread
threading.Thread(
target=lambda: self.app.run(port=5000),
daemon=True
).start()
```
### Error Recovery
Implement reconnection logic:
```python
def start_listening(self, callback):
"""Listen with automatic reconnection."""
def listen_with_retry():
while self.connected:
try:
your_platform.stream(on_message)
except Exception as e:
logger.error(f"Stream error: {e}")
if self.connected:
logger.info("Reconnecting in 5 seconds...")
time.sleep(5)
threading.Thread(target=listen_with_retry, daemon=True).start()
```
## Documentation
### User-Facing Documentation
Create `docs/platforms/yourplatform.md`:
```markdown
# YourPlatform Setup Guide
## Prerequisites
- YourPlatform account
- Bot account created
- Admin access (for some features)
## Setup Steps
1. Create bot account at https://yourplatform.com/bots
2. Generate API token
3. Copy `config/config.example.yaml` to `config/config.yaml`
4. Update configuration:
```yaml
platform:
type: yourplatform
yourplatform:
api_token: YOUR_TOKEN
server_id: YOUR_SERVER
```
5. Run the bot: `python -m src.govbot`
## Available Features
- Governance proposals
- Voting in threads
- Moderation actions
- Admin commands
## Permissions Required
- Read messages
- Post messages
- Manage server (for admin skills)
```
## Example: Discord Adapter Skeleton
Here's a skeleton for a Discord adapter:
```python
"""Discord platform adapter for Govbot."""
import discord
from discord.ext import commands
from typing import Callable, Optional, Dict, Any, List
import logging
from .base import (
PlatformAdapter,
PlatformMessage,
PlatformSkill,
SkillParameter,
MessageVisibility,
)
logger = logging.getLogger("govbot.platforms.discord")
class DiscordAdapter(PlatformAdapter):
"""Discord implementation of platform adapter."""
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
token = config.get("token")
self.guild_id = config.get("guild_id")
if not token or not self.guild_id:
raise ValueError("Discord requires 'token' and 'guild_id'")
intents = discord.Intents.default()
intents.message_content = True
self.bot = commands.Bot(command_prefix="!", intents=intents)
self.token = token
self.message_callback = None
def connect(self) -> bool:
"""Connect to Discord."""
@self.bot.event
async def on_ready():
logger.info(f"Connected as {self.bot.user}")
self.bot_user_id = str(self.bot.user.id)
self.bot_username = self.bot.user.name
self.connected = True
# Run bot in thread
import threading
threading.Thread(
target=lambda: self.bot.run(self.token),
daemon=True
).start()
# Wait for connection
import time
timeout = 10
while not self.connected and timeout > 0:
time.sleep(0.5)
timeout -= 0.5
return self.connected
def disconnect(self):
"""Disconnect from Discord."""
import asyncio
asyncio.run(self.bot.close())
self.connected = False
def start_listening(self, callback: Callable[[PlatformMessage], None]):
"""Listen for mentions."""
self.message_callback = callback
@self.bot.event
async def on_message(message):
if message.author.id == self.bot.user.id:
return
if self.bot.user in message.mentions:
normalized = self._normalize_message(message)
if self.message_callback:
self.message_callback(normalized)
def post(self, message: str, thread_id: Optional[str] = None,
reply_to_id: Optional[str] = None,
visibility: MessageVisibility = MessageVisibility.PUBLIC) -> str:
"""Post to Discord."""
import asyncio
async def send():
channel = self.bot.get_channel(int(thread_id))
msg = await channel.send(message)
return str(msg.id)
return asyncio.run(send())
def get_skills(self) -> List[PlatformSkill]:
"""Discord-specific skills."""
return [
PlatformSkill(
name="ban_user",
description="Ban user from server",
category="moderation",
parameters=[
SkillParameter("user_id", "str", "User to ban"),
SkillParameter("reason", "str", "Ban reason"),
],
requires_confirmation=True,
reversible=True,
),
# More Discord skills...
]
def execute_skill(self, skill_name: str, parameters: Dict[str, Any],
actor: str) -> Dict[str, Any]:
"""Execute Discord skill."""
# Implementation...
pass
def get_user_info(self, user_id: str) -> Optional[Dict[str, Any]]:
"""Get Discord user info."""
# Implementation...
pass
def format_thread_url(self, thread_id: str) -> str:
"""Format Discord channel URL."""
return f"https://discord.com/channels/{self.guild_id}/{thread_id}"
def _normalize_message(self, msg: discord.Message) -> PlatformMessage:
"""Convert Discord message to PlatformMessage."""
return PlatformMessage(
id=str(msg.id),
text=msg.content,
author_id=str(msg.author.id),
author_handle=msg.author.name,
timestamp=msg.created_at,
thread_id=str(msg.channel.id),
reply_to_id=str(msg.reference.message_id) if msg.reference else None,
visibility=MessageVisibility.PUBLIC,
mentions_bot=True,
raw_data=msg,
)
```
## Getting Help
- Review the **Mastodon adapter** (`src/govbot/platforms/mastodon.py`) for an instance-wide governance example
- Review the **Slack adapter** (`src/govbot/platforms/slack.py`) for a channel-scoped governance example
- Check the base PlatformAdapter (`src/govbot/platforms/base.py`) for interface documentation
- See setup guides: [MASTODON_SETUP.md](MASTODON_SETUP.md), [SLACK_SETUP.md](SLACK_SETUP.md)
- Look at test files for testing patterns
- Ask questions in project discussions
## Contributing Your Adapter
Once your adapter works:
1. Add comprehensive docstrings
2. Write tests
3. Document setup in `docs/platforms/`
4. Update README.md with supported platforms
5. Submit a pull request
Thank you for extending Govbot to new platforms! 🎉