package snowluma import ( "encoding/json" "errors" "fmt" "strconv" "strings" ) // ErrNotConnected 表示当前没有任何可用的 OneBot 连接来执行 API 调用。 var ErrNotConnected = errors.New("snowluma: no connected session") // Response 是 OneBot 11 的通用动作响应信封。 type Response struct { Status string `json:"status"` Retcode int `json:"retcode"` Data json.RawMessage `json:"data"` Echo string `json:"echo"` Message string `json:"message,omitempty"` Wording string `json:"wording,omitempty"` } // Err 在响应失败时返回非 nil 错误。 func (r *Response) Err() error { if r == nil || r.Retcode == 0 || r.Status == "ok" || r.Status == "async" { return nil } msg := r.Wording if msg == "" { msg = r.Message } if msg == "" { msg = "unknown error" } return fmt.Errorf("snowluma: action failed retcode=%d status=%s: %s", r.Retcode, r.Status, msg) } // Decode 将响应的 data 字段解码到 out。 func (r *Response) Decode(out any) error { if r == nil || out == nil { return nil } if len(r.Data) == 0 || string(r.Data) == "null" { return nil } return json.Unmarshal(r.Data, out) } // IntString 兼容数字或字符串形式出现的整数字段(如群成员 level)。 type IntString int64 func (v *IntString) UnmarshalJSON(b []byte) error { s := strings.Trim(string(b), `"`) if s == "" || s == "null" { *v = 0 return nil } n, err := strconv.ParseInt(s, 10, 64) if err != nil { return fmt.Errorf("snowluma: invalid integer %q: %w", s, err) } *v = IntString(n) return nil } // Sender 是消息事件的发送者信息。群聊时含 Card/Role,私聊时可能为空。 // 注意:SnowLuma v4 系的消息事件 sender 可能是精简的,实测可能不含 Role // 字段(完整角色需调 GetGroupMemberInfo 查询);本结构保留 Role 以兼容 // 标准 OneBot 11 实现和需要 role 的接口返回。 type Sender struct { UserID int64 `json:"user_id"` Nickname string `json:"nickname"` Sex string `json:"sex,omitempty"` Card string `json:"card,omitempty"` Role string `json:"role,omitempty"` } type LoginInfo struct { UserID int64 `json:"user_id"` Nickname string `json:"nickname"` } type StrangerInfo struct { UserID int64 `json:"user_id"` Nickname string `json:"nickname"` Sex string `json:"sex,omitempty"` Age int64 `json:"age,omitempty"` QID string `json:"qid,omitempty"` LongNick string `json:"long_nick,omitempty"` Level int64 `json:"level,omitempty"` } type FriendInfo struct { UserID int64 `json:"user_id"` Nickname string `json:"nickname"` Remark string `json:"remark"` Sex string `json:"sex,omitempty"` Level int64 `json:"level,omitempty"` } type GroupInfo struct { GroupID int64 `json:"group_id"` GroupName string `json:"group_name"` MemberCount int64 `json:"member_count"` MaxMemberCount int64 `json:"max_member_count"` } type GroupMemberInfo struct { GroupID int64 `json:"group_id"` UserID int64 `json:"user_id"` Nickname string `json:"nickname"` Card string `json:"card"` CardChangeable bool `json:"card_changeable"` Sex string `json:"sex"` Level IntString `json:"level"` Role string `json:"role"` Title string `json:"title"` JoinTime int64 `json:"join_time"` LastSentTime int64 `json:"last_sent_time"` ShutUpTimestamp int64 `json:"shut_up_timestamp"` Unfriendly bool `json:"unfriendly"` } type Status struct { Online bool `json:"online"` Good bool `json:"good"` } type VersionInfo struct { AppName string `json:"app_name"` AppVersion string `json:"app_version"` ProtocolVersion string `json:"protocol_version"` } // MessageInfo 是 get_msg 等 API 返回的单条消息。 type MessageInfo struct { MessageID int64 `json:"message_id"` RealID int64 `json:"real_id"` MessageSeq int64 `json:"message_seq,omitempty"` Time int64 `json:"time"` Sender Sender `json:"sender"` Message json.RawMessage `json:"message"` RawMessage string `json:"raw_message"` Font int64 `json:"font,omitempty"` GroupID int64 `json:"group_id,omitempty"` } // Segments 将 message 字段解码为消息段数组。 func (m MessageInfo) Segments() ([]Segment, error) { return DecodeMessage(m.Message) } // HistoryMessage 是历史消息接口(get_group_msg_history 等)返回的单条消息。 type HistoryMessage struct { MessageID int64 `json:"message_id"` MessageSeq int64 `json:"message_seq"` Time int64 `json:"time"` Sender Sender `json:"sender"` Message json.RawMessage `json:"message"` RawMessage string `json:"raw_message"` GroupID int64 `json:"group_id,omitempty"` } func (m HistoryMessage) Segments() ([]Segment, error) { return DecodeMessage(m.Message) } // HistoryMessages 兼容 data 为 {"messages": [...]} 或直接数组两种返回形态。 type HistoryMessages []HistoryMessage func (h *HistoryMessages) UnmarshalJSON(b []byte) error { var probe struct { Messages []HistoryMessage `json:"messages"` } if err := json.Unmarshal(b, &probe); err == nil && probe.Messages != nil { *h = probe.Messages return nil } var arr []HistoryMessage if err := json.Unmarshal(b, &arr); err == nil { *h = arr return nil } return fmt.Errorf("snowluma: unexpected history message payload: %s", truncate(b)) } // ForwardMessage 是合并转发中的一条消息。 type ForwardMessage struct { MessageID int64 `json:"message_id"` Time int64 `json:"time"` Sender Sender `json:"sender"` Message json.RawMessage `json:"message"` Content json.RawMessage `json:"content,omitempty"` RawMessage string `json:"raw_message"` } // Segments 将消息体解码为消息段数组,优先取 message 字段,其次 content。 func (m ForwardMessage) Segments() ([]Segment, error) { if len(m.Message) > 0 { return DecodeMessage(m.Message) } return DecodeMessage(m.Content) } // ForwardMessages 兼容 data 为 {"messages": [...]} 或直接数组两种返回形态。 type ForwardMessages []ForwardMessage func (f *ForwardMessages) UnmarshalJSON(b []byte) error { var probe struct { Messages []ForwardMessage `json:"messages"` } if err := json.Unmarshal(b, &probe); err == nil && probe.Messages != nil { *f = probe.Messages return nil } var arr []ForwardMessage if err := json.Unmarshal(b, &arr); err == nil { *f = arr return nil } return fmt.Errorf("snowluma: unexpected forward message payload: %s", truncate(b)) } // SendForwardResult 是发送合并转发消息的返回。 type SendForwardResult struct { MessageID IntString `json:"message_id"` ResID string `json:"res_id"` } type EssenceMessage struct { GroupID int64 `json:"group_id"` SenderID int64 `json:"sender_id"` OperatorID int64 `json:"operator_id"` Time int64 `json:"time"` MessageID int64 `json:"message_id"` Message json.RawMessage `json:"message,omitempty"` RawMessage string `json:"raw_message,omitempty"` } type GroupFile struct { FileID string `json:"file_id"` FileName string `json:"file_name"` BusID int64 `json:"busid"` FileSize int64 `json:"file_size"` UploadTime int64 `json:"upload_time"` DeadTime int64 `json:"dead_time"` ModifyTime int64 `json:"modify_time"` DownloadTimes int64 `json:"download_times"` Uploader string `json:"uploader,omitempty"` UploaderID int64 `json:"uploader_id,omitempty"` } type GroupFolder struct { FolderID string `json:"folder_id"` FolderName string `json:"folder_name"` CreateTime int64 `json:"create_time"` Creator string `json:"creator,omitempty"` CreatorID int64 `json:"creator_id,omitempty"` TotalFileCount int64 `json:"total_file_count"` } type FileList struct { Files []GroupFile `json:"files"` Folders []GroupFolder `json:"folders"` } type FileSystemInfo struct { FileCount int64 `json:"file_count"` LimitCount int64 `json:"limit_count"` UsedSpace int64 `json:"used_space"` TotalSpace int64 `json:"total_space"` } type ImageResult struct { File string `json:"file"` Filename string `json:"filename,omitempty"` Size int64 `json:"size,omitempty"` URL string `json:"url,omitempty"` } type RecordResult struct { File string `json:"file"` URL string `json:"url,omitempty"` } type DownloadedFile struct { File string `json:"file"` URL string `json:"url,omitempty"` FileName string `json:"file_name,omitempty"` FileSize int64 `json:"file_size,omitempty"` } type Cookies struct { Cookies string `json:"cookies"` } type CSRFToken struct { Token string `json:"token"` } type Credentials struct { Cookies string `json:"cookies"` CSRFToken string `json:"csrf_token"` } type URLSafety struct { Level int `json:"level"` Message string `json:"message,omitempty"` } type AtAllRemain struct { CanAtAll bool `json:"can_at_all"` RemainAtAllCount int64 `json:"remain_at_all_count"` RemainAtAllCountForGroup int64 `json:"remain_at_all_count_for_group"` RemainAtAllCountForUser int64 `json:"remain_at_all_count_for_user"` } type RobotRange struct { MinUin int64 `json:"min_uin"` MaxUin int64 `json:"max_uin"` } // RobotUinRanges 兼容 data 为 {"robot_uin_range": [...]} 或直接数组两种形态。 type RobotUinRanges []RobotRange func (r *RobotUinRanges) UnmarshalJSON(b []byte) error { var probe struct { RobotUinRange []RobotRange `json:"robot_uin_range"` } if err := json.Unmarshal(b, &probe); err == nil && probe.RobotUinRange != nil { *r = probe.RobotUinRange return nil } var arr []RobotRange if err := json.Unmarshal(b, &arr); err == nil { *r = arr return nil } return fmt.Errorf("snowluma: unexpected robot uin range payload: %s", truncate(b)) } type OnlineClient struct { AppID int64 `json:"app_id"` DeviceName string `json:"device_name"` DeviceKind string `json:"device_kind"` } type OnlineClients struct { Clients []OnlineClient `json:"clients"` Online bool `json:"online"` } type MiniAppArk struct { Ark string `json:"ark"` } func truncate(b []byte) string { s := string(b) if len(s) > 128 { s = s[:128] + "..." } return s }