Metadata-Version: 2.4
Name: omni-message-provider
Version: 0.2.1
Summary: Unified message provider interface for Discord, Slack, Jira, and custom platforms with distributed relay support
Author-email: Adam Sanchez <agentpython@proton.me>
Maintainer-email: Adam Sanchez <agentpython@proton.me>
License-Expression: MIT
Project-URL: Homepage, https://github.com/AgentSanchez/omni-message-provider
Project-URL: Documentation, https://github.com/AgentSanchez/omni-message-provider#readme
Project-URL: Repository, https://github.com/AgentSanchez/omni-message-provider
Project-URL: Issues, https://github.com/AgentSanchez/omni-message-provider/issues
Keywords: messaging,discord,slack,jira,chatbot,websocket,relay
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.104.0
Requires-Dist: uvicorn>=0.24.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: httpx>=0.25.0
Requires-Dist: requests>=2.31.0
Requires-Dist: websockets>=11.0
Requires-Dist: msgpack>=1.0.0
Provides-Extra: discord
Requires-Dist: discord.py>=2.3.0; extra == "discord"
Provides-Extra: slack
Requires-Dist: slack-bolt>=1.18.0; extra == "slack"
Requires-Dist: slack-sdk>=3.23.0; extra == "slack"
Provides-Extra: jira
Requires-Dist: jira>=3.5.0; extra == "jira"
Provides-Extra: all
Requires-Dist: discord.py>=2.3.0; extra == "all"
Requires-Dist: slack-bolt>=1.18.0; extra == "all"
Requires-Dist: slack-sdk>=3.23.0; extra == "all"
Requires-Dist: jira>=3.5.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.5.0; extra == "dev"
Requires-Dist: discord.py>=2.3.0; extra == "dev"
Requires-Dist: slack-bolt>=1.18.0; extra == "dev"
Requires-Dist: slack-sdk>=3.23.0; extra == "dev"
Requires-Dist: jira>=3.5.0; extra == "dev"
Dynamic: license-file

# Omni Message Provider

A unified Python interface for building chatbots and automated systems across multiple messaging platforms (Discord, Slack, Jira) with optional distributed relay support for scalable deployments.

## Features

- **Unified Interface**: Single `MessageProvider` interface for all platforms
- **Multiple Platforms**: Discord, Slack, Jira, FastAPI (custom), and polling clients
- **Distributed Architecture**: Optional WebSocket relay for microservices deployments
- **High Performance**: MessagePack serialization, WebSocket transport
- **Production Ready**: Kubernetes-ready, auto-reconnection, error handling

## Installation

### Basic Installation

```bash
pip install omni-message-provider
```

### With Platform Support

```bash
# Discord only
pip install omni-message-provider[discord]

# Slack only
pip install omni-message-provider[slack]

# Jira only
pip install omni-message-provider[jira]

# All platforms
pip install omni-message-provider[all]
```

## Quick Start

### Discord Bot

```python
import os
import discord
from message_provider import DiscordMessageProvider

# Configure Discord
intents = discord.Intents.default()
intents.message_content = True

# Create provider
provider = DiscordMessageProvider(
    bot_token=os.getenv("DISCORD_BOT_TOKEN"),
    client_id="discord:my-bot",
    intents=intents,
    trigger_mode="mention",
    command_prefixes=["!support", "!cq"]
)

# Handle messages
def message_handler(message):
    print(f"Received: {message['text']}")

    channel = message['channel']
    message_id = message['message_id']

    # Reply (threads the response)
    provider.send_message(
        message="Hello!",
        user_id=message['user_id'],
        channel=channel,
        previous_message_id=message_id
    )

    # React to the original message
    provider.send_reaction(message_id, "👋", channel=channel)

provider.register_message_listener(message_handler)
provider.start()
```

### Slack Bot

