Event Converter Implementation Guide
The Event Converter (Converter) is one of the core components of the adapter, responsible for transforming platform-native events into ErisPulse's unified OneBot12 standard event format.
Converter Responsibilities
Platform-native event ──→ Converter.convert() ──→ OneBot12 standard event
The Converter is only responsible for forward conversion (receiving direction), transforming platform-native event data into the OneBot12 standard format. Reverse conversion (sending direction) is handled by the Send.Raw_ob12() method.
Core Principles
- Lossless conversion: Original data must be fully retained in the
{platform}_rawfield - Standard compatibility: The converted event must conform to the OneBot12 standard format
- Platform extension: Platform-specific data is stored in fields with the
{platform}_prefix
BaseConverter Base Class (Recommended)
Starting from version 2.7.0, the framework provides the BaseConverter base class (ErisPulse.Core.Bases), which encapsulates the common field construction and common message segment utilities for OneBot12 events, allowing converters to focus only on type mapping:
from ErisPulse.Core.Bases import BaseConverter
class MyConverter(BaseConverter):
def __init__(self):
super().__init__(platform="myplatform")
def convert(self, raw_event: dict) -> dict | None:
if not isinstance(raw_event, dict):
return None
event_type = raw_event.get("type", "")
base = self.build_base_event(raw_event, event_type) # id/time/platform/self/raw
if event_type == "message":
base["type"] = "message"
base["detail_type"] = "group" if raw_event.get("group_id") else "private"
base["user_id"] = str(raw_event.get("sender_id", ""))
base["message"] = [self.text(raw_event.get("content", ""))]
base["alt_message"] = raw_event.get("content", "")
return base
return None
build_base_event() already fills the following common fields:
| Field | Source |
|---|---|
id |
raw_event["event_id"], UUID generated if missing |
time |
raw_event["timestamp"], current time if missing |
platform |
platform passed during initialization |
self |
{"platform": ..., "user_id": raw_event["bot_id"]} |
{platform}_raw |
Raw event (satisfies "lossless conversion" principle) |
{platform}_raw_type |
Raw event type |
Common message segment utility methods (all static methods, directly reusable):
converter.text("hi") # {"type": "text", "data": {"text": "hi"}}
converter.at("123456") # {"type": "at", "data": {"user_id": "123456"}}
converter.image("file.png") # {"type": "image", "data": {"file": "file.png"}}
When manually implementing, the common field construction in
build_base_eventis boilerplate code that must be repeatedly written. UsingBaseConvertereliminates this, and naturally ensures "lossless conversion" (the raw event always goes into{platform}_raw).
convert() Method
Method Signature
def convert(self, raw_event: dict) -> dict:
"""
Converts platform-native event data to OneBot12 standard format.
:param raw_event: Platform-native event data
:return: OneBot12 standard event dictionary
"""
pass
Return Value Structure
The converted event dictionary should include the following standard fields:
{
"id": "unique event ID",
"time": 1234567890, # Unix timestamp (seconds)
"type": "message", # Event type
"detail_type": "private", # Detailed type
"platform": "myplatform", # Platform name
"self": {
"platform": "myplatform",
"user_id": "bot_user_id"
},
# Message event fields
"user_id": "sender_id",
"message": [...], # OneBot12 message segment list
"alt_message": "plain text content",
# Original data must be preserved
"myplatform_raw": { ... }, # Platform-native event complete data
"myplatform_raw_type": "native event type name",
}
Required Field Mapping
Common Fields (All Event Types)
| OB12 Field | Type | Description |
|---|---|---|
id |
str | Unique event identifier |
time |
int | Unix timestamp (seconds) |
type |
str | Event type: message / notice / request / meta |
detail_type |
str | Detailed type: private / group / friend etc. |
platform |
str | Platform name, consistent with adapter registration name |
self |
dict | Bot information: {"platform": "...", "user_id": "..."} |
Message Event Additional Fields
| OB12 Field | Type | Description |
|---|---|---|
user_id |
str | Sender ID |
message |
list[dict] | OneBot12 message segment list |
alt_message |
str | Plain text fallback content |
Notice Event Additional Fields
| OB12 Field | Type | Description |
|---|---|---|
user_id |
str | Related user ID |
operator_id |
str | Operator ID (e.g., group member changes) |
Message Segment Conversion
OneBot12 standard defines the following message segment types:
# Text
{"type": "text", "data": {"text": "Hello"}}
# Image
{"type": "image", "data": {"file": "https://example.com/img.jpg"}}
# Audio
{"type": "audio", "data": {"file": "https://example.com/audio.mp3"}}
# Video
{"type": "video", "data": {"file": "https://example.com/video.mp4"}}
# File
{"type": "file", "data": {"file": "https://example.com/doc.pdf"}}
# Mention
{"type": "mention", "data": {"user_id": "123"}}
# Mention All
{"type": "mention_all", "data": {}}
# Reply
{"type": "reply", "data": {"message_id": "msg_123"}}
If the platform does not support certain message segment types, you can omit the segment or convert it to the closest standard type.
Platform Extension Fields
Platform-specific data should be stored using the {platform}_ prefix to avoid conflicts with standard fields:
{
# Standard fields
"type": "message",
"detail_type": "group",
# ...
# Platform extension fields
"myplatform_raw": { ... }, # Raw event data (required)
"myplatform_raw_type": "chat", # Raw event type (required)
# Other platform-specific fields
"myplatform_group_name": "Group Name",
"myplatform_sender_role": "admin",
}
Important: The
{platform}_rawfield is required, as ErisPulse's event system and modules may depend on it to access platform-native data.
Complete Example
Here is a complete implementation of a Converter:
class MyConverter:
def __init__(self, platform: str):
self.platform = platform
def convert(self, raw_event: dict) -> dict:
event_type = raw_event.get("type", "")
base_event = {
"id": raw_event.get("id", ""),
"time": raw_event.get("timestamp", 0),
"platform": self.platform,
"self": {
"platform": self.platform,
"user_id": raw_event.get("self_id", ""),
},
"myplatform_raw": raw_event,
"myplatform_raw_type": event_type,
}
if event_type == "chat":
return self._convert_message(raw_event, base_event)
elif event_type == "notification":
return self._convert_notice(raw_event, base_event)
elif event_type == "request":
return self._convert_request(raw_event, base_event)
return base_event
def _convert_message(self, raw: dict, base: dict) -> dict:
base["type"] = "message"
base["detail_type"] = "group" if raw.get("group_id") else "private"
base["user_id"] = raw.get("sender_id", "")
base["message"] = self._convert_message_segments(raw.get("content", ""))
base["alt_message"] = raw.get("content", "")
if raw.get("group_id"):
base["group_id"] = raw["group_id"]
return base
def _convert_message_segments(self, content: str) -> list:
segments = []
if content:
segments.append({"type": "text", "data": {"text": content}})
return segments
def _convert_notice(self, raw: dict, base: dict) -> dict:
base["type"] = "notice"
notification_type = raw.get("notification_type", "")
if notification_type == "member_join":
base["detail_type"] = "group_member_increase"
base["user_id"] = raw.get("user_id", "")
base["group_id"] = raw.get("group_id", "")
base["operator_id"] = raw.get("operator_id", "")
elif notification_type == "friend_add":
base["detail_type"] = "friend_increase"
base["user_id"] = raw.get("user_id", "")
return base
def _convert_request(self, raw: dict, base: dict) -> dict:
base["type"] = "request"
request_type = raw.get("request_type", "")
if request_type == "friend":
base["detail_type"] = "friend"
base["user_id"] = raw.get("user_id", "")
base["comment"] = raw.get("message", "")
elif request_type == "group_invite":
base["detail_type"] = "group"
base["group_id"] = raw.get("group_id", "")
base["user_id"] = raw.get("inviter_id", "")
return base
Rich Media Message Conversion Example
Platform messages often contain rich media such as images, mentions, and replies. Here is an example of _convert_message_segments handling multiple message types:
def _convert_message_segments(self, raw_content: list) -> list:
"""Converts platform-native message segment list into OneBot12 standard message segments"""
segments = []
for item in raw_content:
item_type = item.get("type", "")
if item_type == "text":
segments.append({
"type": "text",
"data": {"text": item.get("content", "")}
})
elif item_type == "image":
file_url = item.get("url") or item.get("file_id", "")
segments.append({
"type": "image",
"data": {"file": file_url}
})
elif item_type == "at":
segments.append({
"type": "mention",
"data": {"user_id": item.get("target_id", "")}
})
elif item_type == "reply":
segments.append({
"type": "reply",
"data": {"message_id": item.get("reply_to_id", "")}
})
elif item_type == "at_all":
segments.append({"type": "mention_all", "data": {}})
else:
segments.append({
"type": "text",
"data": {"text": f"[Unsupported message type: {item_type}]"}
})
return segments
Common Pitfalls
1. Missing {platform}_raw Field
This is the most common mistake. Missing the raw data field will prevent modules from accessing platform-specific information.
base_event["myplatform_raw"] = raw_event # Required!
base_event["myplatform_raw_type"] = event_type # Required!
2. Incorrect Timestamp Format
OneBot12 requires the time field to be a Unix timestamp in seconds (integer). If your platform returns milliseconds or an ISO format string, you must convert it:
import time
# Milliseconds → seconds
"time": raw_event.get("timestamp", 0) // 1000
# ISO string → seconds
"time": int(time.mktime(time.strptime(raw_event["created_at"], "%Y-%m-%dT%H:%M:%S")))
3. Missing self Field
The self field contains bot information, with user_id being the bot's account ID. This field is crucial in multi-bot scenarios:
"self": {
"platform": self.platform,
"user_id": raw_event.get("bot_id", ""), # Bot's own ID
}
4. Using Non-standard detail_type Values
detail_type must use the values defined by OneBot12, such as private, group, friend_increase, group_member_increase, etc. Do not use platform-specific naming.
5. Round-trip Consistency
Ensure that the message segment types generated by the Converter correspond to the methods supported by the Send end. For example, if the Converter converts a platform image message to {"type": "image", ...}, then the Send end's Image() method must be able to handle image sending.
Best Practices
- Always preserve raw data: The
{platform}_rawfield must not be omitted - Use standard message segments: Convert platform messages to OneBot12 standard message segments whenever possible
- Set
detail_typeappropriately: Use standard types (private/group/channeletc.), do not define custom ones - Handle edge cases: Raw events may lack certain fields; use
.get()with reasonable default values - Performance considerations:
convert()is called for every event; avoid performing time-consuming operations within it
Related Documentation
- Adapter Core Concepts - Overall adapter architecture
- SendDSL Guide - Reverse conversion (sending direction)
- Event Conversion Standard - Formal event conversion specification
- Session Type System - Session type mapping rules