Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dooray_sdk-0.0.1a5.tar.gz (101.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dooray_sdk-0.0.1a5-py3-none-any.whl (79.7 kB view details)

Uploaded Python 3

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

Hashes for dooray_sdk-0.0.1a5.tar.gz
Algorithm Hash digest
SHA256 f24eced6aefa93f92db1eaace6e0eb843e147724ee64a6ed1003e5dfa4aebb50
MD5 ee9c032dc410f6fe5639d26f7d57b6eb
BLAKE2b-256 e221d1685d0d62dcf5f012402c7091e9cac4a498a935d72e709662103514854c

See more details on using hashes here.

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

Hashes for dooray_sdk-0.0.1a5-py3-none-any.whl
Algorithm Hash digest
SHA256 4621251de1fc92eb86e1b255ec034549a9685282db8506a91b06eb225ec411d1
MD5 d0b9bd95f6d5beb445b0b58ea566d982
BLAKE2b-256 23a6cec34cbf47f48b1e9de8a3264f196f75091b6a4c0b0b0f98a5aa54bc4b05

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page