```python
import os
from message_provider import SlackMessageProvider

provider = SlackMessageProvider(
    bot_token=os.getenv("SLACK_BOT_TOKEN"),
    app_token=os.getenv("SLACK_APP_TOKEN"),
    use_socket_mode=True,
    trigger_mode="mention",
    allowed_channels=["#support", "C12345678"]
)

def message_handler(message):
    channel = message['channel']
    message_id = message['message_id']

    # Reply in thread (previous_message_id is used as thread_ts)
    provider.send_message(
        message="Got it!",
        user_id=message['user_id'],
        channel=channel,
        previous_message_id=message_id
    )

    # React to the original message
    provider.send_reaction(message_id, "eyes", channel=channel)

provider.register_message_listener(message_handler)
provider.start()
```

### Jira Issue Monitor

```python
import os
from message_provider import JiraMessageProvider

provider = JiraMessageProvider(
    server="https://company.atlassian.net",
    email=os.getenv("JIRA_EMAIL"),
    api_token=os.getenv("JIRA_API_TOKEN"),
    project_keys=["SUPPORT", "BUG"],
    client_id="jira:main",
    watch_labels=["bot-watching"],
    trigger_phrases=["@bot"]
)

def message_handler(message):
    if message['type'] == 'new_issue':
        # Add comment to ticket
        provider.send_message(
            message="We're on it!",
            channel=message['channel']  # Issue key
        )
        # Add label
        provider.send_reaction(message['channel'], "bot-acknowledged")
        # Change status
        provider.update_message(message['channel'], "In Progress")

provider.register_message_listener(message_handler)
provider.start()
```

## Distributed Architecture

For scalable, Kubernetes-ready deployments:

