Skip to content

Commit 0fcc858

Browse files
committed
feat: add AgentsController for agent management and integrate Micronaut OpenAPI support
1 parent e619cbb commit 0fcc858

File tree

7 files changed

+265
-2
lines changed

7 files changed

+265
-2
lines changed

demo/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ repositories {
99
}
1010

1111
dependencies {
12+
// Add Micronaut Platform BOM
13+
implementation(platform("io.micronaut.platform:micronaut-platform:${micronautVersion}"))
14+
1215
annotationProcessor("io.micronaut:micronaut-http-validation")
1316
annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
1417
annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
@@ -53,6 +56,9 @@ dependencies {
5356
// Add snakeyaml for YAML configuration parsing
5457
runtimeOnly("org.yaml:snakeyaml")
5558

59+
// Add Micronaut OpenAPI and Swagger UI support
60+
implementation("io.micronaut.openapi:micronaut-openapi")
61+
5662
testImplementation("io.micronaut:micronaut-http-client")
5763
testImplementation("org.mockito:mockito-core:5.15.2")
5864

@@ -71,6 +77,9 @@ java {
7177

7278
tasks.withType(JavaCompile) {
7379
options.encoding = "UTF-8"
80+
options.compilerArgs += [
81+
"-Amicronaut.openapi.views.spec=swagger-ui.enabled=true"
82+
]
7483
options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"]
7584
}
7685

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package com.tcn.exile.demo.single;
2+
3+
4+
import com.google.protobuf.StringValue;
5+
import com.tcn.exile.gateclients.UnconfiguredException;
6+
import com.tcn.exile.gateclients.v2.GateClient;
7+
import com.tcn.exile.models.*;
8+
import io.micronaut.http.MediaType;
9+
import io.micronaut.http.annotation.*;
10+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.inject.Inject;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import tcnapi.exile.gate.v2.Entities;
16+
import tcnapi.exile.gate.v2.Public;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
@Controller("/api/agents")
22+
@OpenAPIDefinition(tags = {@Tag(name = "agents")})
23+
public class AgentsController {
24+
private final static Logger log = LoggerFactory.getLogger(AgentsController.class);
25+
@Inject
26+
ConfigChangeWatcher configChangeWatcher;
27+
28+
29+
@Get
30+
@Tag(name = "agents")
31+
public List<Agent> listAgents() throws UnconfiguredException {
32+
log.debug("listAgents");
33+
var ret = configChangeWatcher.getGateClient().listAgents(Public.ListAgentsRequest.newBuilder().build());
34+
List<Agent> agents = new java.util.ArrayList<Agent>();
35+
while (ret.hasNext()) {
36+
var agent = ret.next();
37+
agents.add(new Agent(
38+
agent.getAgent().getUserId(),
39+
agent.getAgent().getPartnerAgentId(),
40+
agent.getAgent().getUsername(),
41+
agent.getAgent().getFirstName(),
42+
agent.getAgent().getLastName()
43+
));
44+
}
45+
46+
return agents;
47+
}
48+
49+
@Post
50+
@Consumes(MediaType.APPLICATION_JSON)
51+
@Tag(name = "agents")
52+
public Agent createAgent(@Body AgentUpsertRequest agent) throws UnconfiguredException {
53+
log.debug("createAgent");
54+
// find
55+
var req = Public.UpsertAgentRequest.
56+
newBuilder().
57+
setUsername(agent.username());
58+
59+
if (agent.firstName() != null) {
60+
req.setFirstName(agent.firstName());
61+
}
62+
if (agent.lastName() != null) {
63+
req.setLastName(agent.lastName());
64+
}
65+
if (agent.partnerAgentId() != null) {
66+
req.setPartnerAgentId(agent.partnerAgentId());
67+
}
68+
if (agent.password() != null) {
69+
req.setPassword(agent.password());
70+
}
71+
var ret = configChangeWatcher.getGateClient().upsertAgent(req.build());
72+
if (ret != null) {
73+
return new Agent(
74+
ret.getAgent().getUserId(),
75+
ret.getAgent().getPartnerAgentId(),
76+
ret.getAgent().getUsername(),
77+
ret.getAgent().getFirstName(),
78+
ret.getAgent().getLastName());
79+
}
80+
throw new RuntimeException("Failed to create agent");
81+
}
82+
83+
@Put("{partnerAgentId}/dial")
84+
@Tag(name = "agents")
85+
public DialResponse dial(@PathVariable String partnerAgentId, @Body DialRequest req) throws UnconfiguredException {
86+
log.debug("dial {}", req);
87+
88+
var dialReq = Public.DialRequest.newBuilder()
89+
.setPartnerAgentId(partnerAgentId)
90+
.setPhoneNumber(req.phoneNumber());
91+
if (req.callerId() != null) {
92+
dialReq.setCallerId(StringValue.of(req.callerId()));
93+
}
94+
if (req.poolId() != null) {
95+
dialReq.setPoolId(StringValue.of(req.poolId()));
96+
}
97+
if (req.recordId() != null) {
98+
dialReq.setRecordId(StringValue.of(req.recordId()));
99+
}
100+
101+
var res = configChangeWatcher.getGateClient().dial(dialReq.build());
102+
if (res != null) {
103+
return new DialResponse(
104+
res.getPhoneNumber(),
105+
res.getCallerId(),
106+
res.getCallSid(),
107+
CallType.valueOf(res.getCallType().name()),
108+
res.getOrgId(),
109+
res.getPartnerAgentId()
110+
);
111+
}
112+
throw new RuntimeException("Failed to dial");
113+
}
114+
115+
@Get("{partnerAgentId}/recording")
116+
@Tag(name = "agents")
117+
public RecordingResponse getRecording(@PathVariable String partnerAgentId) throws UnconfiguredException {
118+
log.debug("getRecording");
119+
var res = configChangeWatcher.getGateClient().getRecordingStatus(Public.GetRecordingStatusRequest.newBuilder().
120+
setPartnerAgentId(partnerAgentId).build());
121+
return new RecordingResponse(res.getIsRecording());
122+
}
123+
124+
@Put("{partnerAgentId}/recording/{status}")
125+
@Tag(name = "agents")
126+
public RecordingResponse setRecording(@PathVariable String partnerAgentId, @PathVariable String status) throws UnconfiguredException {
127+
log.debug("setRecording");
128+
boolean res = false;
129+
if (status.equalsIgnoreCase("on")
130+
|| status.equalsIgnoreCase("resume")
131+
|| status.equalsIgnoreCase("start")
132+
|| status.equalsIgnoreCase("true")) {
133+
configChangeWatcher.getGateClient().startCallRecording(Public.StartCallRecordingRequest.newBuilder().setPartnerAgentId(partnerAgentId).build());
134+
return new RecordingResponse(true);
135+
} else if (status.equalsIgnoreCase("off")
136+
|| status.equalsIgnoreCase("stop")
137+
|| status.equalsIgnoreCase("pause")
138+
|| status.equalsIgnoreCase("paused")
139+
|| status.equalsIgnoreCase("false")) {
140+
configChangeWatcher.getGateClient().stopCallRecording(Public.StopCallRecordingRequest.newBuilder().setPartnerAgentId(partnerAgentId).build());
141+
return new RecordingResponse(false);
142+
}
143+
throw new RuntimeException("Invalid status");
144+
}
145+
146+
147+
@Get("{partnerAgentId}/state")
148+
@Tag(name = "agents")
149+
public AgentStatus getState(@PathVariable String partnerAgentId) throws UnconfiguredException {
150+
log.debug("getState");
151+
var res = configChangeWatcher.getGateClient().getAgentStatus(Public.GetAgentStatusRequest.newBuilder().setPartnerAgentId(partnerAgentId).build());
152+
if (res.hasConnectedParty()) {
153+
return new AgentStatus(
154+
res.getPartnerAgentId(),
155+
AgentState.values()[res.getAgentState().getNumber()],
156+
res.getCurrentSessionId(),
157+
new ConnectedParty(
158+
res.getConnectedParty().getCallSid(),
159+
CallType.values()[res.getConnectedParty().getCallType().getNumber()],
160+
res.getConnectedParty().getIsInbound()
161+
)
162+
);
163+
} else {
164+
return new AgentStatus(
165+
res.getPartnerAgentId(),
166+
AgentState.values()[res.getAgentState().getNumber()],
167+
res.getCurrentSessionId(),
168+
null
169+
);
170+
171+
}
172+
}
173+
174+
@Put("{partnerAgentId}/state/{state}")
175+
@Tag(name = "agents")
176+
public SetAgentStatusResponse setState(@PathVariable String partnerAgentId, @PathVariable SetAgentState state/*, @Body PauseCodeReason pauseCodeReason*/) throws UnconfiguredException {
177+
log.debug("setState");
178+
var request = Public.UpdateAgentStatusRequest.newBuilder()
179+
.setPartnerAgentId(partnerAgentId)
180+
.setNewState(Entities.AgentState.values()[state.getValue()]);
181+
// if (pauseCodeReason != null && pauseCodeReason.reason() != null) {
182+
// request.setReason(pauseCodeReason.reason());
183+
// // request.setPauseCodeReason(pauseCodeReason.reason());
184+
// }
185+
var res = configChangeWatcher.getGateClient().updateAgentStatus(request.build());
186+
return new SetAgentStatusResponse();
187+
}
188+
189+
@Get("{partnerAgentId}/pausecodes")
190+
@Tag(name = "agents")
191+
public List<String> listPauseCodes(@PathVariable String partnerAgentId) {
192+
log.debug("listPauseCodes");
193+
var res = configChangeWatcher.getGateClient().listHuntGroupPauseCodes(Public.ListHuntGroupPauseCodesRequest.newBuilder()
194+
.setPartnerAgentId(partnerAgentId)
195+
.build());
196+
return res.getPauseCodesList().stream().toList();
197+
}
198+
199+
200+
@Get("{partnerAgentId}/simplehold")
201+
@Tag(name = "agents")
202+
public Map<String, Object> putCallOnSimpleHold(@PathVariable String partnerAgentId) {
203+
var res = configChangeWatcher.getGateClient().putCallOnSimpleHold(Public.PutCallOnSimpleHoldRequest.newBuilder()
204+
.setPartnerAgentId(partnerAgentId)
205+
.build());
206+
return Map.of("success", true);
207+
208+
}
209+
210+
@Get("{partnerAgentId}/simpleunhold")
211+
@Tag(name = "agents")
212+
public Map<String, Object> removeCallFromSimpleHold(@PathVariable String partnerAgentId) {
213+
var res = configChangeWatcher.getGateClient().takeCallOffSimpleHold(Public.TakeCallOffSimpleHoldRequest.newBuilder()
214+
.setPartnerAgentId(partnerAgentId)
215+
.build());
216+
return Map.of("success", true);
217+
}
218+
219+
@Put("{partnerAgentId}/callresponse")
220+
@Tag(name = "agents")
221+
public Map<String, Object> addAgentCallResponse(@PathVariable String partnerAgentId, @Body Public.AddAgentCallResponseRequest req) {
222+
var res = configChangeWatcher.getGateClient().addAgentCallResponse(req.toBuilder().setPartnerAgentId(partnerAgentId).build());
223+
return Map.of("success", true);
224+
}
225+
}

demo/src/main/java/com/tcn/exile/demo/single/Application.java

+9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
package com.tcn.exile.demo.single;
22

33
import io.micronaut.runtime.Micronaut;
4+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
5+
import io.swagger.v3.oas.annotations.info.Info;
46

7+
@OpenAPIDefinition(
8+
info = @Info(
9+
title = "Sati Demo API",
10+
version = "1.0",
11+
description = "Demo API for Sati with Swagger UI"
12+
)
13+
)
514
public class Application {
615
public static void main(String[] args) {
716
Micronaut.run(Application.class, args);

demo/src/main/java/com/tcn/exile/demo/single/ConfigChangeWatcher.java

+17
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import com.tcn.exile.gateclients.v2.GateClientConfiguration;
2222
import com.tcn.exile.gateclients.v2.GateClientJobStream;
2323
import com.tcn.exile.gateclients.v2.GateClientPollEvents;
24+
import com.tcn.exile.plugin.PluginInterface;
25+
2426
import io.methvin.watcher.DirectoryWatcher;
2527
import io.micronaut.context.ApplicationContext;
2628
import io.micronaut.context.event.ApplicationEventListener;
@@ -167,11 +169,13 @@ public ConfigChangeWatcher() throws IOException {
167169
private final static String gateClientJobStreamPrefix = "gate-client-job-stream-";
168170
private final static String gateClientPollEventsPrefix = "gate-client-poll-events-";
169171
private final static String gateClientConfigurationPrefix = "gate-client-config-";
172+
private final static String pluginPrefix = "plugin-";
170173
private void destroyBeans() {
171174
context.destroyBean(Argument.of(GateClient.class), Qualifiers.byName(gateClientPrefix + this.currentConfig.getOrg()));
172175
context.destroyBean(Argument.of(GateClientJobStream.class), Qualifiers.byName(gateClientJobStreamPrefix + this.currentConfig.getOrg()));
173176
context.destroyBean(Argument.of(GateClientPollEvents.class), Qualifiers.byName(gateClientPollEventsPrefix + this.currentConfig.getOrg()));
174177
context.destroyBean(Argument.of(GateClientConfiguration.class), Qualifiers.byName(gateClientConfigurationPrefix + this.currentConfig.getOrg()));
178+
context.destroyBean(Argument.of(PluginInterface.class), Qualifiers.byName(pluginPrefix + this.currentConfig.getOrg()));
175179
}
176180

177181
private void createBeans() {
@@ -188,6 +192,7 @@ private void createBeans() {
188192
context.registerSingleton(GateClientJobStream.class, gateClientJobStream,Qualifiers.byName(gateClientJobStreamPrefix + this.currentConfig.getOrg()), true);
189193
context.registerSingleton(GateClientPollEvents.class, gateClientPollEvents, Qualifiers.byName(gateClientPollEventsPrefix + this.currentConfig.getOrg()), true);
190194
context.registerSingleton(GateClientConfiguration.class, gateClientConfiguration, Qualifiers.byName(gateClientConfigurationPrefix + this.currentConfig.getOrg()), true);
195+
context.registerSingleton(PluginInterface.class, demoPlugin, Qualifiers.byName(pluginPrefix + this.currentConfig.getOrg()), true);
191196
TaskScheduler taskScheduler = context.getBean(TaskScheduler.class);
192197
taskScheduler.scheduleAtFixedRate(Duration.ZERO, Duration.ofSeconds(1), gateClientJobStream::start);
193198
taskScheduler.scheduleAtFixedRate(Duration.ZERO, Duration.ofSeconds(10), gateClientPollEvents::start);
@@ -205,4 +210,16 @@ public void onApplicationEvent(StartupEvent event) {
205210
});
206211
this.watcher.watchAsync();
207212
}
213+
214+
public GateClient getGateClient() {
215+
if (this.currentConfig == null) {
216+
throw new IllegalStateException("No current config loaded");
217+
}
218+
String beanName = gateClientPrefix + this.currentConfig.getOrg();
219+
return context.getBean(GateClient.class, Qualifiers.byName(beanName));
220+
}
221+
222+
public PluginInterface getPlugin() {
223+
return context.getBean(PluginInterface.class, Qualifiers.byName(pluginPrefix + this.currentConfig.getOrg()));
224+
}
208225
}

demo/src/main/java/com/tcn/exile/demo/single/DemoPlugin.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.tcn.exile.demo.single;
22

33
import ch.qos.logback.classic.LoggerContext;
4+
import jakarta.inject.Singleton;
5+
46
import com.tcn.exile.gateclients.UnconfiguredException;
57
import com.tcn.exile.gateclients.v2.GateClient;
68
import com.tcn.exile.memlogger.LogShipper;

demo/src/main/java/com/tcn/exile/demo/single/VersionController.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
@Controller("/version")
99
public class VersionController {
1010
@Inject
11-
PluginInterface plugin;
11+
ConfigChangeWatcher configChangeWatcher;
1212

1313
@Get
1414
public VersionInfo index() {
15-
var ver = plugin.info();
15+
var ver = configChangeWatcher.getPlugin().info();
1616
return new VersionInfo(
1717
ver.getCoreVersion(),
1818
ver.getServerName(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
swagger-ui.enabled=true

0 commit comments

Comments
 (0)