chatom chatom

chatom

Framework-agnostic chat application models and utilities

Build Status codecov License PyPI

Overview

chatom provides a unified, framework-agnostic representation of chat applications. It offers:

  • Base models for users, channels, messages, attachments, embeds, reactions, and more

  • Backend-specific implementations for Discord, Slack, Symphony, and Telegram

  • Rich text formatting with nodes for bold, italic, code, tables, lists, and more

  • Format converters to render messages as plaintext, markdown, HTML, Slack mrkdwn, Discord markdown, or Symphony MessageML

Installation

pip install chatom

Quick Start

Basic Models

from chatom import User, Channel, Message, Emoji, Reaction

# Create a user
user = User(id="u123", name="Alice", email="alice@example.com")

# Create a channel
channel = Channel(id="c456", name="general", topic="General discussion")

# Create a message with reactions
emoji = Emoji(name="thumbsup", unicode="👍")
reaction = Reaction(emoji=emoji, count=5)

message = Message(
    id="m789",
    content="Hello, world!",
    author=user,
    channel=channel,
    reactions=[reaction],
)

Rich Text Formatting

from chatom import (
    Format,
    Text,
    Bold,
    Italic,
    Paragraph,
    Span,
    Table,
    FormattedMessage,
    MessageBuilder,
)

# Build a formatted message using nodes
msg = FormattedMessage(
    content=[
        Paragraph(children=[
            Text(content="Welcome to "),
            Bold(child=Text(content="chatom")),
            Text(content="!"),
        ]),
    ]
)

# Render to different formats
print(msg.render(Format.MARKDOWN))      # "Welcome to **chatom**!\n"
print(msg.render(Format.HTML))          # "<p>Welcome to <strong>chatom</strong>!</p>"
print(msg.render(Format.SLACK_MARKDOWN)) # "Welcome to *chatom*!\n"

Tables

from chatom import Table, Format

# Create a table from data
data = [
    ["Alice", "100", "Gold"],
    ["Bob", "85", "Silver"],
    ["Carol", "70", "Bronze"],
]
table = Table.from_data(data, headers=["Name", "Score", "Rank"])

# Render as markdown
print(table.render(Format.MARKDOWN))
# | Name | Score | Rank |
# |---|---|---|
# | Alice | 100 | Gold |
# | Bob | 85 | Silver |
# | Carol | 70 | Bronze |

# Render as HTML
print(table.render(Format.HTML))
# <table><thead>...</thead><tbody>...</tbody></table>

Backend-Specific Models

Each backend provides specialized models with platform-specific fields:

# Discord
from chatom.discord import (
    DiscordUser,
    DiscordChannel,
    DiscordChannelType,
    DiscordPresence,
    mention_user,
    mention_channel,
    mention_role,
    mention_everyone,
    mention_here,
)

user = DiscordUser(
    id="123456789",
    name="Alice",
    discriminator="1234",
    is_bot=False,
)
print(mention_user(user))  # "<@123456789>"

channel = DiscordChannel(
    id="987654321",
    name="general",
    channel_type=DiscordChannelType.GUILD_TEXT,
)
print(mention_channel(channel))  # "<#987654321>"

# Slack
from chatom.slack import (
    SlackUser,
    SlackChannel,
    SlackPresence,
    mention_user,
    mention_channel,
    mention_user_group,
    mention_here,
    mention_channel_all,
    mention_everyone,
)

user = SlackUser(
    id="U123456",
    name="alice",
    real_name="Alice Smith",
    team_id="T123",
)
print(mention_user(user))  # "<@U123456>"

# Symphony
from chatom.symphony import (
    SymphonyUser,
    SymphonyChannel,
    SymphonyStreamType,
    mention_user,
    format_hashtag,
    format_cashtag,
)

user = SymphonyUser(id="123", name="alice", user_id=12345)
print(mention_user(user))  # '<mention uid="12345"/>'
print(format_hashtag("python"))  # '<hash tag="python"/>'
print(format_cashtag("AAPL"))  # '<cash tag="AAPL"/>'

# Telegram
from chatom.telegram import (
    TelegramUser,
    TelegramChannel,
    TelegramChatType,
    TelegramPresence,
    mention_user,
    mention_channel,
)