![Distributed Architecture](https://raw.githubusercontent.com/AgentSanchez/omni-message-provider/main/docs/distributed_architecture.png)

```python
# Message Provider Pod (Discord/Slack/Jira)
from message_provider import DiscordMessageProvider, RelayClient

discord_provider = DiscordMessageProvider(...)
relay_client = RelayClient(
    local_provider=discord_provider,
    relay_hub_url="ws://relay-hub:8765",
    client_id="discord:guild-123"
)
relay_client.start_blocking()

# RelayHub Pod (Central Router)
from message_provider import RelayHub, FastAPIMessageProvider

mp_provider = FastAPIMessageProvider(...)
hub = RelayHub(local_provider=mp_provider, port=8765)
await hub.start()

# Orchestrator Pods (Multiple instances)
from message_provider import RelayMessageProvider

provider = RelayMessageProvider(websocket_url="ws://relay-hub:8765")
provider.register_message_listener(my_handler)
provider.start()
```

## Unified Interface

All providers implement the same interface:

```python
class MessageProvider:
    def send_message(message: str, user_id: str, channel: str = None, previous_message_id: str = None) -> dict:
        """Send a message. Use previous_message_id to reply in a thread."""

    def send_reaction(message_id: str, reaction: str, channel: str = None) -> dict:
        """Add a reaction/label. channel is required for Discord and Slack."""

    def update_message(message_id: str, new_text: str, channel: str = None) -> dict:
        """Update a message/status. channel is required for Discord and Slack."""

    def register_message_listener(callback: Callable) -> None:
        """Register callback for incoming messages"""

    def start() -> None:
        """Start the provider (blocking)"""
```

Providers are stateless -- they do not cache message or channel metadata internally. The application is responsible for tracking conversation state (e.g., which channel and thread a message belongs to) using the data provided in incoming messages.

## Platform-Specific Mappings

### Discord
- `send_message()` → Send Discord message (uses `previous_message_id` as reply reference)
- `send_reaction(channel=...)` → Add emoji reaction (channel required)
- `update_message(channel=...)` → Edit message (channel required)
- `channel` = Discord channel ID

### Slack
- `send_message()` → Post Slack message (uses `previous_message_id` as `thread_ts`)
- `send_reaction(channel=...)` → Add reaction emoji (channel required)
- `update_message(channel=...)` → Update message (channel required)
- `channel` = Slack channel ID

### Jira
- `send_message()` → Add comment to ticket
- `send_reaction()` → Add label to ticket
- `update_message()` → Change ticket status
- `channel` = Jira issue key (e.g., "SUPPORT-123")

## Message Format

All providers return messages in standardized format:

```python
{
    "type": "new_issue" | "new_comment" | "new_message",
    "message_id": "unique-id",
    "text": "message content",
    "user_id": "user-identifier",
    "channel": "channel-identifier",
    "metadata": {
        "client_id": "platform:instance",
        # Platform-specific fields
    }
}
```

### Provider-Specific Fields

Provider-specific data is included under `metadata`. Use it only when needed.

```python
{
    "type": "new_message",
    "message_id": "unique-id",
    "text": "message content",
    "user_id": "user-identifier",
    "channel": "channel-identifier",
    "metadata": {
        "client_id": "platform:instance",
        "provider": "slack",  # comment: "slack", "discord", "jira", "fastapi", "relay"
        "provider_data": {    # comment: provider-specific details (see below)
            # comment: Slack example
            "thread_ts": "1700000000.123456",
            "channel_type": "channel",
            "event_type": "app_mention"
        }
    }
}
```

Slack provider_data (examples):
```python
{
    "thread_ts": "1700000000.123456",  # comment: parent thread timestamp
    "channel_type": "channel",         # comment: channel, group, im, mpim
    "event_type": "app_mention"        # comment: app_mention or message
}
```

Discord provider_data (examples):
```python
{
    "author_name": "User#1234",        # comment: display name
    "author_discriminator": "1234",    # comment: discriminator (if available)
    "guild_id": "1234567890",          # comment: server ID
    "guild_name": "My Server",         # comment: server name
    "channel_name": "support",         # comment: channel name
    "is_thread": False,                # comment: thread flag
    "thread_id": None,                 # comment: thread ID if applicable
    "reference_message_id": None,      # comment: replied-to message ID
    "is_mention": False                # comment: whether the bot was mentioned
}
```

Jira provider_data (examples):
```python
{
    "issue_key": "SUPPORT-123",        # comment: issue key
    "project": "SUPPORT",              # comment: project key
    "project_name": "Support",         # comment: project name
    "issue_type": "Bug",               # comment: issue type
    "priority": "High",                # comment: priority
    "status": "In Progress",           # comment: current status
    "labels": ["bot-watching"],        # comment: issue labels
    "reporter_name": "Jane Doe",       # comment: reporter display name
    "reporter_email": "jane@acme.com",  # comment: reporter email if available
    "created": "2024-01-01T00:00:00Z",  # comment: created timestamp
    "url": "https://.../browse/KEY"     # comment: issue URL
}
```

## Configuration

All providers accept explicit parameters (no environment variables in library):

```python
# User controls their own env var names
provider = DiscordMessageProvider(
    bot_token=os.getenv("MY_DISCORD_TOKEN"),  # Your choice
    client_id=os.getenv("MY_CLIENT_ID"),
    trigger_mode="both",  # "mention", "chat", "command", "both"
    command_prefixes=["!support", "!cq"]
)
```

## Examples

See the `src/message_provider/examples/` directory for complete working examples:
- `discord_example.py` - Discord bot with reactions
- `slack_example.py` - Slack bot with Socket Mode
- `jira_example.py` - Jira issue monitor
- `relay_example.py` - Distributed relay setup
- `polling_client_example.py` - FastAPI polling client

## Development

```bash
# Clone repository
git clone https://github.com/AgentSanchez/omni-message-provider
cd omni-message-provider

# Install with dev dependencies
pip install -e ".[dev,all]"

# Run tests
pytest

# Format code
black src/message_provider/
ruff check src/message_provider/
```

## Requirements

- Python 3.9+
- Core: `fastapi`, `uvicorn`, `websockets`, `msgpack`
- Optional: `discord.py`, `slack-bolt`, `jira`

## License

MIT License - see LICENSE file

## Support

- Documentation: https://github.com/AgentSanchez/omni-message-provider#readme
- Issues: https://github.com/AgentSanchez/omni-message-provider/issues
