Skip to main content

Python SDK for Dooray.com with Socket Mode support

Project description

Dooray! SDK - Python

A Python SDK for building agents on Dooray!. This document covers the core classes, decorators, and usage patterns.

Highlights

  • One SDK surface for the messenger, task, and wiki APIs — plus real-time events from other Dooray! services
  • Real-time task / wiki events via the common WebSocket (@agent.on("task", action="createTask"))
  • 13 task and 16 wiki REST methods — 29 in total (agent.task.create_task(...), agent.wiki.create_page(...))
  • Unified LLM access for Dooray! AI, OpenAI, and Azure OpenAI (dooray_sdk.query)

Installation

Install via pip

# Currently in beta (requires --pre flag)
pip install --pre dooray-sdk

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

Dependencies

Installed automatically with the package:

Package Version Description
requests >= 2.25.0 Sync HTTP client
websocket-client >= 1.0.0 WebSocket client
python-dotenv >= 1.0.0 .env file support
aiohttp >= 3.8.0 Async HTTP/WebSocket transport (powers Agent real-time runs)

Optional Extras

Extra Installs When to use
[aiohttp] aiohttp>=3.8.0 Included by default since 0.1.0b1 — kept as a backward-compat alias
[templates] cookiecutter>=2.0.0 dooray-sdk init project templates
[llm] langchain-core, langchain-openai, pydantic LLM helpers (query, QueryOptions)
[all] Everything above Full installation

The LLM helpers expose a simple query() API on top of LangChain — see examples/ai_agent.py in the repository for a working agent.

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
DOORAY_BOT_TOKEN No Fallback token (checked when DOORAY_AGENT_TOKEN is unset) your_agent_token
DOORAY_SERVICES No Comma-separated service list (default: messenger) messenger,common

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"] WebSocket services to connect: "messenger" and/or "common". Falls back to the DOORAY_SERVICES env var. Task/wiki events arrive over the common connection

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. Only valid after run()/start() — accessing it earlier raises AttributeError
messenger ServiceNamespace Messenger API namespace (agent.messenger.send_message(...) etc.)
task ServiceNamespace Task API namespace (agent.task.create_task(...) etc., 13 methods)
wiki ServiceNamespace Wiki API namespace (agent.wiki.create_page(...) etc., 16 methods)

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 methods together (Slack Bolt / atlassian-python-api style).

Namespace Method 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 create_task, get_task, list_tasks, modify_task, set_done, ... Task (project post) CRUD — 13 methods, see TaskAPI
agent.wiki list_wikis, create_page, get_page, modify_page, move_page, ... Wiki page/comment CRUD — 16 methods, see WikiAPI

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

Task and wiki events are delivered over the common WebSocket connection — pass services=["messenger", "common"] to receive them alongside messenger events.

from dooray_sdk import Agent

agent = Agent(services=["messenger", "common"])

@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="createTask")
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]
    accepts_response_payload: bool = False
    service: str = "messenger"
    action: str = ""                                # wrapped into ActionWrapper
    entity: Any = None                              # wrapped into EntityWrapper
    actor: Any = None                               # wrapped into ActorWrapper
    action_data: Optional[Dict[str, Any]] = None    # internal; access via action.data
    channel_data: Optional[Dict[str, Any]] = None   # access via channel_info

Raw dicts passed to entity, actor, and action are automatically converted to wrapper objects (EntityWrapper, ActorWrapper, ActionWrapper) in __post_init__.

Properties

Property Type Description
envelope_id str Unique message ID
type str Event type (message, task, taskComment, page, pageComment, etc.)
payload Dict[str, Any] Raw payload data
service str Service name (messenger, task, wiki)
action ActionWrapper Action info; compares equal to strings (req.action == "create")
entity EntityWrapper Entity information wrapper
actor ActorWrapper Actor information wrapper

Convenience Properties

Property Type Description
is_message bool Whether this is a message type
text str | None Message text
channel_info ChannelInfo | None Channel info (id, scope, type). None when the raw event has no channel field
channel str | None Deprecated. Use channel_info.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.<method>(...) directly.
is_message / is_task / is_page Type check properties
is_action_type(action) Check action (accepts a string or a list of strings). req.action == "create" also works
is_service(service) Check service
to_dict() Convert to dictionary
from_dict(data) Create from dictionary (class method)

