Understanding OpenIM API to RPC Call Logic: A Comprehensive Guide

Introduction

This document provides an in-depth analysis of the API to RPC (Remote Procedure Call) call logic within the OpenIM framework, focusing on the SearchMsg functionality. We will explore the codebase, tracing the flow from the API endpoint definition to the final RPC method invocation.

Overview of OpenIM's Structure

Before diving into the specific API, it's essential to understand OpenIM's general architecture. OpenIM is structured to separate concerns between its API layer, business logic, and data access layer, facilitating a scalable and maintainable codebase.

API Endpoint Definition

The journey begins in the internal/api/route.go file, where the API endpoints are defined. Here, msgGroup.POST("/search_msg", m.SearchMsg) sets up the route for the search message functionality.

msgGroup.POST("/search_msg", m.SearchMsg)

Key File: internal/api/route.go

  • Purpose: Route definition for API endpoints.
  • Key Function: msgGroup.POST
func (m *MessageApi) SearchMsg(c *gin.Context) {
	a2r.Call(msg.MsgClient.SearchMessage, m.Client, c)
}

API Function Logic

Moving to internal/api/msg.go, we find the SearchMsg function. This function acts as a bridge, translating the HTTP request into a format suitable for RPC communication.

Key File: internal/api/msg.go

  • Function: SearchMsg
  • Role: Converts HTTP requests to RPC calls.

RPC Call Abstraction

The a2r.Call function in github.com/OpenIMSDK/tools/a2r abstracts the RPC call. It handles JSON binding, request validation, and invoking the actual RPC method.

Key File: github.com/OpenIMSDK/tools/a2r

  • Function: Call
  • Responsibility: Error handling, request validation, and RPC invocation.
func Call[A, B, C any](
	rpc func(client C, ctx context.Context, req *A, options ...grpc.CallOption) (*B, error),
	client C,
	c *gin.Context,
) {
	var req A
	if err := c.BindJSON(&req); err != nil {
		log.ZWarn(c, "gin bind json error", err, "req", req)
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) // 参数错误
		return
	}
	if err := checker.Validate(&req); err != nil {
		apiresp.GinError(c, err) // 参数校验失败
		return
	}
	data, err := rpc(client, c, &req)
	if err != nil {
		apiresp.GinError(c, err) // RPC调用失败
		return
	}
	apiresp.GinSuccess(c, data) // 成功
}

RPC Method Execution

The actual RPC method, SearchMessage, is defined in internal/rpc/msg/sync_msg.go. This method handles the business logic for searching messages, interacting with the database, and constructing the response.

Key File: internal/rpc/msg/sync_msg.go

  • Function: SearchMessage
  • Operation: Business logic and database interaction.
func (m *msgServer) SearchMessage(ctx context.Context, req *msg.SearchMessageReq) (resp *msg.SearchMessageResp, err error) {
	var chatLogs []*sdkws.MsgData
	var total int32
	resp = &msg.SearchMessageResp{}
	if total, chatLogs, err = m.MsgDatabase.SearchMessage(ctx, req); err != nil {
		return nil, err
	}

	var (
		sendIDs  []string
		recvIDs  []string
		groupIDs []string
		sendMap  = make(map[string]string)
		recvMap  = make(map[string]string)
		groupMap = make(map[string]*sdkws.GroupInfo)
	)
	for _, chatLog := range chatLogs {
		if chatLog.SenderNickname == "" {
			sendIDs = append(sendIDs, chatLog.SendID)
		}
		switch chatLog.SessionType {
		case constant.SingleChatType:
			recvIDs = append(recvIDs, chatLog.RecvID)
		case constant.GroupChatType, constant.SuperGroupChatType:
			groupIDs = append(groupIDs, chatLog.GroupID)
		}
	}
	if len(sendIDs) != 0 {
		sendInfos, err := m.User.GetUsersInfo(ctx, sendIDs)
		if err != nil {
			return nil, err
		}
		for _, sendInfo := range sendInfos {
			sendMap[sendInfo.UserID] = sendInfo.Nickname
		}
	}
	if len(recvIDs) != 0 {
		recvInfos, err := m.User.GetUsersInfo(ctx, recvIDs)
		if err != nil {
			return nil, err
		}
		for _, recvInfo := range recvInfos {
			recvMap[recvInfo.UserID] = recvInfo.Nickname
		}
	}
	if len(groupIDs) != 0 {
		groupInfos, err := m.Group.GetGroupInfos(ctx, groupIDs, true)
		if err != nil {
			return nil, err
		}
		for _, groupInfo := range groupInfos {
			groupMap[groupInfo.GroupID] = groupInfo
		}
	}
	for _, chatLog := range chatLogs {
		pbchatLog := &msg.ChatLog{}
		utils.CopyStructFields(pbchatLog, chatLog)
		pbchatLog.SendTime = chatLog.SendTime
		pbchatLog.CreateTime = chatLog.CreateTime
		if chatLog.SenderNickname == "" {
			pbchatLog.SenderNickname = sendMap[chatLog.SendID]
		}
		switch chatLog.SessionType {
		case constant.SingleChatType:
			pbchatLog.RecvNickname = recvMap[chatLog.RecvID]

		case constant.GroupChatType, constant.SuperGroupChatType:
			pbchatLog.SenderFaceURL = groupMap[chatLog.GroupID].FaceURL
			pbchatLog.GroupMemberCount = groupMap[chatLog.GroupID].MemberCount
			pbchatLog.RecvID = groupMap[chatLog.GroupID].GroupID
			pbchatLog.GroupName = groupMap[chatLog.GroupID].GroupName
			pbchatLog.GroupOwner = groupMap[chatLog.GroupID].OwnerUserID
			pbchatLog.GroupType = groupMap[chatLog.GroupID].GroupType
		}
		resp.ChatLogs = append(resp.ChatLogs, pbchatLog)
	}
	resp.ChatLogsNum = total
	return resp, nil
}

