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

25 KiB

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

"""
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

    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:

    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

    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
    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

    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

    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

    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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

# 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
  1. 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, 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! 🎉