SocketModeRequest is now a pure event-data object — action methods 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.action == "create":       # or req.is_action_type("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 methods (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",
        timeout: int = 30
    )

Service Namespace

Namespace Type Description
client.messenger AsyncMessengerAPI Messenger service methods (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 method is provided in both sync (MessengerAPI) and async (AsyncMessengerAPI) flavours.

TaskAPI / AsyncTaskAPI

Task (project post) service clients — 13 methods. Reached via agent.task.<method>(...) inside a running agent, or constructed directly with a transport (TaskAPI(WebClient(...)) / AsyncTaskAPI(AsyncWebClient(...))). Both classes are importable from dooray_sdk.

Method Description
create_task(project_id, subject, body, **kw) Create a task. A str body is auto-wrapped as text/x-markdown
get_task(project_id, post_id) Get task details
get_task_by_id(post_id) Get a task without a project ID
list_tasks(project_id, *, page=0, size=20, **filters) List tasks. size is clamped to 100 with a UserWarning
modify_task(project_id, post_id, *, version=None, **kw) Modify a task (version enables optimistic locking)
set_workflow(project_id, post_id, workflow_id) Change the task workflow state
set_assignee_workflow(project_id, post_id, member_id, workflow_id) Change one assignee's state (member_id="me" allowed)
set_done(project_id, post_id) Mark the task as done
create_comment(project_id, post_id, body, *, attach_file_ids=None) Create a comment
list_comments(project_id, post_id, *, page=0, size=20, order="createdAt") List comments
get_comment(project_id, post_id, log_id) Get one comment
modify_comment(project_id, post_id, log_id, body, *, attach_file_ids=None) Modify a comment
delete_comment(project_id, post_id, log_id) Delete a comment
@agent.on("message")
async def create_task_from_message(req, client):
    result = await client.task.create_task(
        project_id="<project-id>",
        subject="New task from chat",
        body=req.text or "(no text)",   # str is wrapped as text/x-markdown
    )
    print(result)

WikiAPI / AsyncWikiAPI

Wiki service clients — 16 methods (11 page + 5 comment). Reached via agent.wiki.<method>(...), or constructed directly (WikiAPI(WebClient(...)) / AsyncWikiAPI(AsyncWebClient(...))). Both classes are importable from dooray_sdk.

Method Description
list_wikis(*, page=0, size=20) List accessible wikis
create_page(wiki_id, subject, body, *, parent_page_id=None, attach_file_ids=None, referrers=None) Create a page
list_pages(wiki_id, *, parent_page_id=None) List sibling pages (top-level when parent_page_id is omitted)
get_page(wiki_id, page_id) Get a page
get_page_by_id(page_id) Get a page without a wiki ID
modify_page(wiki_id, page_id, *, subject=None, body=None, referrers=None) Modify a page. None fields are omitted; all-None raises ValueError
set_page_title(wiki_id, page_id, subject) Update the title only
set_page_content(wiki_id, page_id, body) Update the content only
set_page_referrers(wiki_id, page_id, referrers) Update the referrers only
move_page(wiki_id, page_id, target_parent_page_id, *, target_wiki_id=None, with_children=True, before_page_id=None) Move / reorder a page
delete_page(wiki_id, page_id) Delete a page
create_comment(wiki_id, page_id, body) Create a comment
list_comments(wiki_id, page_id, *, page=0, size=20) List comments
get_comment(wiki_id, page_id, comment_id) Get one comment
modify_comment(wiki_id, page_id, comment_id, body) Modify a comment
delete_comment(wiki_id, page_id, comment_id) Delete a comment
@agent.on("message")
async def note_to_wiki(req, client):
    page = await client.wiki.get_page(wiki_id="<wiki-id>", page_id="<page-id>")
    await client.wiki.create_comment(
        wiki_id="<wiki-id>",
        page_id="<page-id>",
        body=f"From chat: {req.text}",
    )

As with the messenger methods, body accepts either a raw str (auto-wrapped as {"mimeType": "text/x-markdown", "content": ...}) or a dict for full control (e.g. text/html). Every method is provided in both sync and async 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="createTask")    # task registration 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.<method>(...) etc. to send.

