For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add RTPEngine proxy RPC methods to bin-common-handler requesthandler so call-manager can send NG protocol commands to specific rtpengine-proxy instances.
Architecture: Follow the asterisk-proxy pattern — dynamic queue names (rtpengine.<rtpengineID>.request) with a sendRequestRTPEngine() helper and a single generic RTPEngineV1CommandsSend method that accepts any NG command as map[string]interface{}.
Tech Stack: Go, RabbitMQ RPC, gomock
The voip-rtpengine-proxy service exists and handles RTPEngine NG protocol commands via RabbitMQ RPC, but there are no typed methods in bin-common-handler/pkg/requesthandler for other services (specifically bin-call-manager) to call it.
- Dynamic queue name (like asterisk-proxy), not a constant
- Generic
map[string]interface{}for request/response (NG protocol is dynamic) - Single method
RTPEngineV1CommandsSendmatching the proxy's single/v1/commandsPOST endpoint
Files:
- Modify:
bin-common-handler/pkg/requesthandler/send_request.go(append aftersendRequestAstat line ~104)
Step 1: Add the send helper
Add after the existing sendRequestAst function:
// sendRequestRTPEngine sends a request to the rtpengine-proxy and returns the response.
// The queue name is dynamically constructed from the rtpengineID: rtpengine.<rtpengineID>.request
// timeout millisecond
// delayed millisecond
func (r *requestHandler) sendRequestRTPEngine(ctx context.Context, rtpengineID, uri string, method sock.RequestMethod, resource string, timeout int, delayed int, dataType string, data []byte) (*sock.Response, error) {
// create target
target := fmt.Sprintf("rtpengine.%s.request", rtpengineID)
return r.sendRequest(ctx, commonoutline.QueueName(target), uri, method, resource, timeout, delayed, dataType, data)
}Step 2: Verify it compiles
Run: cd bin-common-handler && go build ./...
Expected: Success (no new imports needed — fmt and commonoutline already imported)
Files:
- Modify:
bin-common-handler/pkg/requesthandler/main.go(add toRequestHandlerinterface, after the Ast* methods block around line ~312)
Step 1: Add the interface method
Add in the RequestHandler interface, grouped with a comment:
// RTPEngine
RTPEngineV1CommandsSend(ctx context.Context, rtpengineID string, command map[string]interface{}) (map[string]interface{}, error)Step 2: Verify it compiles (expect failure — method not implemented yet)
Run: cd bin-common-handler && go build ./...
Expected: Build error — requestHandler does not implement RTPEngineV1CommandsSend
Files:
- Create:
bin-common-handler/pkg/requesthandler/rtpengine_commands.go
Step 1: Create the implementation file
package requesthandler
import (
"context"
"encoding/json"
"monorepo/bin-common-handler/models/sock"
)
// RTPEngineV1CommandsSend sends an RTPEngine NG protocol command to a specific rtpengine-proxy instance.
//
// The command is sent as POST /v1/commands to the rtpengine-proxy identified by rtpengineID.
// The rtpengineID determines the target queue: rtpengine.<rtpengineID>.request
//
// The only required field in command is "call-id". Fields like "from-tag", "to-tag",
// and "via-branch" are optional.
//
// Request examples:
//
// {"command": "start recording", "call-id": "abc123@sip-server"}
// {"command": "stop recording", "call-id": "abc123@sip-server"}
//
// Response examples:
//
// Success: {"result": "ok"}
// Error: {"result": "error", "error-reason": "Unknown call-id"}
func (r *requestHandler) RTPEngineV1CommandsSend(ctx context.Context, rtpengineID string, command map[string]interface{}) (map[string]interface{}, error) {
uri := "/v1/commands"
m, err := json.Marshal(command)
if err != nil {
return nil, err
}
tmp, err := r.sendRequestRTPEngine(ctx, rtpengineID, uri, sock.RequestMethodPost, "rtpengine/commands", requestTimeoutDefault, 0, ContentTypeJSON, m)
if err != nil {
return nil, err
}
var res map[string]interface{}
if errParse := parseResponse(tmp, &res); errParse != nil {
return nil, errParse
}
return res, nil
}Step 2: Verify it compiles
Run: cd bin-common-handler && go build ./...
Expected: Success
Files:
- Create:
bin-common-handler/pkg/requesthandler/rtpengine_commands_test.go
Step 1: Write the test file
package requesthandler
import (
"context"
"testing"
"go.uber.org/mock/gomock"
"monorepo/bin-common-handler/models/sock"
"monorepo/bin-common-handler/pkg/sockhandler"
)
func Test_RTPEngineV1CommandsSend(t *testing.T) {
tests := []struct {
name string
rtpengineID string
command map[string]interface{}
expectQueue string
expectURI string
expectMethod sock.RequestMethod
response *sock.Response
expectError bool
}{
{
"start recording",
"10.164.0.12",
map[string]interface{}{
"command": "start recording",
"call-id": "abc123@sip-server",
},
"rtpengine.10.164.0.12.request",
"/v1/commands",
sock.RequestMethodPost,
&sock.Response{StatusCode: 200, Data: []byte(`{"result":"ok"}`)},
false,
},
{
"stop recording",
"10.164.0.12",
map[string]interface{}{
"command": "stop recording",
"call-id": "abc123@sip-server",
},
"rtpengine.10.164.0.12.request",
"/v1/commands",
sock.RequestMethodPost,
&sock.Response{StatusCode: 200, Data: []byte(`{"result":"ok"}`)},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mc := gomock.NewController(t)
defer mc.Finish()
mockSock := sockhandler.NewMockSockHandler(mc)
reqHandler := requestHandler{
sock: mockSock,
}
mockSock.EXPECT().RequestPublish(
gomock.Any(),
tt.expectQueue,
gomock.Any(),
).Return(tt.response, nil)
res, err := reqHandler.RTPEngineV1CommandsSend(context.Background(), tt.rtpengineID, tt.command)
if tt.expectError {
if err == nil {
t.Errorf("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Wrong match. expect: ok, got: %v", err)
}
if res["result"] != "ok" {
t.Errorf("Wrong result. expect: ok, got: %v", res["result"])
}
})
}
}Step 2: Run test to verify it passes
Run: cd bin-common-handler && go test ./pkg/requesthandler/ -run Test_RTPEngineV1CommandsSend -v
Expected: PASS
Files:
- Regenerated:
bin-common-handler/pkg/requesthandler/mock_main.go
Step 1: Run full verification workflow
cd bin-common-handler && \
go mod tidy && \
go mod vendor && \
go generate ./... && \
go test ./... && \
golangci-lint run -v --timeout 5mExpected: All steps pass. mock_main.go will be updated with the new RTPEngineV1CommandsSend mock method.
Step 1: Stage and commit
git add bin-common-handler/pkg/requesthandler/send_request.go \
bin-common-handler/pkg/requesthandler/main.go \
bin-common-handler/pkg/requesthandler/rtpengine_commands.go \
bin-common-handler/pkg/requesthandler/rtpengine_commands_test.go \
bin-common-handler/pkg/requesthandler/mock_main.go \
docs/plans/2026-03-09-add-rtpengine-requesthandler-methods-design.md
git commit -m "NOJIRA-Add-rtpengine-requesthandler-methods
Add RTPEngine proxy RPC methods to bin-common-handler requesthandler,
following the asterisk-proxy pattern with dynamic queue names.
- bin-common-handler: Add sendRequestRTPEngine() helper for dynamic queue routing
- bin-common-handler: Add RTPEngineV1CommandsSend() to RequestHandler interface
- bin-common-handler: Add implementation and tests for RTPEngine command sending
- docs: Add design document for rtpengine requesthandler integration"