Request and Response Structures

Understanding the request and response structures is crucial. The SearchMessageReq and SearchMessageResp structs define the data format for the request and response of the search message functionality.

Structures:

  • SearchMessageReq
  • SearchMessageResp

Database Interaction

Finally, pkg/common/db/controller/msg.go contains the database logic. This file handles the actual querying of messages and the preparation of data to be sent back to the client.

Key File: pkg/common/db/controller/msg.go

  • Role: Database querying and data preparation.
func (db *commonMsgDatabase) SearchMessage(ctx context.Context, req *pbmsg.SearchMessageReq) (total int32, msgData []*sdkws.MsgData, err error) {
	var totalMsgs []*sdkws.MsgData
	total, msgs, err := db.msgDocDatabase.SearchMessage(ctx, req)
	if err != nil {
		return 0, nil, err
	}
	for _, msg := range msgs {
		if msg.IsRead {
			msg.Msg.IsRead = true
		}
		totalMsgs = append(totalMsgs, convert.MsgDB2Pb(msg.Msg))
	}
	return total, totalMsgs, nil
}

func (db *commonMsgDatabase) ConvertMsgsDocLen(ctx context.Context, conversationIDs []string) {
	db.msgDocDatabase.ConvertMsgsDocLen(ctx, conversationIDs)
}

SearchMsg API Documentation

Overview

The SearchMsg API allows users to search for messages based on various criteria. This document details the API request structure, required parameters, and examples of use.

Request URL

http://x.x.x.x:10002/msg/search_msg

Header Parameters

Header NameExample ValueRequiredTypeDescription
operationID1646445464564YesstringUsed for global link tracing
tokeneyJhbxxxx3XsYesstringAdministrator's token

Request Parameters

JSON Payload

{
  "sendID": "exampleSender",
  "recvID": "exampleReceiver",
  "msgType": 101,
  "sendTime": "2023-01-01T00:00:00Z",
  "sessionType": 1,
  "pagination": {
    "pageNumber": 1,
    "showNumber": 20
  }
}

Parameters Description

Field NameRequiredTypeDescription
sendIDYesstringSender's ID
recvIDYesstringReceiver's ID
msgTypeNointType of the message
sendTimeNostringTime when the message was sent (ISO 8601 format)
sessionTypeYesintType of session (e.g., 1 for individual chat)
paginationNoobjectPagination details
pagination.pageNumberNointPage number for pagination
pagination.showNumberNointNumber of items per page

Response

Success Response

Code: 200 OK

Content Example:

{
  "status": 0,
  "data": {
    "messages": [
      {
        "id": "msg123",
        "content": "Hello world!",
        ...
      }
    ],
    "total": 150
  }
}

Error Response

Code: 400 Bad Request

Content Example:

{
  "status": 1,
  "error": "Invalid request parameters"
}

Remarks

  • The sendTime should be in ISO 8601 format for consistency.
  • Ensure that the pagination object is used correctly to manage large datasets.
  • The msgType field is optional and can be used to filter messages by type.

Conclusion

This document has traced the path from an API endpoint definition to the RPC method execution within the OpenIM framework, using the SearchMsg functionality as an example. Understanding this flow is crucial for developers working with OpenIM, as it provides insights into the framework's design and operation.