Examples

# Messenger messages only
@agent.on("message")
async def handle_message(req, client):
    pass

# Specific action
@agent.on("message", action="create")
async def handle_new_message(req, client):
    pass

# common service — new task created
@agent.on("task", action="createTask")
async def on_task_created(req, client):
    pass

# common service — wiki page created / referrers added
@agent.on("page", action=["createPage", "addPageUser"])
async def on_wiki_changed(req, client):
    pass

# common service — task comment created
# comment events use event_type "taskComment" — subscribing to "task" does NOT match
@agent.on("taskComment", action="createComment")
async def on_task_comment(req, client):
    pass

# Everything (debugging / unified logging)
@agent.on("all")
async def log_all(req, client):
    pass

Events delivered via the common service

When connected with Agent(services=["messenger", "common"]), a single WebSocket delivers task / wiki and other Dooray! events together. Register handlers by event type (req.type) and narrow them down with service / action:

Event type (event_type) service action (as observed) Description
task task createTask / addTaskUser Task created / assignees·cc added
taskComment task createComment Task comment created
page wiki createPage / addPageUser Wiki page created / referrers added
pageComment wiki createComment Wiki page comment created
driveFile drive (as published by the server) Drive file changes

Handlers are routed by entity.type. Comment events are routed separately with event_type taskComment / pageComment@agent.on("task", action="createComment") does not match; subscribe with @agent.on("taskComment", action="createComment") instead.

This table reflects events as published by the server. The SDK forwards the service / type / action values of common payloads as-is without filtering, so new event kinds added on the server side arrive without an SDK update.

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: typed wrapper    # message→MessageData, task→TaskData,
│                              # taskComment→TaskCommentData, page→PageData,
│                              # pageComment→PageCommentData, unknown→DataWrapper
├── actor: ActorWrapper        # Action performer info
│   ├── type: str              # Actor type
│   └── data: DataWrapper      # Actor data
└── action: ActionWrapper      # Action details
    ├── type: str              # Action type
    └── data: DataWrapper      # Action data

Entity types with typed wrappers (message / task / taskComment / page / pageComment) support nested dot access such as req.entity.data.project.id and req.entity.data.workflow.class_. Unknown types (driveFile, etc.) fall back to the generic DataWrapper, which supports one-level dot access only.

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

Usage:

# Dot notation access (one-level fields only on the generic DataWrapper)
data.id
data.subject

# Dictionary access (use this for nested dicts)
data["id"]
data.get("subject", "default")
data["nested"]["value"]

The generic DataWrapper does not wrap nested dicts, so nested dot access like data.nested.value is not available on it. Entity types with typed wrappers (task, page, etc. — see Task/Wiki Event Data below) do support nested dot access.

EntityWrapper

Wraps entity information (tasks, pages, etc.).

Property Type Description
type str Entity type (e.g., "task", "taskComment", "page", "pageComment")
data typed wrapper Entity detail data — messageMessageData, taskTaskData, taskCommentTaskCommentData, pagePageData, pageCommentPageCommentData, unknown types→DataWrapper

Usage:

@agent.on("task", action="createTask")
async def handle_task(req, client):
    print(req.entity.type)               # "task"
    data = req.entity.data               # TaskData
    print(data.id)                       # Task ID
    print(data.subject)                  # Task subject
    print(data.project.id)               # Project ID (nested dot access)
    print(data.workflow.class_)          # Workflow class — 'class' is a reserved word, hence class_
    print(data.workflow["class"])        # dict access also supported
    if data.to:
        print(data.to[0].organizationMember.name)  # Assignee name

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)

Task/Wiki Event Data

For task / wiki events, entity.data is wrapped in a typed wrapper that supports nested dot access.

