SendDSL Explained
SendDSL is a fluent-style message sending interface provided by the ErisPulse adapter.
Basic Calling Methods
1. Specify Type and ID
await adapter.Send.To("group", "123").Text("Hello")
2. Specify ID Only
await adapter.Send.To("123").Text("Hello")
3. Specify Sender Account
await adapter.Send.Using("bot1").Text("Hello")
4. Combine Usage
await adapter.Send.Using("bot1").To("group", "123").Text("Hello")
Method Chaining
flowchart LR
A["Using / Account<br/>(Optional sender account, optional)"] --> B["To<br/>(Optional target type and ID)"]
B --> C["Modifier Methods<br/>At / Reply / Expire / ForMember, etc."]
C --> D["Sending Methods<br/>Text / Image / Voice / Raw_ob12"]
D --> E["Returns asyncio.Task"]
Sending Methods
All sending methods return an asyncio.Task object.
Basic Methods (Built-in in Base Class)
The following standard methods are implemented by the SendDSL base class. By default, they are delegated to Raw_ob12, so adapter subclasses do not need to re-implement them and can use them directly, with IDE auto-completion available:
| Method Name | Description | Return Value |
|---|---|---|
Text(text: str) |
Send text message | asyncio.Task |
Image(file: bytes | str) |
Send image | asyncio.Task |
Voice(file: bytes | str) |
Send voice (OneBot12 audio segment) |
asyncio.Task |
Video(file: bytes | str) |
Send video | asyncio.Task |
File(file: bytes | str, filename: str = None) |
Send file | asyncio.Task |
Adapters can override individual standard methods to provide platform-specific logic:
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs):
# Must be implemented
...
# Optional: Override Text to provide platform-specific logic
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Protocol Methods
| Method Name | Description | Return Value | Required |
|---|---|---|---|
Raw_ob12(message) |
Send OneBot12 formatted message | asyncio.Task |
Must be implemented |
Important:
Raw_ob12is the core method of the adapter and must be implemented. It serves as the unified entry point for reverse transformation (OneBot12 → platform). If not implemented, the base class will log an error and return a standard error response (status: "failed",retcode: 10002). Standard methods (Text,Image, etc.) are delegated toRaw_ob12by default.
Platform-Specific Methods
Adapters can add platform-specific sending methods in the Send subclass (these will be recognized by event.supports() / event.available_methods()):
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs): ...
# Platform-specific method
def Sticker(self, sticker_id: str):
return self.Raw_ob12([{"type": "sticker", "data": {"id": sticker_id}}])
Modifiers
Modifiers return self to support method chaining.
At Method
# @single user
await adapter.Send.To("group", "123").At("456").Text("Hello")
# @multiple users
await adapter.Send.To("group", "123").At("456").At("789").Text("Hello everyone")
AtAll Method
# @all group members
await adapter.Send.To("group", "123").AtAll().Text("Hello everyone")
Reply Method
# Reply to a message
await adapter.Send.To("group", "123").Reply("msg_id").Text("Reply content")
Combined Modifiers
await adapter.Send.To("group", "123").At("456").Reply("msg_id").Text("Reply to the @ message")
Platform-Specific Modifier Methods
In addition to the built-in At/AtAll/Reply, adapters can define platform-specific modifier methods. These methods only need to return self and do not require any decorators — the framework automatically recognizes them:
- Return
self(an instance ofSendDSL) → Modifier method, does not trigger send wrapping/lifecycle events, continues method chaining - Return
Task/Awaitable→ Send method
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs): ...
# Modifier method: returns self, does not send
def Expire(self, seconds: int):
self._expire = seconds
return self
def ForMember(self, user_id: str):
self._member = user_id
return self
# Send method: returns Task, depends on states set by modifier methods
def Board(self, content: str, **kwargs):
return self.Raw_ob12([{"type": "board", "data": {"text": content}}])
Usage:
# Modifier methods can be chained continuously
await adapter.Send.To("group", "big").Expire(3600).ForMember("114").Board("Board content")
Using Modifier Methods in Event Wrapper Classes
Note
The reply(via=) and event.send_chain() features require ErisPulse 2.7.0+.
By default, event.reply() exposes only built-in modifier parameters such as at_sender/at_users/at_all/quote. To use platform-specific modifier methods, there are two approaches:
Method 1: The via Parameter in reply()
Suitable for a small number of known modifier methods:
await event.reply("Board content", method="Board",
via=[("Expire", 3600), ("ForMember", "114514")])
The via parameter is a list, where each element can take one of the following forms:
| Format | Equivalent Chain Call |
|---|---|
"Name" |
.Name() |
("Name", arg1, arg2) |
.Name(arg1, arg2) |
("Name", (arg1,), {kw: val}) |
.Name(arg1, kw=val) |
Method 2: event.send_chain()
Suitable for multiple consecutive modifier methods or action-type methods without content parameters (such as recall or delete). The send_chain() method returns a send chain already configured with To/Using, allowing you to freely append arbitrary modifier methods and send methods:
# Platform-specific modifier methods + board message
await event.send_chain().Expire(3600).Board("Expires in one hour")
# Multiple consecutive modifier methods
await (event.send_chain()
.Expire(3600)
.ForMember("114514")
.Board("Board content", content_type="markdown"))
# Built-in modifier methods are also available
await event.send_chain().At("123").Reply("msg_id").Text("hi")
# Action-type methods without content parameters
await event.send_chain().DismissBoard()
The
send_chain()method returns a complete SendDSL instance, so all chainable features are available—not just modifier methods, but also send rules and batch building:
# Send rules: retry + timeout + success callback
await (event.send_chain()
.Retry(3).Timeout(10)
.Hook(lambda r: print("Message sent successfully"))
.Text("Reliable message"))
# Delayed message + platform modifier + board
await event.send_chain().Defer(5).Expire(3600).Board("Delayed board")
# Batch building mode
results = await (event.send_chain()
.Build()
.Text("First sentence").Image("pic.jpg").Text("Second sentence")
.send_all())
Account Management
Using Method
The Using() method is used to specify the account for sending messages. The identifier passed in will be matched through _resolve_account() in the following priority order:
- Account Name — The key name in the configuration (e.g.,
"default","bot1") - Runtime Injected bot_id — The identifier automatically injected during event conversion
- Any str Field — Any other string field in the configuration
- Fallback — The first enabled account
# Using account name
await adapter.Send.Using("account1").To("user", "123").Text("Hello")
# Using bot_id (i.e., self.user_id from the event)
await adapter.Send.Using("bot_123").To("user", "123").Text("Hello")
Account Method
The Account method is equivalent to Using:
await adapter.Send.Account("account1").To("user", "123").Text("Hello")
Asynchronous Processing
Not Waiting for Results
# Message is sent in the background
task = adapter.Send.To("user", "123").Text("Hello")
# Continue executing other operations
# ...
Waiting for Results
# Await directly to get the result
result = await adapter.Send.To("user", "123").Text("Hello")
print(f"Send result: {result}")
# Save the Task first, then wait later
task = adapter.Send.To("user", "123").Text("Hello")
# ... other operations ...
result = await task
Send Rule System
SendDSL includes a built-in set of send rule decorators. Rules are attached via chainable methods and applied collectively when the final send operation is executed. These rules cover common production scenarios: timeout control, retry on failure, success callbacks, delayed sending, priority-based dropping, and progress monitoring.
Rule methods return self (like At/AtAll/Reply), and must be called before the send method (Text/Image, etc.). Rules propagate along with new instances created by To/Using/Account.
List of Rule Methods
| Method | Description |
|---|---|
.Hook(callback) |
Callback executed on successful send (can be called multiple times, executed in order) |
.Retry(times=1) |
Automatically retry N times on failure (total N+1 attempts, including the first) |
.Timeout(seconds) |
Single send timeout; cancels current attempt if exceeded (can be combined with Retry) |
.Defer(seconds=1.0) |
Delayed send (in-process timer, not persisted) |
.Priority(level, drop_if_busy=False) |
Set priority; messages may be dropped during congestion |
.OnProgress(callback) |
Progress callback at each stage (receives SendContext) |
.OnError(callback) |
Error callback triggered only once on final failure |
Executing Logic After Successful Send (Hook)
# Synchronous callback
await (adapter.Send.To("user", "123")
.Hook(lambda r: print(f"Send successful, message ID: {r['message_id']}"))
.Text("Hello"))
# Asynchronous callback
async def deduct_points(result):
await db.update(user_id="123", points=-1)
await adapter.Send.To("user", "123").Hook(deduct_points).Text("Deduct points")
The Hook is only executed when the send operation is ultimately successful (including after retries); it is not triggered on failure, timeout, or cancellation.
Automatic Retry on Failure (Retry)
# Retry 2 times after the first failure, for a total of 3 attempts
result = await adapter.Send.To("user", "123").Retry(2).Text("With retry")
Retry is triggered when an exception is thrown during send, when the send times out, or when the send returns a response with status == "failed".
Automatic Cancellation on Timeout (Timeout)
# Cancel if a single send exceeds 10 seconds
await adapter.Send.To("user", "123").Timeout(10).Text("With timeout")
# Timeout + Retry: Each attempt lasts up to 10 seconds, with a maximum of 3 attempts
await adapter.Send.To("user", "123").Timeout(10).Retry(2).Text("Timeout retry")
Progress Monitoring (OnProgress / OnError)
def on_progress(ctx):
print(f"Stage: {ctx.stage}, Attempt: {ctx.attempt + 1}/{ctx.max_attempts}, Elapsed: {ctx.elapsed:.2f}s")
if ctx.stage == "failed":
print(f" Error: {ctx.error!r}")
async def on_error(ctx):
await notify_admin(f"Failed to send to {ctx.target_id}: {ctx.error!r}")
await (adapter.Send.To("user", "123")
.Retry(3).Timeout(10)
.OnProgress(on_progress)
.OnError(on_error)
.Text("Monitored"))
SendContext includes the following fields: task_id, platform, method, target_type, target_id, bot_id, stage, attempt, max_attempts, started_at, finished_at, elapsed, error, result, extra.
Possible values for stage: pending, sending, retrying, success, failed, timeout, cancelled, dropped.
Delayed Sending (Defer)
# Send after a 5-second delay
await adapter.Send.To("user", "123").Defer(5).Text("Delayed message")
Note: The delay is an in-process timer; it is not persisted and will be lost if the process restarts.
Priority and Congestion Dropping (Priority)
# Low-priority message, automatically dropped during queue congestion
result = await (adapter.Send.To("user", "123")
.Priority(-1, drop_if_busy=True)
.Text("Droppable notification"))
# If dropped, result["status"] == "failed"
When drop_if_busy is enabled, if the number of in-flight send tasks exceeds the threshold (default 64), the current send is immediately abandoned. The global threshold can be adjusted using .PriorityThreshold(n).
Rule Composition and Background Execution
# Execute without blocking the main flow; rules still apply
task = (adapter.Send.To("user", "123")
.Hook(lambda r: print("Send successful!"))
.Retry(3)
.Timeout(10)
.OnProgress(on_progress)
.Text("Hello"))
# Continue executing other operations
await handle_next_action()
Rule Propagation
Rules propagate with new instances created by To/Using/Account, preventing loss of rules during chained calls:
# Rules set before To are also propagated to the instance created by To
builder = adapter.Send.Retry(3).Timeout(10)
send = builder.To("user", "123") # send still carries Retry(3) and Timeout(10)
await send.Text("hi")
Rule sets for multiple instances are independent (the hooks list is deeply copied).
Batch Build Mode (Build)
In addition to the single-send mode, SendDSL also supports batch build mode: multiple send methods are written in a single chain, and executed at once. This is suitable for scenarios where you want to send multiple messages in one go.
Entering Build Mode
Before calling a send method, call .Build(), which returns a SendBuilder. After this, send methods (such as Text/Image) will not be executed immediately, but will accumulate as send intents:
results = await (adapter.Send.To("user", "123")
.Build() # Enter build mode
.Text("First sentence")
.Image("pic.jpg")
.Text("Second sentence")
.send_all()) # Execute all at once
# results = [Text result, Image result, Text result]
.send_all() returns an asyncio.Task, and awaiting it yields a list of results (in the order of the intents).
Parallel vs Sequential
By default, execution is parallel (concurrent sending, total time is approximately equal to the slowest message). To ensure the order of message arrival, call .Sequential():
# Sequential: Send in order
await (adapter.Send.To("group", "456")
.Build()
.Sequential()
.Text("Send this first").Text("Then send this")
.send_all())
# Parallel (default, can be explicitly called)
await (adapter.Send.To("group", "456")
.Build()
.Parallel()
.Text("Parallel 1").Text("Parallel 2")
.send_all())
Continue on Failure and Retry
Batch execution uses a continue on failure strategy: if one message fails, it does not interrupt the sending of other messages. When combined with .Retry(), failed messages will automatically retry (retry applies to individual messages, not the entire batch):
await (adapter.Send.To("user", "123")
.Build()
.Retry(2) # Each message retries 2 times
.Text("May fail").Image("May also fail")
.send_all())
Batch-wide Rules and Callbacks
Rules apply uniformly to the entire batch:
| Method | Description |
|---|---|
.Timeout(seconds) |
Timeout for each individual send |
.Retry(times) |
Each send retries individually (continue on failure) |
.Defer(seconds) |
Delay the entire batch |
.Hook(callback) |
Triggered after the entire batch succeeds, receives results list |
.OnError(callback) |
Triggered when the batch has failures, receives BatchContext |
.OnProgress(callback) |
Triggered when each message completes, receives BatchContext |
def on_progress(ctx):
print(f"Progress: {ctx.completed}/{ctx.total}, succeeded {ctx.succeeded}, failed {ctx.failed}")
async def on_error(ctx):
print(f"There are {ctx.failed} failed messages in the batch")
results = await (adapter.Send.To("user", "123")
.Build()
.Retry(2).Timeout(10)
.OnProgress(on_progress)
.OnError(on_error)
.Hook(lambda rs: print("Batch completed"))
.Text("a").Text("b").Text("c")
.send_all())
BatchContext contains: task_id, total, completed, succeeded, failed, stage, results, errors, elapsed, extra.
stage possible values: pending, sending, success (all succeeded), partial (partially succeeded), failed (all failed).
Decorators and Rule Inheritance
Decorators and rules before .Build() are inherited by the entire batch and apply to each message:
await (adapter.Send.To("group", "456")
.At("789") # Inherited: each message @789
.Build()
.Retry(2) # Inherited + appended: each message retries individually
.Text("@Your notification")
.Image("Announcement image")
.send_all())
After entering Build mode, you can still append decorators (applying to the entire batch):
await (adapter.Send.To("group", "456")
.Build()
.At("111").At("222") # Appended @, applies to entire batch
.Text("@Multiple people")
.send_all())
Background Execution
As with single-send, .send_all() returns a Task, which can be executed in the background without awaiting:
task = (adapter.Send.To("user", "123")
.Build()
.Hook(lambda rs: print("Batch send completed"))
.Text("a").Text("b")
.send_all())
# Non-blocking main flow
await do_something_else()
Naming Convention
PascalCase Naming
All send methods should use the PascalCase naming convention:
# ✅ Correct
def Text(self, text: str):
pass
def Image(self, file: bytes):
pass
# ❌ Incorrect
def text(self, text: str):
pass
def send_image(self, file: bytes):
pass
Platform-specific Methods
Avoid adding platform prefixes to methods:
# ✅ Recommended
def Sticker(self, sticker_id: str):
pass
# ❌ Not recommended
def TelegramSticker(self, sticker_id: str):
pass
Use Raw methods instead:
# ✅ Recommended
await adapter.Send.Raw_ob12([{"type": "sticker", ...}])
# ❌ Not recommended
def TelegramSticker(self, ...):
pass
Internal Decomposition of the Send Chain
Behind a single await adapter.Send.To("group", "123").Text("x"), the framework performs the following sequence of operations for you:
flowchart TD
A["adapter.Send.To(...).Text(...)"] --> B["Chain methods To/Using<br/>Each returns a new immutable instance (order doesn't matter)"]
B --> C["__getattribute__ intercepts send methods<br/>Wraps with a rule wrapper"]
C --> D["Calls the original method (e.g. Text)<br/>Internally delegates to Raw_ob12"]
D --> E["Raw_ob12 returns asyncio.create_task(...)"]
E --> F["Write [Send] log"]
F --> G["emit message.sending (fire-and-forget)"]
G --> H{"Is a send rule declared?"}
H -->|"No"| I["Task done_callback → emit message.sent"]
H -->|"Yes"| J["apply_send_rules wraps into an outer Task<br/>Retry/timeout/delay/priority"]
J --> I
I --> K["await receives a standard response dict"]
What the framework does at each step:
| Phase | What the framework does |
|---|---|
| Chain merging | To/Using/Account each call creates a new immutable instance and inherits previously set fields, so To(...).Using(...) and Using(...).To(...) are equivalent and order doesn't matter |
| Method wrapping | Send methods (Text, etc.) are intercepted and wrapped by __getattribute__; modifier methods (To/Using/At/Retry, etc.) are not wrapped. Nested Raw_ob12 calls are prevented from repeated wrapping using the _in_rule_wrap marker |
| Task creation | Raw_ob12 internally uses asyncio.create_task() to create the Task; Text() only synchronously returns this Task, without blocking |
| Send logging | Writes [Send] platform/method -> target event log (can be suppressed with exclude_levels=["EVENT"]) |
message.sending |
Triggered immediately in a fire-and-forget manner when the send method is called (only if listeners exist, short-circuited by has_handlers) |
message.sent |
Bound to the Task's done_callback — when rules are present, it covers the final result of the entire retry process; without rules, it is simply the completion of the original Task |
Account Resolution Fallback Chain
When the adapter internally calls _resolve_account(account_id), it resolves to a specific account in the following order:
- Single-account adapter (no
AccountConfigClass) → directly returns - Exact match of account name
account_id - Match of each account's
bot_idfield - Match of any
strfield value in each account (excludingenabled/name) - Fallback to the first enabled account
- If all fail → raises
ValueError
The
account_idyou provide comes from:Using()explicitly specified > eventselffield (whereaccount_idtakes precedence overuser_id, automatically injected byevent.reply()) > unspecified (adapter falls back to the first enabled account).
Send Rule Engine (Retry/Timeout/Delay)
Rules are wrapped into a new outer Task after Raw_ob12 returns the Task, without affecting the main flow. Key facts:
| Rule | Description |
|---|---|
Retry(n) |
Total attempts: n+1; immediate re-send after failure, no exponential backoff |
Timeout(s) |
Single send times out and is cancelled (asyncio.wait_for), retries if not exhausted |
Defer(s) |
Delays sleep before sending |
Priority(level, drop_if_busy) |
If backlog exceeds threshold, directly returns {status:"failed", retcode:10002, message:"dropped_low_priority"} |
Hook(fn) |
Only executed in order when the final send is successful |
on_progress / on_error |
Callbacks at each stage / final failure |
Note: Retries are "immediate re-sends" without any backoff interval; if platform rate limiting requires backoff, please manually sleep and re-send in the
on_errorcallback. Rule success is determined bystatus == "ok"in the returned dict (whereretcode == 0).
For the complete semantics of the standard response format and
retcode, see API Response Specification.
Return Values
Task Object
All send methods return an asyncio.Task. The adapter only needs to implement Raw_ob12, and the standard methods (Text/Image, etc.) are delegated by default:
import asyncio
def Raw_ob12(self, message, **kwargs):
async def _do_send():
segments = self._apply_modifiers(message)
return await self._adapter.call_api(
endpoint="/send_message",
message=segments,
**self.send_context,
**kwargs,
)
return asyncio.create_task(_do_send())
# Text/Image/Voice/Video/File are inherited from the base class and automatically delegated to Raw_ob12
# If you need to override standard methods, just return an asyncio.Task:
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Standardized Response
call_api should return a standardized response. It is recommended to use the make_response() / make_error() methods:
async def call_api(self, endpoint: str, **params):
try:
result = await self._do_api_call(endpoint, **params)
return self.make_response(
data=result.get("data"),
message_id=result.get("message_id", ""),
raw=result,
)
except Exception as e:
return self.make_error(message=str(e))
Manual construction is also supported (the old way is still compatible):
async def call_api(self, endpoint: str, **params):
return {
"status": "ok" or "failed",
"retcode": 0 or error_code,
"data": {...},
"message_id": "msg_id" or "",
"message": "",
"{platform}_raw": raw_response
}
Complete Examples
Basic Usage
from ErisPulse.Core import adapter
my_adapter = adapter.get("myplatform")
# Send text message
await my_adapter.Send.To("user", "123").Text("Hello World!")
# Send image
await my_adapter.Send.To("group", "456").Image("https://example.com/image.jpg")
# Send file
with open("document.pdf", "rb") as f:
await my_adapter.Send.To("user", "123").File(f.read())
Chained Calls
# @user + reply
await my_adapter.Send.To("group", "456").At("789").Reply("msg123").Text("Reply to @ message")
# @all + multiple modifiers
await my_adapter.Send.Using("bot1").To("group", "456").AtAll().Text("Announcement message")
Raw Messages and Message Building
Raw_ob12 is the core entry point for reverse conversion (OneBot 12 message segments → platform API calls), and MessageBuilder is a chainable message segment builder designed to work with it.
For the complete
Raw_ob12implementation specification,MessageBuilderusage, and code examples, please refer to:
Related Documentation
- Getting Started with Adapter Development - Create an adapter
- Core Concepts of Adapters - Understand the adapter architecture
- Best Practices for Adapters - Develop high-quality adapters
- Send Method Specification - Complete specification for the send method