Python SDK for Dooray.com with Socket Mode support
Project description
Dooray SDK - Python
A comprehensive Python SDK for building agents on Dooray.com. This document covers the core classes, decorators, and usage patterns.
Installation
Install via pip
# Currently in alpha (requires --pre flag)
pip install --pre dooray-sdk
# Or specify version explicitly
pip install dooray-sdk==0.0.1a5
Install with aiohttp (Recommended)
pip install --pre "dooray-sdk[aiohttp]"
# Or specify version explicitly
pip install "dooray-sdk[aiohttp]==0.0.1a5"
Using Virtual Environment (Recommended)
# Create virtual environment
python3 -m venv .venv
# Activate (Linux/macOS)
source .venv/bin/activate
# Activate (Windows)
.venv\Scripts\activate
# Install
pip install --pre "dooray-sdk[aiohttp]"
Dependencies
| Package | Version | Description |
|---|---|---|
aiohttp |
>= 3.8.0 | Async HTTP/WebSocket client |
requests |
>= 2.28.0 | Sync HTTP client |
Quick Start (Templates)
Use the CLI tool to quickly bootstrap your project.
Install with Templates
pip install --pre "dooray-sdk[templates]"
Usage
# List available templates
dooray-sdk list
# Create an echo agent project
dooray-sdk init echo-agent
# Create with default values (no prompts)
dooray-sdk init echo-agent --no-input
# Create in a specific directory
dooray-sdk init echo-agent -o ./projects
Available Templates
| Template | Description |
|---|---|
echo-agent |
A simple agent that echoes messages back |
Environment Variables
The SDK uses the following environment variables:
| Variable | Required | Description | Example |
|---|---|---|---|
DOORAY_AGENT_TOKEN |
Yes | Agent authentication token | your_agent_token |
DOORAY_DOMAIN |
Yes | Dooray domain | company.dooray.com |
Classes
Agent
The core class for building Dooray agents. Supports message handling, auto-replies, and event processing.
class Agent:
def __init__(
self,
token: str = None,
domain: str = None,
services: List[str] = None,
**kwargs
)
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
token |
str | None |
None |
Agent token (falls back to DOORAY_AGENT_TOKEN env var) |
domain |
str | None |
None |
Dooray domain (falls back to DOORAY_DOMAIN env var) |
services |
List[str] | None |
["messenger"] |
List of services to connect |
Properties
| Property | Type | Description |
|---|---|---|
token |
str |
Agent authentication token |
domain |
str |
Dooray domain |
services |
List[str] |
Connected service list |
base_url |
str |
API base URL |
is_connected |
bool |
Connection status |
Methods
| Method | Description |
|---|---|
run() |
Start the agent (blocking) |
on(event_type, action, service) |
Event handler decorator (preferred) |
add_handler(event_type, handler, action, service) |
Register handler (non-decorator) |
reply_to(trigger, response) |
Simple response mapping |
send_message(channel, text) |
Deprecated. Use agent.messenger.send_message(...) |
Service Namespace Clients
agent.messenger, agent.task, and agent.wiki expose service-scoped API clients that group related verbs together (Slack Bolt / atlassian-python-api style).
| Namespace | Verb | Description |
|---|---|---|
agent.messenger |
send_message(channel, text, **kw) |
Post a new message to a channel/DM |
agent.messenger |
reply_to_message(channel, message_id, text, **kw) |
Quote-reply to a specific message |
agent.messenger |
reply_in_thread(parent_channel_id, text, **kw) |
Reply inside a thread (raises NotImplementedError — implementation pending) |
agent.task |
(slot) | Reserved for the task service; methods land in a future release |
agent.wiki |
(slot) | Reserved for the wiki service; methods land in a future release |
The same names also still accept decorator usage (@agent.messenger etc.) for backward compatibility, but that path emits a DeprecationWarning. Use @agent.on(...) instead.
Example - Basic Usage (recommended)
Handlers now receive (req, client) — req carries the event data and client (the Agent instance) exposes the service namespaces.
from dooray_sdk import Agent
agent = Agent()
@agent.on("message")
async def handle_message(req, client):
"""Handle messenger messages."""
if req.text:
await client.messenger.send_message(
channel=req.channel_info.id,
text=f"Received: {req.text}",
)
agent.run()
A handler that only accepts (req) keeps working for one minor version.
Example - Multi-Service
from dooray_sdk import Agent
agent = Agent(services=["messenger", "task", "wiki"])
@agent.on("message")
async def handle_messenger(req, client):
await client.messenger.send_message(
channel=req.channel_info.id,
text="This is a messenger message",
)
@agent.on("task", action="create")
async def handle_task_created(req, client):
# Send a notification message about the new task
notify_channel = "<channel-id>"
subject = req.entity.data.subject if req.entity.data else "(no subject)"
await client.messenger.send_message(
channel=notify_channel,
text=f"New task: {subject}",
)
@agent.on("page")
async def handle_wiki(req, client):
print(f"Wiki event: {req.action}")
agent.run()
Example - Reply to a Specific Message
@agent.on("message")
async def handle(req, client):
if req.text == "quote me":
await client.messenger.reply_to_message(
channel=req.channel_info.id,
message_id=req.entity.data.id,
text="Quoted reply",
)
SocketModeRequest
Represents an incoming request received via WebSocket.
@dataclass
class SocketModeRequest:
envelope_id: str
type: str
payload: Dict[str, Any]
service: str = ""
action: str = ""
entity: EntityWrapper = None
actor: ActorWrapper = None
action_data: ActionWrapper = None
Properties
| Property | Type | Description |
|---|---|---|
envelope_id |
str |
Unique message ID |
type |
str |
Event type (message, task, page, etc.) |
payload |
Dict[str, Any] |
Raw payload data |
service |
str |
Service name (messenger, task, wiki) |
action |
str |
Action type (create, update, delete) |
entity |
EntityWrapper |
Entity information wrapper |
actor |
ActorWrapper |
Actor information wrapper |
action_data |
ActionWrapper |
Action data wrapper |
Convenience Properties
| Property | Type | Description |
|---|---|---|
is_message |
bool |
Whether this is a message type |
text |
str | None |
Message text |
channel |
str | None |
Channel ID |
data |
Dict[str, Any] |
Raw data (alias for payload) |
Methods
| Method | Description |
|---|---|
reply(text) |
Deprecated. Delegates to client.messenger.send_message(channel=req.channel_info.id, text=text) with a DeprecationWarning. Use the (req, client) handler signature and client.messenger.<verb>(...) directly. |
is_type(types) |
Check type |
is_action(actions) |
Check action |
is_service(services) |
Check service |
to_dict() |
Convert to dictionary |
from_dict(data, default_service) |
Create from dictionary (class method) |
SocketModeRequest is now a pure event-data object — action verbs have moved to the service namespace clients (client.messenger.*).
Example - Request Handling
@agent.on("message")
async def handle(req, client):
# Check message type
if req.is_message:
print(f"Text: {req.text}")
print(f"Channel: {req.channel_info.id}")
# Check action
if req.is_action("create"):
print("This is a new message")
# Check service
if req.is_service("messenger"):
await client.messenger.send_message(
channel=req.channel_info.id,
text="Responding from messenger",
)
WebClient
Synchronous REST API client (HTTP transport).
class WebClient:
def __init__(
self,
token: str,
base_url: str = "https://api.dooray.com/",
timeout: int = 30
)
Service Namespace
| Namespace | Type | Description |
|---|---|---|
client.messenger |
MessengerAPI |
Messenger service verbs (send_message, reply_to_message, reply_in_thread) |
Methods
| Method | Return Type | Description |
|---|---|---|
get_member(member_id) |
Dict |
Get organization member info |
send_message(channel, text, **kwargs) |
Dict |
Deprecated. Use client.messenger.send_message(...). |
get_channels(**kwargs) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
get_messages(channel, limit, **kwargs) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
get_user_info(user_id) |
Dict |
Deprecated. Will move under client.messenger.* in a future release. |
AsyncWebClient
Asynchronous REST API client (HTTP transport).
class AsyncWebClient:
def __init__(
self,
token: str,
base_url: str = "https://api.dooray.com/"
)
Service Namespace
| Namespace | Type | Description |
|---|---|---|
client.messenger |
AsyncMessengerAPI |
Messenger service verbs (send_message, reply_to_message, reply_in_thread) |
Methods
| Method | Return Type | Description |
|---|---|---|
get_member(member_id) |
Awaitable[Dict] |
Get organization member info |
send_message(channel, text, **kwargs) |
Awaitable[Dict] |
Deprecated. Use client.messenger.send_message(...). |
get_channels(**kwargs) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
get_messages(channel, limit, **kwargs) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
get_user_info(user_id) |
Awaitable[Dict] |
Deprecated. Will move under client.messenger.* in a future release. |
MessengerAPI / AsyncMessengerAPI
Messenger service clients. Reached via agent.messenger, WebClient.messenger, or AsyncWebClient.messenger.
| Method | Description | HTTP |
|---|---|---|
send_message(channel, text, **kw) |
Post a new message to a channel/DM | POST /messenger/v1/channels/{channel}/logs |
reply_to_message(channel, message_id, text, **kw) |
Quote-reply to a specific message | POST /messenger/v1/channels/{channel}/logs/{message_id}/reply |
reply_in_thread(parent_channel_id, text, **kw) |
Reply inside a thread | Not implemented yet — raises NotImplementedError. Implementation pending. |
Each verb is provided in both sync (MessengerAPI) and async (AsyncMessengerAPI) flavours.
Decorators
@agent.on() Decorator (recommended)
Unified event handler decorator. Use service=... and event_type=... to scope.
@agent.on("message") # all message events
@agent.on(service="messenger") # all messenger events
@agent.on("task", action="create") # task creation only
@agent.on("page") # all wiki page events
Service Decorators (deprecated)
@agent.messenger, @agent.task, and @agent.wiki still work for one minor version but emit a DeprecationWarning. They are kept because the same names (agent.messenger etc.) now also expose service namespace clients (see above) — the decorator form will be retired in a future release.
@agent.messenger # deprecated — use @agent.on(service="messenger") or @agent.on("message")
@agent.task # deprecated — use @agent.on(service="task")
@agent.wiki # deprecated — use @agent.on(service="wiki") or @agent.on("page")
@agent.on() Signature
def on(
event_type: Union[str, List[str]] = "all",
action: Union[str, List[str]] = None,
service: Union[str, List[str]] = None
) -> Callable
| Parameter | Type | Default | Description |
|---|---|---|---|
event_type |
str | List[str] |
"all" |
Event type filter |
action |
str | List[str] | None |
None |
Action filter |
service |
str | List[str] | None |
None |
Service filter |
Handlers can accept (req, client) (recommended) or (req) (one-minor backward compatibility). client is the Agent instance — use client.messenger.<verb>(...) etc. to send.
Examples
# Handle only message events
@agent.on("message")
async def handle_message(req, client):
pass
# Handle specific action
@agent.on("message", action="create")
async def handle_new_message(req, client):
pass
# Handle multiple types
@agent.on(["task", "page"])
async def handle_task_or_page(req, client):
pass
# Combine service and action
@agent.on(service="wiki", action=["create", "update"])
async def handle_wiki_changes(req, client):
pass
Request Data Structure
The SDK provides wrapper types that enable dot notation access to dictionary data.
Type Structure Overview
SocketModeRequest
├── entity: EntityWrapper # Entity info (task, page, etc.)
│ ├── type: str # Entity type
│ └── data: DataWrapper # Entity data
├── actor: ActorWrapper # Action performer info
│ ├── type: str # Actor type
│ └── data: DataWrapper # Actor data
└── action_data: ActionWrapper # Action details
├── type: str # Action type
└── data: DataWrapper # Action data
DataWrapper
The base class for all wrapper types. Enables dot notation access to dictionary data.
class DataWrapper:
def __getattr__(self, name: str) -> Any # Dot notation access
def __getitem__(self, key: str) -> Any # Dictionary access
def get(self, key: str, default=None) # Safe access
def to_dict(self) -> Dict[str, Any] # Return original dictionary
Usage:
# Dot notation access
data.id
data.subject
data.nested.value
# Dictionary access
data["id"]
data.get("subject", "default")
# Get original dictionary
data.to_dict()
EntityWrapper
Wraps entity information (tasks, pages, etc.).
| Property | Type | Description |
|---|---|---|
type |
str |
Entity type (e.g., "task", "page") |
data |
DataWrapper |
Entity detail data |
Usage:
@agent.on("task")
async def handle_task(req, client):
print(req.entity.type) # "task"
print(req.entity.data.id) # Task ID
print(req.entity.data.subject) # Task subject
Message Data
For messenger messages, entity.type is "message".
| Property | Type | Description |
|---|---|---|
id |
str |
Message ID |
channelId |
str |
Channel ID |
senderId |
str |
Sender member ID |
text |
str |
Message text |
sentAt |
int |
Sent time (timestamp ms) |
seq |
int |
Message sequence number |
directMemberId |
str |
DM recipient member ID |
parentChannelId |
str |
Parent channel ID (threads) |
@agent.on("message")
async def handle(req, client):
print(req.entity.message.text)
print(req.entity.message.channelId)
print(req.entity.message.senderId)
print(req.entity.message.sentAt)
ActorWrapper
Wraps information about the user who performed the action.
| Property | Type | Description |
|---|---|---|
type |
str |
Actor type (e.g., "organizationMember") |
data |
DataWrapper |
Actor detail data |
organizationMember |
LazyMemberData | None |
Organization member info (supports lazy loading) |
Usage:
@agent.on("message")
async def handle(req, client):
if req.actor:
print(req.actor.type) # "organizationMember"
# Access via organizationMember (recommended)
member = req.actor.organizationMember
print(member.id) # Member ID (local data)
print(member.name) # Member name (lazy loading)
organizationMember Fields
| Property | Type | Description |
|---|---|---|
id |
str |
Member ID |
name |
str |
Member name |
externalEmailAddress |
str |
Email address |
nickname |
str |
Nickname |
englishName |
str |
English name |
nativeName |
str |
Native name |
userCode |
str |
User code |
locale |
str |
Locale |
timezoneName |
str |
Timezone |
Lazy Loading
organizationMember returns data included in the WebSocket message immediately, and fetches missing fields via API when accessed.
@agent.on("message")
async def handle(req, client):
member = req.actor.organizationMember
# Local data (no API call)
member_id = member.id
# Async access - API call when needed
name = await member.name
email = await member.externalEmailAddress
# Sync access (print, comparison, etc.)
print(member.name)
if member.name == "John Doe":
await client.messenger.send_message(
channel=req.channel_info.id, text="Hello!"
)
# Get full data
full_data = await member
ActionWrapper
Wraps action detail information.
| Property | Type | Description |
|---|---|---|
type |
str |
Action type (e.g., "create", "update") |
data |
DataWrapper |
Action detail data |
Usage Examples
Echo Agent
from dooray_sdk import Agent
agent = Agent()
@agent.on("message")
async def echo(req, client):
if req.text:
await client.messenger.send_message(
channel=req.channel_info.id,
text=f"Echo: {req.text}",
)
agent.run()
Auto-Reply Agent
from dooray_sdk import Agent
agent = Agent()
# Simple mapping (unchanged)
agent.reply_to("ping", "pong")
agent.reply_to("hello", "Hello there!")
# Custom handler
@agent.on("message")
async def handle(req, client):
text = req.text or ""
if text.startswith("help"):
await client.messenger.send_message(
channel=req.channel_info.id,
text="Available commands: ping, hello, help",
)
agent.run()
Multi-Service Agent
from dooray_sdk import Agent
agent = Agent(services=["messenger", "task", "wiki"])
@agent.on("message")
async def on_message(req, client):
print(f"[Messenger] {req.text}")
await client.messenger.send_message(
channel=req.channel_info.id, text="Message received!"
)
@agent.on("task")
async def on_task(req, client):
print(f"[Task] {req.action}: {req.entity.data.subject}")
@agent.on("page")
async def on_wiki(req, client):
print(f"[Wiki] {req.action}: {req.entity.data.title}")
@agent.on("all")
async def on_any(req, client):
print(f"[All] {req.service}/{req.type}/{req.action}")
agent.run()
Reply to a Specific Message (Quote Reply)
@agent.on("message")
async def handle(req, client):
if req.text == "quote me":
await client.messenger.reply_to_message(
channel=req.channel_info.id,
message_id=req.entity.data.id,
text="This is a quoted reply",
)
Error Handling
Common Exceptions
| Exception | Cause | Solution |
|---|---|---|
ValueError |
Environment variables not set | Set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN |
ConnectionError |
WebSocket connection failed | Check network and token |
RuntimeError |
Calling reply before agent starts | Use after agent.run() |
Exception Handling Example
from dooray_sdk import Agent
try:
agent = Agent()
except ValueError as e:
print(f"Configuration error: {e}")
print("Please set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN")
exit(1)
@agent.on("message")
async def handle(req, client):
try:
# Business logic
process_message(req)
except Exception as e:
# Log error (avoid exposing errors to users)
logger.error(f"Processing error: {e}")
agent.run()
Migration Guide (Service Namespace Refactor)
Starting with this release, the SDK separates event data from action verbs and groups actions under per-service namespaces. Older surfaces keep working for one minor version with DeprecationWarning; the next minor release will remove them.
Handler signature
# Before (deprecated; still works for one minor)
@agent.messenger
async def handle(req):
await req.reply("pong")
# After
@agent.on("message")
async def handle(req, client):
await client.messenger.send_message(
channel=req.channel_info.id, text="pong"
)
Three messenger verbs
req.reply() historically meant three different things in Dooray. Each is now its own verb:
| Intent | New verb |
|---|---|
| Send a fresh message to a channel/DM | client.messenger.send_message(channel, text) |
| Quote-reply to a specific message | client.messenger.reply_to_message(channel, message_id, text) |
| Reply inside a thread | client.messenger.reply_in_thread(parent_channel_id, text) (implementation pending — raises NotImplementedError) |
High availability (active/standby)
The SDK transparently supports running multiple agent processes against the same token: the server enforces one active WebSocket per token (close code 1008 AGENT_ALREADY_CONNECTED), and the SDK puts duplicates into a STANDBY state that retries every 15 seconds. When the active process exits, a standby is promoted automatically — no extra configuration required.
Summary of deprecated surfaces
| Old | New |
|---|---|
req.reply(text) |
await client.messenger.send_message(channel=req.channel_info.id, text=text) |
@agent.messenger / @agent.task / @agent.wiki (decorator) |
@agent.on(service="messenger") (or @agent.on("message"), @agent.on("task"), @agent.on("page")) |
Agent.send_message / get_channels / get_messages / get_user_info |
agent.messenger.send_message(...) (and future agent.messenger.* verbs) |
WebClient / AsyncWebClient .send_message / .get_channels / .get_messages / .get_user_info |
client.messenger.<verb>(...) |
Run under python -W error::DeprecationWarning to discover remaining call sites.
License
MIT License
Project details
Release history Release notifications | RSS feed
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 dooray_sdk-0.0.1a5.tar.gz.
File metadata
- Download URL: dooray_sdk-0.0.1a5.tar.gz
- Upload date:
- Size: 101.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f24eced6aefa93f92db1eaace6e0eb843e147724ee64a6ed1003e5dfa4aebb50
|
|
| MD5 |
ee9c032dc410f6fe5639d26f7d57b6eb
|
|
| BLAKE2b-256 |
e221d1685d0d62dcf5f012402c7091e9cac4a498a935d72e709662103514854c
|
File details
Details for the file dooray_sdk-0.0.1a5-py3-none-any.whl.
File metadata
- Download URL: dooray_sdk-0.0.1a5-py3-none-any.whl
- Upload date:
- Size: 79.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4621251de1fc92eb86e1b255ec034549a9685282db8506a91b06eb225ec411d1
|
|
| MD5 |
d0b9bd95f6d5beb445b0b58ea566d982
|
|
| BLAKE2b-256 |
23a6cec34cbf47f48b1e9de8a3264f196f75091b6a4c0b0b0f98a5aa54bc4b05
|