Minimum viable chat components
Project description
chatom
Framework-agnostic chat application models and utilities
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, and Symphony
- 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"/>'
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
discord_user = DiscordUser(id="123", name="alice")
slack_user = SlackUser(id="U123", name="alice")
print(mention_user(discord_user)) # "<@123>"
print(mention_user(slack_user)) # "<@U123>"
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_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)
# 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,
)
# 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 |
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chatom-0.1.1.tar.gz.
File metadata
- Download URL: chatom-0.1.1.tar.gz
- Upload date:
- Size: 249.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3effbee298097d54038211cac5e24e82fb0d88c2650f7a34c685f7df0a95e8aa
|
|
| MD5 |
3036ab4d308d6928119294267298b31f
|
|
| BLAKE2b-256 |
58f6ea42e46439e79040c88d717a65435a9fbb375f50a355e3376bf333a03bac
|
File details
Details for the file chatom-0.1.1-py3-none-any.whl.
File metadata
- Download URL: chatom-0.1.1-py3-none-any.whl
- Upload date:
- Size: 305.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45515816ebbd961c4be2664dd1b272b73f1bb410ec979edc5f491c41a7184d45
|
|
| MD5 |
528a67570ce10db67e665b8761c7e59f
|
|
| BLAKE2b-256 |
be60fa17bf85c8108a120434f967c8f2122cb1bd1f04e3c8a969f831ebf8ba0e
|