user = TelegramUser(id="123456", name="Alice", username="alice")
print(mention_user(user))  # "@alice"

channel = TelegramChannel(id="-1001234567890", name="General")
print(mention_channel(channel))  # "#General"

Polymorphic Mentions

The mention_user and mention_channel functions use singledispatch to automatically route to the correct backend implementation:

from chatom import mention_user
from chatom.discord import DiscordUser
from chatom.slack import SlackUser
from chatom.telegram import TelegramUser

discord_user = DiscordUser(id="123", name="alice")
slack_user = SlackUser(id="U123", name="alice")
telegram_user = TelegramUser(id="789", name="alice", username="alice")

print(mention_user(discord_user))  # "<@123>"
print(mention_user(slack_user))    # "<@U123>"
print(mention_user(telegram_user)) # "@alice"

Backend-Agnostic Mentions

Use mention_user_for_backend and mention_channel_for_backend when you have a base User or Channel object and want to format it for a specific backend:

from chatom import User, Channel, mention_user_for_backend, mention_channel_for_backend

user = User(id="123", name="Alice", email="alice@example.com")
channel = Channel(id="C456", name="general")

# Mention for different backends
print(mention_user_for_backend(user, "discord"))   # "<@123>"
print(mention_user_for_backend(user, "slack"))     # "<@123>"
print(mention_user_for_backend(user, "symphony"))  # '<mention uid="123"/>'
print(mention_user_for_backend(user, "telegram"))  # "@Alice"

print(mention_channel_for_backend(channel, "discord"))  # "<#C456>"
print(mention_channel_for_backend(channel, "slack"))    # "<#C456>"

Rendering Messages for a Backend

Use render_for to render a formatted message using the appropriate format for a backend:

from chatom import FormattedMessage, Paragraph, Text, Bold, get_format_for_backend

msg = FormattedMessage(
    content=[
        Paragraph(children=[
            Text(content="Hello, "),
            Bold(child=Text(content="world")),
            Text(content="!"),
        ]),
    ]
)

# Render for different backends
print(msg.render_for("slack"))     # "Hello, *world*!\n" (Slack mrkdwn)
print(msg.render_for("discord"))   # "Hello, **world**!\n" (Discord markdown)
print(msg.render_for("symphony"))  # "<p>Hello, <b>world</b>!</p>" (MessageML)
print(msg.render_for("telegram"))  # "Hello, <b>world</b>!" (HTML)

# Get the format for a backend
from chatom import BACKEND_FORMAT_MAP
print(BACKEND_FORMAT_MAP["slack"])  # Format.SLACK_MARKDOWN

Type Conversion

Convert between base types and backend-specific types with validation:

from chatom import (
    User,
    promote,
    demote,
    can_promote,
    validate_for_backend,
    DISCORD,
    SLACK,
)
from chatom.discord import DiscordUser

# Create a base user
user = User(id="123", name="Alice", handle="alice")

# Check if it can be promoted
if can_promote(user, DISCORD):
    # Promote to DiscordUser with extra fields
    discord_user = promote(user, DISCORD, discriminator="1234")
    print(discord_user.discriminator)  # "1234"
    print(type(discord_user))  # <class 'chatom.discord.user.DiscordUser'>

# Demote back to base User
base_user = demote(discord_user)
print(type(base_user))  # <class 'chatom.base.user.User'>

# Cross-backend conversion: Discord -> Slack
slack_user = promote(demote(discord_user), SLACK, team_id="T123")
print(slack_user.team_id)  # "T123"

Backend Capabilities

from chatom import (
    Capability,
    DISCORD_CAPABILITIES,
    SLACK_CAPABILITIES,
    SYMPHONY_CAPABILITIES,
    TELEGRAM_CAPABILITIES,
)

# Check what a backend supports
print(DISCORD_CAPABILITIES.supports(Capability.THREADS))        # True
print(DISCORD_CAPABILITIES.supports(Capability.VOICE_CHAT))     # True
print(DISCORD_CAPABILITIES.supports(Capability.EMOJI_REACTIONS)) # True

Presence and Status

from chatom import Presence, PresenceStatus

presence = Presence(
    status=PresenceStatus.ONLINE,
    status_text="Working on chatom",
    activity="Coding",
)