entity.type Wrapper Key fields
task TaskData id, number, subject, createdAt, project(id, code), workflow(id, name, class_), to[]/cc[](organizationMember/emailUser), files[]
taskComment TaskCommentData id, task(id, subject), project, createdAt, files[] — the spec includes no comment body; re-fetch via the API when needed
page PageData id, subject, createdAt, project, referrers[], files[]
pageComment PageCommentData id, page(id, subject), project, createdAt, body(mimeType, content — optional), files[]
  • The workflow class field is the Python reserved word class, so it is exposed as the workflow.class_ property; workflow["class"] dict access also works.
  • Fields defined in the spec return safe defaults ("" / 0 / []) instead of raising when absent, and the ["key"] / .get() dict contract is preserved.
  • These wrappers live in the dooray_sdk.socket_mode.request module and are not exported at the top level (dooray_sdk) — for isinstance checks etc., import them as from dooray_sdk.socket_mode.request import TaskData.
@agent.on("taskComment", action="createComment")
async def on_task_comment(req, client):
    c = req.entity.data                    # TaskCommentData
    print(c.task.subject)                  # Subject of the commented task
    print(c.project.code)                  # Project code
    for f in c.files:
        print(f.name, f.size)              # Attached files

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, no API call)
        name = await member.name      # Member name (API call when needed)
        print(name)

Inside an async handler, always fetch remote fields with await member.<field>. Sync access (print / comparison) only works for fields present in the local data or already fetched via await; sync-accessing an unfetched field inside an async handler raises RuntimeError (see Lazy Loading below).

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 (always use this form in async handlers)
    name = await member.name
    email = await member.externalEmailAddress

    # Sync access (print, comparison, etc.) — only for fields present in
    # local data or already fetched via await, as above
    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

Caution: Sync-accessing a remote field that has not been fetched yet inside an async handler raises RuntimeError: Sync access not available in async context. Use await member.<field> in async contexts. Sync access that triggers an API call only works outside async contexts (plain sync code).

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

Task/wiki events arrive over the common WebSocket connection.

from dooray_sdk import Agent

agent = Agent(services=["messenger", "common"])

@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.subject}")

@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

All SDK exceptions inherit from DooraySDKError and are importable from dooray_sdk.

Exception Cause Solution
DoorayConfigurationError Token/domain not set or invalid Set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN
DoorayConnectionError WebSocket connection failed Check network and token. (DoorayWebSocketError is defined but not currently raised by the SDK)
RuntimeError Calling reply before agent starts Use after agent.run()

Exception Handling Example

from dooray_sdk import Agent, DoorayConfigurationError

try:
    agent = Agent()
except DoorayConfigurationError 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 methods 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 methods

req.reply() historically meant three different things in Dooray!. Each is now its own method:

Intent New method
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.* methods)
WebClient / AsyncWebClient .send_message / .get_channels / .get_messages / .get_user_info client.messenger.<method>(...)

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.1.0b1.tar.gz (150.4 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.1.0b1-py3-none-any.whl (93.8 kB view details)

Uploaded Python 3

File details

Details for the file dooray_sdk-0.1.0b1.tar.gz.

File metadata

  • Download URL: dooray_sdk-0.1.0b1.tar.gz
  • Upload date:
  • Size: 150.4 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.1.0b1.tar.gz
Algorithm Hash digest
SHA256 d78d520c976eeb76a0b5aed652fe397acb1a58a9d8ce8e6e4f22ac99eaa9ddfe
MD5 573064cb04c7ccc1e42b9987ba576667
BLAKE2b-256 6e23886d7d4dae15544d8ba36c4bb0cb6f7d859e4b58a43966a515faaccd7a9c

See more details on using hashes here.

File details

Details for the file dooray_sdk-0.1.0b1-py3-none-any.whl.

File metadata

  • Download URL: dooray_sdk-0.1.0b1-py3-none-any.whl
  • Upload date:
  • Size: 93.8 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.1.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 2c5a9d41710d3f6fb1ca9cae9af19378f580287c5c81886c6d57a5e24396b38c
MD5 6d58e2fc6d1ebb6606ad224a18fb7254
BLAKE2b-256 61a017298fe3069b7e1fef98bd3f82e0b62f64b892f4dfad7cb0ea8fbf4a9779

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