How to Extend OpenIMSDK
- If you need to develop new features based on OpenIMSDK, first determine whether they target the business side or the core IM logic.
- Since OpenIMSDK already provides a high level of abstraction with most chat features built in, directly modifying OpenIMSDK is not recommended.
- If you need to extend IM capabilities, you can follow the process below and submit a PR to maintain future code consistency.
Server
OpenIMServer is primarily divided into long-connection and short-connection interfaces. Long-connection interfaces handle core IM message logic (entry point at
/internal/msggateway), while short-connection interfaces handle IM business logic (entry point at/internal/api/). Below is a detailed guide on how to add new business features to IM.The following example demonstrates how to add a complete API from api => rpc => storage layer.
1. Prerequisites
- Environment Setup
- Set up Go environment, recommended Go version >= 1.22, see Go Official Documentation
- Set up gRPC environment, recommended
proto-gen-go>=1.36.1,protoc-gen-go-grpc>= 1.5.1, see gRPC Official Documentation - Set up proto environment, recommended
protoc>= 5.29.2, place the binary in your PATH, see Proto Official Documentation
-
Fork the external protocol repository that OpenIMServer depends on
- Clone the official protocol repository: github.com/openimsdk/protocol
Note: IMServer uses protobuf protocols as a dependency from
github.com/openimsdk/protocol. If you need to modify protocols, first fork the protocol repository, then add new interface protocols in the forked repo, then reference the new package path in OpenIMServer'sgo.modvia:replace github.com/openimsdk/protocol => github.com/<your-username>/protocolWhere
your_protocol_pathis the path of your forked protocol repository.
2. Protobuf Protocol Additions & Code Generation
Writing Proto Files
- First, define a new feature based on business requirements. This example demonstrates adding an
AddFriendCategoryto the Friend module. We need to add the corresponding definition in the Friend module's proto file atrelation/relation.proto. - Write the proto file, defining the new
AddFriendCategoryRPC method:
syntax = "proto3";
package openim.relation;
option go_package = "github.com/openimsdk/protocol/relation";
// Define AddFriendCategory request parameters
message AddFriendCategoryReq {
string ownerUserID = 1;
string friendUserID = 2;
int category = 3;
}
// Define AddFriendCategory response parameters
// If no return parameters are needed, this can be left empty. Error codes and messages are defined by default.
message AddFriendCategoryResp {
}
// Define a Friend module RPC service
service Friend {
// Define an AddFriendCategory RPC method
rpc AddFriendCategory(AddFriendCategoryReq) returns (AddFriendCategoryResp);
}
This defines a request parameter AddFriendCategoryReq, a response parameter AddFriendCategoryResp, and an RPC service Friend containing the new AddFriendCategory method.
Generating Go Code
After writing the proto file, generate the corresponding Go pb code:
- Install the mage command tool (OpenIM uses mage to execute various commands, avoiding cross-platform compatibility issues with shell scripts): run
go install github.com/magefile/mage@latest. - In the repository, run
mage InstallDependto install the required Go dependencies. - After editing the proto files, run
mage GenGoin the cloned protocol repository to generate the corresponding Go code. - For more details, see Generating PB Files with Mage.
Adding Validation Functions
API request parameter validation is typically generated by plugins and defined directly in proto files. OpenIM does not use this approach — instead, it adds a file in the generated pb protocol directory and implements check functions to define validation logic for each parameter. This is more intuitive and avoids reflection-based tag syntax for better performance.
For example, the AddFriendCategory interface requires the following code in relation/relation.go:
func (x *AddFriendCategoryReq) Check() error {
if x.OwnerUserID == "" {
return errors.New("OwnerUserID is empty")
}
if x.FriendUserID == "" {
return errors.New("FriendUserID is empty")
}
if x.Category == 0 {
return errors.New("Category is empty")
}
return nil
}
3. Adding API Functionality
Add new API features, including route definitions and interface implementations.
API Route Definition
- Routes are defined in
/internal/api/router.go. Add the corresponding route in thenewGinRouterfunction: For example, to define aAddFriendCategoryinterface for the Friend module, add the following innewGinRouter:
// friend routing group
{
f := NewFriendApi(relation.NewFriendClient(friendConn))
friendRouterGroup := r.Group("/friend")
friendRouterGroup.POST("/delete_friend", f.DeleteFriend)
// ......
// Add route for AddFriendCategory interface
friendRouterGroup.POST("/add_friend_category", f.AddFriendCategory)
}
If the new interface belongs to an existing route group, add it directly. Otherwise, create a new route group file following existing patterns.
API Interface Implementation
Based on the route definition above, add the corresponding interface implementation in /internal/api/friend/friend.go.
If the API's JSON request matches the RPC Request, you can call a2r.Call directly. Otherwise, parse the JSON request manually and call the gRPC interface (refer to the Message module's SendMessage interface).
For example:
// When the API Request matches the JSON request
func (o *FriendApi) AddFriendCategory(c *gin.Context) {
// AddFriendCategory is the method defined in the RPC
a2r.Call(c,relation.FriendClient.AddFriendCategory, o.client)
}
4. Adding RPC Methods
Add new gRPC methods to the corresponding module's Server struct to implement the Server interface. Then write the core business logic.
DB update/insert operations that require real-time SDK notifications can follow the pattern of s.notificationSender.FriendsInfoUpdateNotification (the SDK needs to handle the new notification type accordingly).
Adding New RPC Methods
Add the new AddFriendCategory rpc method in internal/rpc/relation/friend/friend.go with the core business logic:
// AddFriendCategory adds a friend category
func (s *friendServer) AddFriendCategory(ctx context.Context, req *relation.AddFriendCategoryReq) (*relation.AddFriendCategoryResp, error) {
// Implement business logic
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
return nil, err
}
_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})
if err != nil {
return nil, err
}
// Call DB operation
if err := s.db.AddFriendCategory(ctx,req.OwnerUserID, req.FriendUserID,req.category); err != nil {
return nil, err
}
// Send SDK notification (for corresponding DB operations)
s.notification.FriendCategoryAddNotification(ctx, req.OwnerUserID, req.FriendUserID) // Example only — implement notification function based on business requirements
return &relation.AddFriendCategoryResp{}, nil
}
The corresponding notification function FriendCategoryAddNotification should be implemented in internal/rpc/relation/notification.go:
func (f *FriendNotificationSender) FriendCategoryAddNotification(ctx context.Context,fromUserID, toUserID string) {
tips := sdkws.FriendInfoChangedTips{FromToUserID: &sdkws.FromToUserID{}}
tips.FromToUserID.FromUserID = fromUserID
tips.FromToUserID.ToUserID = toUserID
f.setSortVersion(ctx, &tips.FriendVersion, &tips.FriendVersionID, database.FriendVersionName, toUserID, &tips.FriendSortVersion)
f.Notification(ctx, fromUserID, toUserID, constant.FriendCategoryAddNotification, &tips)
}
The constant.FriendCategoryAddNotification constant should be defined in constant/constant.go in the protocol repository:
const(
FriendApplicationApprovedNotification = 1201 // add_friend_response
// ...
// Add FriendCategoryAddNotification constant
FriendCategoryAddNotification = 1211
)
Also update the corresponding fields in sdkws/sdkws.proto and regenerate the sdkws/sdkws.pb.go file:
message FriendInfo {
string ownerUserID = 1;
string remark = 2;
// ...
// Add Category field
int32 category = 9;
}
5. Adding Storage Layer Interfaces
The storage layer is divided into three layers:
- Controller layer: Business logic control layer responsible for database transaction management and cache integration. It coordinates database operations and caching to ensure data consistency and correct transaction processing.
- Cache layer: Caches frequently accessed data to reduce database queries and improve performance. Uses caching strategies and effective update mechanisms to ensure efficient data access.
- Database layer: Handles persistent storage for business data. Ensures data durability and consistency through database management systems (relational or NoSQL).
Adding Controller Layer Interface
In pkg/common/storage/controller, add a new interface and implement it for the RPC logic layer to call.
For the AddFriendCategory interface, add the following in pkg/common/storage/controller/friend.go:
type FriendDatabase interface {
CheckIn(ctx context.Context, user1, user2 string) (inUser1Friends bool, inUser2Friends bool, err error)
// ...
// Define Controller layer AddFriendCategory interface
AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error
}
// Implement AddFriendCategory interface
func (f *FriendDatabase) AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error {
// Implement business logic, data transformation, etc.
if err := f.friend.AddFriendCategory(ctx, ownerUserID, friendUserID, category); err != nil {
return err
}
return f.cache.DeleteFriend(ownerUserID, friendUserID).DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx)
}
Adding Cache Layer Interface
Add new interfaces in pkg/common/storage/cache, implement the corresponding keys in pkg/common/storage/cache/cachekey, and implement the interfaces for the controller layer to call.
For the AddFriendCategory interface, we can directly use the existing DeleteFriend cache interface.
Note: The cache layer typically deletes cache on writes and updates cache on reads — a "delete on write, update on read" strategy.
Adding Database Layer Interface
In pkg/common/storage/model, define the corresponding database model struct. Then in pkg/common/storage/database, add new interfaces and implementations for the cache layer to integrate.
For the AddFriendCategory interface, add the corresponding field to the model struct in pkg/common/storage/model/friend.go, add the interface in pkg/common/storage/database/friend.go, and implement the database operations in pkg/common/storage/database/mgo/friend.go.
model/friend.go
type Friend struct {
ID primitive.ObjectID `bson:"_id"`
OwnerUserID string `bson:"owner_user_id"`
// ...
Category int `bson:"category"` // Add Category field
}
database/friend.go
type Friend interface {
UpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) (err error)
// ...
// Define DB layer AddFriendCategory interface
AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error
}
database/mgo/friend.go
func (f *FriendMgo) AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error{
return f.UpdateByMap(ctx, ownerUserID, friendUserID, map[string]any{"category": category})
}
Client
The core of the client is the cross-platform layer OpenIM SDK Core, which is fully responsible for the IM system's core functionality, including but not limited to:
- Network Connection Management: Implements stable, efficient WebSocket long-lived connections with the server, ensuring real-time data transmission and smooth communication.
- Message Forwarding & Storage: Handles receiving, forwarding, and storing instant messages between users, ensuring accurate and timely delivery along with persistent data management.
- Contact & Group Management: Provides flexible contact management and group management features, supporting operations such as adding/removing contacts, creating and managing groups — providing complete social interaction infrastructure.
- Cross-Platform Support: The SDK runs seamlessly on multiple platforms (Android, iOS, Windows, Mac, etc.), ensuring consistent behavior and user experience across devices and operating systems.
1. Define Server API Interface
If the new method needs to call server-side interfaces, define the corresponding interface method in server_api.
For the AddFriendCategory interface, add the following:
- Define the Server API call variable in
pkg/api/api.go:
relation.AddFriendCategoryReqis the protocol defined in the server's proto files, which sdk-core also needs to reference
// relation
var(
AddFriend = newApi[relation.ApplyToAddFriendReq, relation.ApplyToAddFriendResp]("/friend/add_friend")
// ...
// Define AddFriendCategory interface
AddFriendCategory = newApi[relation.AddFriendCategoryReq, relation.AddFriendCategoryResp]("/friend/add_friend_category")
)
- Add the corresponding server_api caller in
relation/server_api.go:
func (r *Relation) AddFriendCategory(ctx context.Context, req *relation.AddFriendCategoryReq) error {
// Implement logic and data transformation
req.OwnerUserID = r.loginUserID
return api.AddFriendCategory.Execute(ctx, req)
}
2. Implement SDK Interface Business Logic
Implement the corresponding logic method in internal/relation/api.go:
The
req *sdkpb.AddFriendCategoryReqand*sdkpb.AddFriendCategoryRespstructs can use pb structs or custom structs with json tags
func (r *Relation) AddFriendCategory(ctx context.Context, req *sdkpb.AddFriendCategoryReq) (*sdkpb.AddFriendCategoryResp, error) {
// Call the Server API interface
sReq:= &relation.AddFriendCategoryReq{ OwnerUserID: r.loginUserID, FriendUserID: req.friendUserID, Category: req.Category}
if err := r.AddFriendCategory(ctx,sReq) ; err != nil {
return nil, err
}
r.relationSyncMutex.Lock()
defer r.relationSyncMutex.Unlock()
if err := r.IncrSyncFriends(ctx); err != nil {
return nil, err
}
return &sdkpb.AddFriendCategoryResp, nil
}
3. Handle IMServer Notifications
Process notifications sent by IMServer by implementing the corresponding notification handler in internal/relation/notification.go.
For the FriendCategoryAddNotification, add the following in internal/relation/notification.go:
func (r *Relation) doNotification(ctx context.Context, msg *sdkws.MsgData) error {
r.relationSyncMutex.Lock()
defer r.relationSyncMutex.Unlock()
switch msg.ContentType {
case constant.FriendRemarkSetNotification:
// ...
// Add corresponding notification handler
case constant.FriendCategoryAddNotification:
var tips sdkws.FriendCategoryAddTips // Parse the server-defined notification struct
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == r.loginUserID {
// Execute incremental sync logic
return r.IncrSyncFriends(ctx)
}
}
}
}
The IncrSyncFriends method needs to write to the local DB, so update the conversion function:
Update the ServerFriendToLocalFriend function in internal/relation/conversion.go:
func ServerFriendToLocalFriend(info *sdkws.FriendInfo) *model_struct.LocalFriend {
return &model_struct.LocalFriend{
OwnerUserID: info.OwnerUserID,
FriendUserID: info.FriendUser.UserID,
Remark: info.Remark,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.FriendUser.Nickname,
FaceURL: info.FriendUser.FaceURL,
Ex: info.Ex,
IsPinned: info.IsPinned,
// Add Category field
Category: info.Category,
}
}
4. Handle Local DB Layer
If DB operations are involved, call the db layer interface to update local db data.
- Add the interface method in
pkg/db/db_interface/databse.gofor the SDK to call.
Here we use the existing UpdateFriend method.
- Update the
LocalFriendstruct inpkg/db/model_struct/data_model_struct.go:
Add the corresponding field to the LocalFriend struct:
type LocalFriend struct {
OwnerUserID string `gorm:"column:owner_user_id;primary_key;type:varchar(64)" json:"ownerUserID"`
FriendUserID string `gorm:"column:friend_user_id;primary_key;type:varchar(64)" json:"userID"`
Remark string `gorm:"column:remark;type:varchar(255)" json:"remark"`
// ...
// Add Category field
Category int32 `gorm:"column:category" json:"category"`
}
- Add the concrete implementation in
pkg/db/friend_model.go.
Here we use the existing UpdateFriend method.
5. Export Layer — Provide Interfaces for Other Languages
OpenIM SDK Core is developed in Go. After completing the logic in Step 2, export the corresponding interfaces for other languages to call:
Using Android and iOS as examples, the export layer interfaces are located at:
open_im_sdk/(function interface layer)open_im_sdk_callback/(callback definition layer, using callbacks for communication)
Then use gomobile to compile into Android AAR and iOS xcframework library files. For details on how OpenIM SDK Core uses gomobile to build into Android AAR and iOS xcframework, refer to the repository documentation.
The above example defines the interface in open_im_sdk/relation.go to provide to Android/iOS:
func AddFriendCategory(callback open_im_sdk_callback.Base, operationID string, req string){
call(callback, operationID, UserForSDK.Relation().AddFriendCategory, req)
}
Note: OpenIM SDK Core provides interfaces to Android/iOS via gomobile, and also provides C language interfaces for cross-platform calls from additional languages. The implementation is in OpenIM SDK CPP. If you need to call from other languages via C interfaces, refer to that repository's SDK encapsulation.