简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

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

  1. Lossless conversion: Original data must be fully retained in the {platform}_raw field
  2. Standard compatibility: The converted event must conform to the OneBot12 standard format
  3. Platform extension: Platform-specific data is stored in fields with the {platform}_ prefix

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_event is boilerplate code that must be repeatedly written. Using BaseConverter eliminates 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}_raw field 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

  1. Always preserve raw data: The {platform}_raw field must not be omitted
  2. Use standard message segments: Convert platform messages to OneBot12 standard message segments whenever possible
  3. Set detail_type appropriately: Use standard types (private/group/channel etc.), do not define custom ones
  4. Handle edge cases: Raw events may lack certain fields; use .get() with reasonable default values
  5. Performance considerations: convert() is called for every event; avoid performing time-consuming operations within it