print(presence.is_available)  # True
print(presence.is_online)     # True

Connections and Registries

The Connection base class provides a foundation for backend connections, along with UserRegistry and ChannelRegistry for managing and looking up users and channels:

from chatom import Connection, UserRegistry, ChannelRegistry, User, Channel, LookupError

# Create registries for managing users and channels
users = [
    User(id="u1", name="Alice", email="alice@example.com", handle="alice"),
    User(id="u2", name="Bob", email="bob@example.com", handle="bob"),
]
user_registry = UserRegistry(users=users)

# Look up users by different fields
print(user_registry.id_to_user("u1").name)         # "Alice"
print(user_registry.email_to_user("bob@example.com").name)  # "Bob"
print(user_registry.name_to_user("Alice").id)      # "u1"
print(user_registry.handle_to_user("alice").email) # "alice@example.com"

# Reverse lookups
print(user_registry.user_to_id(users[0]))          # "u1"
print(user_registry.user_to_email(users[0]))       # "alice@example.com"

# Generic lookup - searches all fields
print(user_registry.lookup("alice").id)            # "u1" (matches handle)
print(user_registry.lookup("bob@example.com").id)  # "u2" (matches email)

# Check if a user exists
print(user_registry.get_user("u1") is not None)    # True
print(user_registry.get_user("unknown"))           # None (no exception)

# Same pattern for channels
channels = [
    Channel(id="c1", name="general", topic="General discussion"),
    Channel(id="c2", name="random"),
]
channel_registry = ChannelRegistry(channels=channels)

print(channel_registry.id_to_channel("c1").name)   # "general"
print(channel_registry.name_to_channel("random").id) # "c2"
print(channel_registry.lookup("general").topic)    # "General discussion"

Subclass Connection to implement backend-specific connection logic:

from chatom import Connection, UserRegistry, ChannelRegistry

class MyBackendConnection(Connection):
    """Custom connection implementation."""

    async def connect(self):
        # Establish connection to your backend
        pass

    async def disconnect(self):
        # Clean up connection
        pass

    async def fetch_users(self) -> UserRegistry:
        # Fetch and return users from the backend
        users = await self._fetch_users_from_api()
        return UserRegistry(users=users)

    async def fetch_channels(self) -> ChannelRegistry:
        # Fetch and return channels from the backend
        channels = await self._fetch_channels_from_api()
        return ChannelRegistry(channels=channels)

Supported Backends

Backend

User Model

Channel Model

Mention Support

Presence

Discord

DiscordUser

DiscordChannel

DiscordPresence

Slack

SlackUser

SlackChannel

SlackPresence

Symphony

SymphonyUser

SymphonyChannel

SymphonyPresence

Telegram

TelegramUser

TelegramChannel

TelegramPresence

Output Formats

Format

Enum Value

Description

Plaintext

Format.PLAINTEXT

Plain text with no formatting

Markdown

Format.MARKDOWN

Standard Markdown

Slack mrkdwn

Format.SLACK_MARKDOWN

Slack’s mrkdwn format

Discord Markdown

Format.DISCORD_MARKDOWN

Discord-flavored Markdown

HTML

Format.HTML

Standard HTML

Symphony MessageML

Format.SYMPHONY_MESSAGEML

Symphony’s XML-based format

Text Node Types

Node

Description

Example Output (Markdown)

Text

Plain text

Hello

Bold

Bold text

**Hello**

Italic

Italic text

*Hello*

Strikethrough

Strikethrough

~~Hello~~

Underline

Underlined text

<u>Hello</u> (HTML only)

Code

Inline code

`code`

CodeBlock

Code block

```python\ncode\n```

Link

Hyperlink

[text](url)

Quote

Block quote

> quoted text

Heading

Heading (h1-h6)

# Heading

Paragraph

Paragraph

Content with newline

UnorderedList

Bullet list

- item

OrderedList

Numbered list

1. item

Table

Data table

Markdown/HTML table

UserMention

User mention

@user or <@id>

ChannelMention

Channel mention

#channel

Development

# Clone the repository
git clone https://github.com/1kbgz/chatom.git
cd chatom

# Install development dependencies
make develop

# Run tests
make test

# Run linting
make lint

License

Apache License 2.0 - see LICENSE for details.