├── README.en.md ├── README.md ├── inbound ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── cn │ │ └── ch3nnn │ │ ├── InboundApplication.java │ │ ├── common │ │ ├── InitCacheLineDataRunner.java │ │ └── ResultCode.java │ │ ├── config │ │ └── RedisConfig.java │ │ ├── controller │ │ └── OutboundController.java │ │ ├── dto │ │ ├── LineCountParam.java │ │ ├── LineDataDto.java │ │ └── OutboundParam.java │ │ ├── esl │ │ └── EslEventListener.java │ │ ├── handle │ │ ├── ExampleInboundClientOptionHandler.java │ │ ├── HeartbeatEslEventHandler.java │ │ ├── ReScheduleEslEventHandler.java │ │ └── ServerConnectionListenerImpl.java │ │ └── utils │ │ ├── JsonUtils.java │ │ ├── RedisUtils.java │ │ └── Utils.java │ └── resources │ ├── LineTest.json │ └── application.yml └── outbound ├── pom.xml └── src └── main ├── java └── cn │ └── ch3nnn │ ├── AbstractOutboundClientEventHandler.java │ ├── OutboundApplication.java │ ├── OutboundHandler.java │ ├── PipelineFactory.java │ ├── annotation │ └── OutBoundEventName.java │ ├── config │ └── OutboundServerConfig.java │ ├── handle │ ├── ChannelAnswerOutboundEventHandler.java │ ├── OutBoundEventHandler.java │ └── PlayBackSTOPOutBoundEventHandler.java │ ├── service │ └── BridgeCallService.java │ └── utils │ └── ClassUtil.java └── resources └── test.wav /README.en.md: -------------------------------------------------------------------------------- 1 | # springboot-freeswitch 2 | 3 | #### Description 4 | {**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} 5 | 6 | #### Software Architecture 7 | Software architecture description 8 | 9 | #### Installation 10 | 11 | 1. xxxx 12 | 2. xxxx 13 | 3. xxxx 14 | 15 | #### Instructions 16 | 17 | 1. xxxx 18 | 2. xxxx 19 | 3. xxxx 20 | 21 | #### Contribution 22 | 23 | 1. Fork the repository 24 | 2. Create Feat_xxx branch 25 | 3. Commit your code 26 | 4. Create Pull Request 27 | 28 | 29 | #### Gitee Feature 30 | 31 | 1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md 32 | 2. Gitee blog [blog.gitee.com](https://blog.gitee.com) 33 | 3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) 34 | 4. The most valuable open source project [GVP](https://gitee.com/gvp) 35 | 5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) 36 | 6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 说明文档 2 | 3 | ## inbound 代码示例 4 | ### pom依赖 5 | ```xml 6 | 7 | 23 | 24 | 26 | 4.0.0 27 | 28 | freeswitch-esl-all 29 | link.thingscloud 30 | 1.6.4.RELEASE 31 | 32 | 33 | 34 | UTF-8 35 | UTF-8 36 | 37 | 4.1.65.Final 38 | 2.3.1.RELEASE 39 | 40 | 1.8 41 | 42 | 43 | 1.8 44 | 1.8 45 | 46 | 47 | freeswitch-esl-spring-boot-starter-example 48 | freeswitch-esl-spring-boot-starter-example-${project.version} 49 | 50 | Example project for Freeswitch Esl Spring Boot Starter 51 | 52 | 53 | 54 | ${project.groupId} 55 | freeswitch-esl-spring-boot-starter 56 | ${project.version} 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-web 61 | 62 | 63 | link.thingscloud 64 | spring-boot-common-aop 65 | 1.0.0-RELEASE 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-devtools 70 | runtime 71 | true 72 | 73 | 74 | org.projectlombok 75 | lombok 76 | true 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-test 81 | test 82 | 83 | 84 | 85 | org.freeswitch.esl.client 86 | org.freeswitch.esl.client 87 | 0.9.2 88 | 89 | 90 | 91 | 92 | com.alibaba 93 | fastjson 94 | 1.2.78 95 | 96 | 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-starter-data-redis 101 | 2.2.6.RELEASE 102 | 103 | 104 | 105 | org.apache.commons 106 | commons-pool2 107 | 2.6.2 108 | 109 | 110 | 111 | 112 | 113 | com.fasterxml.jackson.core 114 | jackson-core 115 | 116 | 117 | com.fasterxml.jackson.core 118 | jackson-databind 119 | 120 | 121 | com.fasterxml.jackson.core 122 | jackson-annotations 123 | 124 | 125 | 126 | 127 | 128 | 129 | ``` 130 | 131 | 132 | 133 | ## outbound示例 134 | 135 | ### 修改dialplan配置 136 | 137 | 出于演示目的,这里修改/usr/local/freeswitch/conf/dialplan/default.xml,在文件开头部分添加一段: 138 | 139 | ``` 140 | 141 | 142 | 143 | 144 | 145 | ``` 146 | 即:当来电的被叫号码为400开头时,fs将利用socket,连接到localhost:8040 147 | 148 | 149 | ## 接口文档 150 | ### 一、机器人外呼发起 151 | 152 | * 接口地址: /callcenter/api/startOutbound 153 | * 请求方法: POST 154 | * 请求参数: 155 | 156 | * 返回数据: 157 | ``` 158 | { 159 | code: 200, 160 | message: "success" 161 | data: null 162 | } 163 | message: 请求处理消息 164 | code = 200 请求处理成功 165 | code != 200 请求处理失败,警告消息提示:message内容 166 | ``` 167 | 168 | 169 | 170 | ## 相关资料 171 | [freeswitch笔记](https://www.cnblogs.com/yjmyzz/p/freeswitch-esl-java-client-turorial.html) 172 | 173 | [github: freeswitch 事件套接字基于 netty 4 并具有一些新功能](https://github.com/zhouhailin/freeswitch-esl-all) 174 | -------------------------------------------------------------------------------- /inbound/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/springboot-freeswitch/d3ed2a9d55da36fdbb56398ae2cdd86b1e34f64e/inbound/README.md -------------------------------------------------------------------------------- /inbound/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 21 | 4.0.0 22 | 23 | freeswitch-esl-all 24 | link.thingscloud 25 | 1.6.4.RELEASE 26 | 27 | 28 | 29 | UTF-8 30 | UTF-8 31 | 32 | 4.1.65.Final 33 | 2.3.1.RELEASE 34 | 35 | 1.8 36 | 37 | 38 | 1.8 39 | 1.8 40 | 41 | 42 | freeswitch-esl-spring-boot-starter-example 43 | freeswitch-esl-spring-boot-starter-example-${project.version} 44 | 45 | Example project for Freeswitch Esl Spring Boot Starter 46 | 47 | 48 | 49 | ${project.groupId} 50 | freeswitch-esl-spring-boot-starter 51 | ${project.version} 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-web 56 | 57 | 58 | link.thingscloud 59 | spring-boot-common-aop 60 | 1.0.0-RELEASE 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-devtools 65 | runtime 66 | true 67 | 68 | 69 | org.projectlombok 70 | lombok 71 | true 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-test 76 | test 77 | 78 | 79 | 80 | org.freeswitch.esl.client 81 | org.freeswitch.esl.client 82 | 0.9.2 83 | 84 | 85 | 86 | 87 | com.alibaba 88 | fastjson 89 | 1.2.78 90 | 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-starter-data-redis 96 | 2.2.6.RELEASE 97 | 98 | 99 | 100 | org.apache.commons 101 | commons-pool2 102 | 2.6.2 103 | 104 | 105 | 106 | 107 | 108 | com.fasterxml.jackson.core 109 | jackson-core 110 | 111 | 112 | com.fasterxml.jackson.core 113 | jackson-databind 114 | 115 | 116 | com.fasterxml.jackson.core 117 | jackson-annotations 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/InboundApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cn.ch3nnn; 19 | 20 | import org.springframework.boot.SpringApplication; 21 | import org.springframework.boot.autoconfigure.SpringBootApplication; 22 | 23 | 24 | // @EnableFreeswitchEslAutoConfiguration 25 | @SpringBootApplication 26 | public class InboundApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(InboundApplication.class, args); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/common/InitCacheLineDataRunner.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.common; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * spring容器加载完自动监听 初始化线路分配数据到缓存 11 | * 12 | * @Author ChenTong 13 | * @Date 2021/11/1 09:53 14 | */ 15 | @Slf4j 16 | @Component 17 | public class InitCacheLineDataRunner implements CommandLineRunner { 18 | 19 | @Autowired 20 | private RedisTemplate redisTemplate; 21 | 22 | @Override 23 | public void run(String... args) throws Exception { 24 | log.info("Start Cache LineData ...."); 25 | // TODO 初始化加载线路数据 26 | // String path = "src/main/resources/LineTest.json"; 27 | // final String jsonData = JsonUtil.readJsonFile(path); 28 | // final LineDataDto parse = JSON.parseObject(jsonData, LineDataDto.class); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/common/ResultCode.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.common; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * SpringBoot 统一响应格式 10 | * 11 | * @Author ChenTong 12 | * @Date 2021/8/27 13:55 13 | */ 14 | @Data 15 | @AllArgsConstructor 16 | public class ResultCode implements Serializable { 17 | 18 | private final static int SUCCESS_CODE = 1; 19 | private final static int ERROR_CODE = 0; 20 | private final static String SUCCESS_MESSAGE = "请求成功"; 21 | private final static String ERROR_MESSAGE = "请求失败"; 22 | 23 | private Integer code; 24 | private String message; 25 | private Object data; 26 | 27 | 28 | /** 29 | * 请求成功 30 | * 31 | * @return 视图模型实例 32 | */ 33 | public static ResultCode success() { 34 | return success(SUCCESS_MESSAGE); 35 | } 36 | 37 | /** 38 | * 请求成功 39 | * 40 | * @param data 响应数据 41 | * @return 视图模型实例 42 | */ 43 | public static ResultCode success(Object data) { 44 | return success(SUCCESS_MESSAGE, data); 45 | } 46 | 47 | /** 48 | * 请求成功 49 | * 50 | * @param message 响应信息 51 | * @return 视图模型实例 52 | */ 53 | public static ResultCode success(String message) { 54 | return success(message, null); 55 | } 56 | 57 | /** 58 | * 请求成功 59 | * 60 | * @param message 响应信息 61 | * @param data 响应数据 62 | * @return 视图模型实例 63 | */ 64 | public static ResultCode success(String message, Object data) { 65 | return new ResultCode(SUCCESS_CODE, message, data); 66 | } 67 | 68 | 69 | /** 70 | * 请求失败 71 | * 72 | * @return 视图模型实例 73 | */ 74 | public static ResultCode error() { 75 | return error(ERROR_MESSAGE); 76 | } 77 | 78 | 79 | /** 80 | * 请求失败 81 | * 82 | * @param message 异常信息 83 | * @return 视图模型实例 84 | */ 85 | public static ResultCode error(String message) { 86 | return error(message, null); 87 | } 88 | 89 | /** 90 | * 请求失败 91 | * 92 | * @param message 异常信息 93 | * @param data 响应数据 94 | * @return 视图模型实例 95 | */ 96 | public static ResultCode error(String message, Object data) { 97 | return new ResultCode(ERROR_CODE, message, data); 98 | } 99 | 100 | 101 | } 102 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.redis.connection.RedisConnectionFactory; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 11 | import org.springframework.data.redis.serializer.StringRedisSerializer; 12 | 13 | /** 14 | * @Author ChenTong 15 | * @Date 2021/11/1 09:59 16 | */ 17 | @Configuration 18 | public class RedisConfig { 19 | @Bean 20 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 21 | RedisTemplate redisTemplate = new RedisTemplate<>(); 22 | //设置工厂链接 23 | redisTemplate.setConnectionFactory(redisConnectionFactory); 24 | //设置自定义序列化方式 25 | setSerializeConfig(redisTemplate, redisConnectionFactory); 26 | return redisTemplate; 27 | } 28 | 29 | private void setSerializeConfig(RedisTemplate redisTemplate, RedisConnectionFactory redisConnectionFactory) { 30 | //对字符串采取普通的序列化方式 适用于key 因为我们一般采取简单字符串作为key 31 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 32 | //普通的string类型的key采用 普通序列化方式 33 | redisTemplate.setKeySerializer(stringRedisSerializer); 34 | //普通hash类型的key也使用 普通序列化方式 35 | redisTemplate.setHashKeySerializer(stringRedisSerializer); 36 | //解决查询缓存转换异常的问题 大家不能理解就直接用就可以了 这是springboot自带的jackson序列化类,但是会有一定问题 37 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 38 | ObjectMapper om = new ObjectMapper(); 39 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 40 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 41 | jackson2JsonRedisSerializer.setObjectMapper(om); 42 | //普通的值采用jackson方式自动序列化 43 | redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 44 | //hash类型的值也采用jackson方式序列化 45 | redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 46 | //属性设置完成afterPropertiesSet就会被调用,可以对设置不成功的做一些默认处理 47 | redisTemplate.afterPropertiesSet(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/controller/OutboundController.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.controller; 2 | 3 | import cn.ch3nnn.common.ResultCode; 4 | import cn.ch3nnn.dto.OutboundParam; 5 | import link.thingscloud.freeswitch.esl.InboundClient; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | 14 | /** 15 | * 外呼中心接口 16 | * 17 | * @Author ChenTong 18 | * @Date 2021/10/28 15:03 19 | */ 20 | @RestController 21 | @RequestMapping("/callcenter/api") 22 | public class OutboundController { 23 | 24 | @Autowired 25 | private RedisTemplate redisTemplate; 26 | 27 | @Autowired 28 | private InboundClient inboundClient; 29 | 30 | 31 | /** 32 | * 机器人外呼发起 33 | * 34 | * @param outboundParam 35 | * @return 36 | */ 37 | @RequestMapping(value = "/startOutbound", method = RequestMethod.POST) 38 | public ResultCode outBound(@RequestBody(required = false) OutboundParam outboundParam) { 39 | // 测试本地 40 | final String originate = inboundClient.sendAsyncApiCommand("127.0.0.1:8021", "originate", "user/1010 &echo outbound"); 41 | return ResultCode.success(originate); 42 | 43 | } 44 | 45 | /** 46 | * 当前主叫号可用线路数量 47 | * 48 | * @param 49 | * @return 50 | */ 51 | @RequestMapping(value = "/currentLineCount", method = RequestMethod.GET) 52 | public ResultCode lineCount() { 53 | return ResultCode.success(); 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/dto/LineCountParam.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.dto; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * 当前主叫号可用线路数量 7 | * 8 | * @Author ChenTong 9 | * @Date 2021/11/4 14:36 10 | */ 11 | public class LineCountParam { 12 | 13 | /** 14 | * 授权 id 15 | */ 16 | @JSONField(name = "appId") 17 | private String appId; 18 | 19 | /** 20 | * 授权 key(已加密过的 key) 21 | */ 22 | @JSONField(name = "appKey") 23 | private String appKey; 24 | 25 | /** 26 | * 主叫号码即电话线路的号码 (请求 caller 下面线路号码为空闲线路) 27 | */ 28 | @JSONField(name = "caller") 29 | private String caller; 30 | 31 | /** 32 | * 时间戳,精确到毫秒(1550645131000) 33 | */ 34 | @JSONField(name = "timeStamp") 35 | private String timeStamp; 36 | 37 | 38 | /** 39 | * 按顺序拼接字符串后 md5 加密 40 | */ 41 | @JSONField(name = "sign") 42 | private String sign; 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/dto/LineDataDto.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.dto; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @Author ChenTong 9 | * @Date 2021/10/28 21:55 10 | */ 11 | @NoArgsConstructor 12 | @Data 13 | public class LineDataDto { 14 | 15 | /** 16 | * $0 17 | */ 18 | @JSONField(name = "0") 19 | public _$0DTO $0; 20 | /** 21 | * $1 22 | */ 23 | @JSONField(name = "1") 24 | public _$1DTO $1; 25 | /** 26 | * $2 27 | */ 28 | @JSONField(name = "2") 29 | public _$2DTO $2; 30 | /** 31 | * $3 32 | */ 33 | @JSONField(name = "3") 34 | public _$3DTO $3; 35 | /** 36 | * $4 37 | */ 38 | @JSONField(name = "4") 39 | public _$4DTO $4; 40 | /** 41 | * $5 42 | */ 43 | @JSONField(name = "5") 44 | public _$5DTO $5; 45 | 46 | /** 47 | * _$0DTO 48 | */ 49 | @NoArgsConstructor 50 | @Data 51 | public static class _$0DTO { 52 | /** 53 | * trunkId 54 | */ 55 | @JSONField(name = "trunk_id") 56 | public Integer trunkId; 57 | /** 58 | * trunkState 59 | */ 60 | @JSONField(name = "trunk_state") 61 | public Integer trunkState; 62 | /** 63 | * appId 64 | */ 65 | @JSONField(name = "app_id") 66 | public String appId; 67 | /** 68 | * trunkPhoneNumber 69 | */ 70 | @JSONField(name = "trunk_phone_number") 71 | public String trunkPhoneNumber; 72 | /** 73 | * trunkLineType 74 | */ 75 | @JSONField(name = "trunk_line_type") 76 | public Integer trunkLineType; 77 | /** 78 | * srcIps 79 | */ 80 | @JSONField(name = "src_ips") 81 | public String srcIps; 82 | /** 83 | * gatewayName 84 | */ 85 | @JSONField(name = "gateway_name") 86 | public String gatewayName; 87 | /** 88 | * webHost 89 | */ 90 | @JSONField(name = "web_host") 91 | public String webHost; 92 | /** 93 | * webPort 94 | */ 95 | @JSONField(name = "web_port") 96 | public String webPort; 97 | /** 98 | * callUuid 99 | */ 100 | @JSONField(name = "call_uuid") 101 | public Integer callUuid; 102 | /** 103 | * useOss 104 | */ 105 | @JSONField(name = "USE_OSS") 106 | public String useOss; 107 | /** 108 | * remoteRecordPath 109 | */ 110 | @JSONField(name = "REMOTE_RECORD_PATH") 111 | public String remoteRecordPath; 112 | /** 113 | * remoteTtsPath 114 | */ 115 | @JSONField(name = "REMOTE_TTS_PATH") 116 | public String remoteTtsPath; 117 | } 118 | 119 | /** 120 | * _$1DTO 121 | */ 122 | @NoArgsConstructor 123 | @Data 124 | public static class _$1DTO { 125 | /** 126 | * trunkId 127 | */ 128 | @JSONField(name = "trunk_id") 129 | public Integer trunkId; 130 | /** 131 | * trunkState 132 | */ 133 | @JSONField(name = "trunk_state") 134 | public Integer trunkState; 135 | /** 136 | * appId 137 | */ 138 | @JSONField(name = "app_id") 139 | public String appId; 140 | /** 141 | * trunkPhoneNumber 142 | */ 143 | @JSONField(name = "trunk_phone_number") 144 | public String trunkPhoneNumber; 145 | /** 146 | * trunkLineType 147 | */ 148 | @JSONField(name = "trunk_line_type") 149 | public Integer trunkLineType; 150 | /** 151 | * srcIps 152 | */ 153 | @JSONField(name = "src_ips") 154 | public String srcIps; 155 | /** 156 | * gatewayName 157 | */ 158 | @JSONField(name = "gateway_name") 159 | public String gatewayName; 160 | /** 161 | * webHost 162 | */ 163 | @JSONField(name = "web_host") 164 | public String webHost; 165 | /** 166 | * webPort 167 | */ 168 | @JSONField(name = "web_port") 169 | public String webPort; 170 | /** 171 | * callUuid 172 | */ 173 | @JSONField(name = "call_uuid") 174 | public Integer callUuid; 175 | /** 176 | * useOss 177 | */ 178 | @JSONField(name = "USE_OSS") 179 | public String useOss; 180 | /** 181 | * remoteRecordPath 182 | */ 183 | @JSONField(name = "REMOTE_RECORD_PATH") 184 | public String remoteRecordPath; 185 | /** 186 | * remoteTtsPath 187 | */ 188 | @JSONField(name = "REMOTE_TTS_PATH") 189 | public String remoteTtsPath; 190 | } 191 | 192 | /** 193 | * _$2DTO 194 | */ 195 | @NoArgsConstructor 196 | @Data 197 | public static class _$2DTO { 198 | /** 199 | * trunkId 200 | */ 201 | @JSONField(name = "trunk_id") 202 | public Integer trunkId; 203 | /** 204 | * trunkState 205 | */ 206 | @JSONField(name = "trunk_state") 207 | public Integer trunkState; 208 | /** 209 | * appId 210 | */ 211 | @JSONField(name = "app_id") 212 | public String appId; 213 | /** 214 | * trunkPhoneNumber 215 | */ 216 | @JSONField(name = "trunk_phone_number") 217 | public String trunkPhoneNumber; 218 | /** 219 | * trunkLineType 220 | */ 221 | @JSONField(name = "trunk_line_type") 222 | public Integer trunkLineType; 223 | /** 224 | * srcIps 225 | */ 226 | @JSONField(name = "src_ips") 227 | public String srcIps; 228 | /** 229 | * gatewayName 230 | */ 231 | @JSONField(name = "gateway_name") 232 | public String gatewayName; 233 | /** 234 | * webHost 235 | */ 236 | @JSONField(name = "web_host") 237 | public String webHost; 238 | /** 239 | * webPort 240 | */ 241 | @JSONField(name = "web_port") 242 | public String webPort; 243 | /** 244 | * callUuid 245 | */ 246 | @JSONField(name = "call_uuid") 247 | public Integer callUuid; 248 | /** 249 | * useOss 250 | */ 251 | @JSONField(name = "USE_OSS") 252 | public String useOss; 253 | /** 254 | * remoteRecordPath 255 | */ 256 | @JSONField(name = "REMOTE_RECORD_PATH") 257 | public String remoteRecordPath; 258 | /** 259 | * remoteTtsPath 260 | */ 261 | @JSONField(name = "REMOTE_TTS_PATH") 262 | public String remoteTtsPath; 263 | } 264 | 265 | /** 266 | * _$3DTO 267 | */ 268 | @NoArgsConstructor 269 | @Data 270 | public static class _$3DTO { 271 | /** 272 | * trunkId 273 | */ 274 | @JSONField(name = "trunk_id") 275 | public Integer trunkId; 276 | /** 277 | * trunkState 278 | */ 279 | @JSONField(name = "trunk_state") 280 | public Integer trunkState; 281 | /** 282 | * appId 283 | */ 284 | @JSONField(name = "app_id") 285 | public String appId; 286 | /** 287 | * trunkPhoneNumber 288 | */ 289 | @JSONField(name = "trunk_phone_number") 290 | public String trunkPhoneNumber; 291 | /** 292 | * trunkLineType 293 | */ 294 | @JSONField(name = "trunk_line_type") 295 | public Integer trunkLineType; 296 | /** 297 | * srcIps 298 | */ 299 | @JSONField(name = "src_ips") 300 | public String srcIps; 301 | /** 302 | * gatewayName 303 | */ 304 | @JSONField(name = "gateway_name") 305 | public String gatewayName; 306 | /** 307 | * webHost 308 | */ 309 | @JSONField(name = "web_host") 310 | public String webHost; 311 | /** 312 | * webPort 313 | */ 314 | @JSONField(name = "web_port") 315 | public String webPort; 316 | /** 317 | * callUuid 318 | */ 319 | @JSONField(name = "call_uuid") 320 | public Integer callUuid; 321 | /** 322 | * useOss 323 | */ 324 | @JSONField(name = "USE_OSS") 325 | public String useOss; 326 | /** 327 | * remoteRecordPath 328 | */ 329 | @JSONField(name = "REMOTE_RECORD_PATH") 330 | public String remoteRecordPath; 331 | /** 332 | * remoteTtsPath 333 | */ 334 | @JSONField(name = "REMOTE_TTS_PATH") 335 | public String remoteTtsPath; 336 | } 337 | 338 | /** 339 | * _$4DTO 340 | */ 341 | @NoArgsConstructor 342 | @Data 343 | public static class _$4DTO { 344 | /** 345 | * trunkId 346 | */ 347 | @JSONField(name = "trunk_id") 348 | public Integer trunkId; 349 | /** 350 | * trunkState 351 | */ 352 | @JSONField(name = "trunk_state") 353 | public Integer trunkState; 354 | /** 355 | * appId 356 | */ 357 | @JSONField(name = "app_id") 358 | public String appId; 359 | /** 360 | * trunkPhoneNumber 361 | */ 362 | @JSONField(name = "trunk_phone_number") 363 | public String trunkPhoneNumber; 364 | /** 365 | * trunkLineType 366 | */ 367 | @JSONField(name = "trunk_line_type") 368 | public Integer trunkLineType; 369 | /** 370 | * srcIps 371 | */ 372 | @JSONField(name = "src_ips") 373 | public String srcIps; 374 | /** 375 | * gatewayName 376 | */ 377 | @JSONField(name = "gateway_name") 378 | public String gatewayName; 379 | /** 380 | * useOss 381 | */ 382 | @JSONField(name = "USE_OSS") 383 | public String useOss; 384 | /** 385 | * remoteRecordPath 386 | */ 387 | @JSONField(name = "REMOTE_RECORD_PATH") 388 | public String remoteRecordPath; 389 | /** 390 | * remoteTtsPath 391 | */ 392 | @JSONField(name = "REMOTE_TTS_PATH") 393 | public String remoteTtsPath; 394 | /** 395 | * webHost 396 | */ 397 | @JSONField(name = "web_host") 398 | public String webHost; 399 | /** 400 | * webPort 401 | */ 402 | @JSONField(name = "web_port") 403 | public String webPort; 404 | /** 405 | * callUuid 406 | */ 407 | @JSONField(name = "call_uuid") 408 | public Integer callUuid; 409 | } 410 | 411 | /** 412 | * _$5DTO 413 | */ 414 | @NoArgsConstructor 415 | @Data 416 | public static class _$5DTO { 417 | /** 418 | * trunkId 419 | */ 420 | @JSONField(name = "trunk_id") 421 | public Integer trunkId; 422 | /** 423 | * trunkState 424 | */ 425 | @JSONField(name = "trunk_state") 426 | public Integer trunkState; 427 | /** 428 | * appId 429 | */ 430 | @JSONField(name = "app_id") 431 | public String appId; 432 | /** 433 | * trunkPhoneNumber 434 | */ 435 | @JSONField(name = "trunk_phone_number") 436 | public String trunkPhoneNumber; 437 | /** 438 | * trunkLineType 439 | */ 440 | @JSONField(name = "trunk_line_type") 441 | public Integer trunkLineType; 442 | /** 443 | * srcIps 444 | */ 445 | @JSONField(name = "src_ips") 446 | public String srcIps; 447 | /** 448 | * gatewayName 449 | */ 450 | @JSONField(name = "gateway_name") 451 | public String gatewayName; 452 | /** 453 | * webHost 454 | */ 455 | @JSONField(name = "web_host") 456 | public String webHost; 457 | /** 458 | * webPort 459 | */ 460 | @JSONField(name = "web_port") 461 | public String webPort; 462 | /** 463 | * callUuid 464 | */ 465 | @JSONField(name = "call_uuid") 466 | public Integer callUuid; 467 | /** 468 | * useOss 469 | */ 470 | @JSONField(name = "USE_OSS") 471 | public String useOss; 472 | /** 473 | * remoteRecordPath 474 | */ 475 | @JSONField(name = "REMOTE_RECORD_PATH") 476 | public String remoteRecordPath; 477 | /** 478 | * remoteTtsPath 479 | */ 480 | @JSONField(name = "REMOTE_TTS_PATH") 481 | public String remoteTtsPath; 482 | } 483 | 484 | } 485 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/dto/OutboundParam.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.dto; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 机器人外呼发起 9 | * 10 | * @Author ChenTong 11 | * @Date 2021/10/28 15:12 12 | */ 13 | 14 | @NoArgsConstructor 15 | @Data 16 | public class OutboundParam { 17 | 18 | 19 | /** 20 | * 主叫号码即电话线路的号码 21 | */ 22 | @JSONField(name = "caller") 23 | private String caller; 24 | /** 25 | * 授权id 26 | */ 27 | @JSONField(name = "appId") 28 | private String appId; 29 | /** 30 | * 授权 key(已加密过的 key) 31 | */ 32 | @JSONField(name = "appKey") 33 | private String appKey; 34 | /** 35 | * 语义模板xml 36 | */ 37 | @JSONField(name = "xmlFileData") 38 | private String xmlFileData; 39 | /** 40 | * 被叫号码即被呼叫的用户的号码 41 | */ 42 | @JSONField(name = "callee") 43 | private String callee; 44 | /** 45 | * 模板名称 46 | */ 47 | @JSONField(name = "xmlFileName") 48 | private String xmlFileName; 49 | /** 50 | * 模板uuid(业务uuid) 51 | */ 52 | @JSONField(name = "uuid") 53 | private String uuid; 54 | /** 55 | * 按顺序拼接字符串后 md5 加密 56 | */ 57 | @JSONField(name = "sign") 58 | private String sign; 59 | 60 | /** 61 | * 时间戳,精确到毫秒(1550645131000) 62 | */ 63 | @JSONField(name = "timeStamp") 64 | private String timeStamp; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/esl/EslEventListener.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.esl; 2 | 3 | import link.thingscloud.freeswitch.esl.IEslEventListener; 4 | import link.thingscloud.freeswitch.esl.InboundClient; 5 | import link.thingscloud.freeswitch.esl.spring.boot.starter.annotation.EslEventName; 6 | import link.thingscloud.freeswitch.esl.spring.boot.starter.handler.EslEventHandler; 7 | import link.thingscloud.freeswitch.esl.transport.event.EslEvent; 8 | import link.thingscloud.freeswitch.esl.util.ArrayUtils; 9 | import link.thingscloud.freeswitch.esl.util.StringUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.InitializingBean; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.CollectionUtils; 15 | 16 | import java.util.*; 17 | 18 | /** 19 | * Esl 事件监听器 20 | * 21 | * @Author ChenTong 22 | * @Date 2021/11/1 11:13 23 | */ 24 | @Slf4j 25 | @Component 26 | public class EslEventListener implements IEslEventListener, InitializingBean { 27 | 28 | @Autowired 29 | private InboundClient inboundClient; 30 | 31 | @Autowired 32 | private final List eslEventHandlers = Collections.emptyList(); 33 | 34 | private final Map> handlerTable = new HashMap<>(16); 35 | 36 | private void handleEslEvent(String addr, EslEvent event) { 37 | String eventName = event.getEventName(); 38 | List handlers = handlerTable.get(eventName); 39 | if (!CollectionUtils.isEmpty(handlers)) { 40 | handlers.forEach(eventHandler -> eventHandler.handle(addr, event)); 41 | } 42 | } 43 | 44 | @Override 45 | public void eventReceived(String addr, EslEvent event) { 46 | handleEslEvent(addr, event); 47 | } 48 | 49 | @Override 50 | public void backgroundJobResultReceived(String addr, EslEvent event) { 51 | handleEslEvent(addr, event); 52 | } 53 | 54 | @Override 55 | public void afterPropertiesSet() { 56 | log.info("IEslEventListener init ..."); 57 | for (EslEventHandler eventHandler : eslEventHandlers) { 58 | EslEventName eventName = eventHandler.getClass().getAnnotation(EslEventName.class); 59 | if (eventName == null) { 60 | // FIXED : AOP 61 | eventName = eventHandler.getClass().getSuperclass().getAnnotation(EslEventName.class); 62 | } 63 | if (eventName == null || ArrayUtils.isEmpty(eventName.value())) { 64 | continue; 65 | } 66 | for (String value : eventName.value()) { 67 | if (StringUtils.isBlank(value)) { 68 | continue; 69 | } 70 | log.info("IEslEventListener add EventName[{}], EventHandler[{}] ...", value, eventHandler.getClass()); 71 | handlerTable.computeIfAbsent(value, k -> new ArrayList<>(4)).add(eventHandler); 72 | 73 | } 74 | } 75 | inboundClient.option().addListener(this); 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/handle/ExampleInboundClientOptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.handle; 2 | 3 | import link.thingscloud.freeswitch.esl.inbound.option.InboundClientOption; 4 | import link.thingscloud.freeswitch.esl.spring.boot.starter.handler.AbstractInboundClientOptionHandler; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | 8 | 9 | @Slf4j 10 | @Component 11 | public class ExampleInboundClientOptionHandler extends AbstractInboundClientOptionHandler { 12 | 13 | /** 14 | * {@inheritDoc} 15 | */ 16 | @Override 17 | protected void intercept(InboundClientOption inboundClientOption) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/handle/HeartbeatEslEventHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cn.ch3nnn.handle; 19 | 20 | import link.thingscloud.freeswitch.esl.constant.EventNames; 21 | import link.thingscloud.freeswitch.esl.spring.boot.starter.annotation.EslEventName; 22 | import link.thingscloud.freeswitch.esl.spring.boot.starter.handler.EslEventHandler; 23 | import link.thingscloud.freeswitch.esl.transport.event.EslEvent; 24 | import lombok.extern.slf4j.Slf4j; 25 | import org.springframework.stereotype.Component; 26 | 27 | @Slf4j 28 | @EslEventName(EventNames.HEARTBEAT) 29 | @Component 30 | public class HeartbeatEslEventHandler implements EslEventHandler { 31 | 32 | @Override 33 | public void handle(String addr, EslEvent event) { 34 | log.info("HeartbeatEslEventHandler handle addr[{}] EslEvent[{}].", addr, event); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/handle/ReScheduleEslEventHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cn.ch3nnn.handle; 19 | 20 | import link.thingscloud.freeswitch.esl.InboundClient; 21 | import link.thingscloud.freeswitch.esl.constant.EventNames; 22 | import link.thingscloud.freeswitch.esl.spring.boot.starter.annotation.EslEventName; 23 | import link.thingscloud.freeswitch.esl.spring.boot.starter.handler.AbstractEslEventHandler; 24 | import link.thingscloud.freeswitch.esl.transport.event.EslEvent; 25 | import link.thingscloud.spring.boot.common.aop.Logging; 26 | import lombok.extern.slf4j.Slf4j; 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.stereotype.Component; 29 | 30 | 31 | @Slf4j 32 | @EslEventName({EventNames.CHANNEL_HANGUP, EventNames.CHANNEL_HANGUP_COMPLETE}) 33 | @Component 34 | public class ReScheduleEslEventHandler extends AbstractEslEventHandler { 35 | 36 | @Autowired 37 | private InboundClient inboundClient; 38 | 39 | @Logging 40 | @Override 41 | public void handle(String addr, EslEvent event) { 42 | log.info("ReScheduleEslEventHandler handle addr[{}] EslEvent[{}].", addr, event); 43 | log.info("{}", inboundClient); 44 | // EslMessage eslMessage = inboundClient.sendSyncApiCommand(addr, "version", null); 45 | // log.info("{}", EslHelper.formatEslMessage(eslMessage)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/handle/ServerConnectionListenerImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cn.ch3nnn.handle; 19 | 20 | import link.thingscloud.freeswitch.esl.ServerConnectionListener; 21 | import link.thingscloud.freeswitch.esl.inbound.option.ServerOption; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.springframework.stereotype.Service; 24 | 25 | 26 | @Slf4j 27 | @Service 28 | public class ServerConnectionListenerImpl implements ServerConnectionListener { 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | @Override 33 | public void onOpened(ServerOption serverOption) { 34 | log.info("onOpened serverOption : {}", serverOption); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | @Override 41 | public void onClosed(ServerOption serverOption) { 42 | log.info("onClosed serverOption : {}", serverOption); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.utils; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileReader; 5 | import java.io.IOException; 6 | 7 | /** 8 | * @Author ChenTong 9 | * @Date 2021/10/28 22:35 10 | */ 11 | public class JsonUtils { 12 | 13 | 14 | /** 15 | * 读取json文件 16 | * 17 | * @param jsonFilePath 文件路径 18 | * @return 文件内容数据 19 | */ 20 | public static String readJsonFile(String jsonFilePath) { 21 | StringBuilder jsonStr = new StringBuilder(); 22 | try { 23 | String lineTxt; 24 | final FileReader fileReader = new FileReader(jsonFilePath); 25 | BufferedReader bufferedReader = new BufferedReader(fileReader); 26 | while ((lineTxt = bufferedReader.readLine()) != null) { 27 | jsonStr.append(lineTxt); 28 | } 29 | // 格式化json存在换行符需要替换 30 | return jsonStr.toString().replaceAll("[ \n\r]",""); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | return null; 35 | } 36 | 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/utils/RedisUtils.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * @author chentong 14 | */ 15 | @Component 16 | @Slf4j 17 | public class RedisUtils { 18 | 19 | @Autowired 20 | private RedisTemplate redisTemplate; 21 | 22 | public boolean expire(String key, long expire) { 23 | try { 24 | // 这儿没有ops什么的是因为每种数据类型都能设置过期时间 25 | redisTemplate.expire(key, expire, TimeUnit.SECONDS); 26 | return true; 27 | } catch (Exception e) { 28 | log.error("redis set key expire exception:" + e); 29 | return false; 30 | } 31 | } 32 | 33 | /*hash数据类型方法 opsForHash 表示是操作字符串类型*/ 34 | 35 | /** 36 | * @param key 健 37 | * @param field 属性 38 | * @param value 值 39 | * @return 40 | */ 41 | public boolean hset(String key, String field, Object value) { 42 | try { 43 | redisTemplate.opsForHash().put(key, field, value); 44 | return true; 45 | } catch (Exception e) { 46 | log.error("redis hset eror,key:{},field:{},value:{}", key, field, value); 47 | return false; 48 | } 49 | } 50 | 51 | /** 52 | * @param key 53 | * @param field 54 | * @param value 55 | * @param seconds(秒) 过期时间 56 | * @return 57 | */ 58 | public boolean hset(String key, String field, Object value, long seconds) { 59 | try { 60 | redisTemplate.opsForHash().put(key, field, value); 61 | // 调用通用方法设置过期时间 62 | expire(key, seconds); 63 | return true; 64 | } catch (Exception e) { 65 | log.error("redis hset and expire eror,key:{},field:{},value:{},exception:{}", key, field, value, e); 66 | return false; 67 | } 68 | } 69 | 70 | /** 71 | * 获取key中field属性的值 72 | * 73 | * @param key 74 | * @param field 75 | * @return 76 | */ 77 | public Object hget(String key, String field) { 78 | return redisTemplate.opsForHash().get(key, field); 79 | } 80 | 81 | /** 82 | * 获取key中多个属性的键值对,这儿使用map来接收 83 | * 84 | * @param key 85 | * @param fields 86 | * @return 87 | */ 88 | public Map hmget(String key, String... fields) { 89 | Map map = new HashMap<>(10); 90 | for (String field : fields) { 91 | map.put(field, hget(key, field)); 92 | } 93 | return map; 94 | } 95 | 96 | /** 97 | * @param key 获得该key下的所有键值对 98 | * @return 99 | */ 100 | public Map hmget(String key) { 101 | return redisTemplate.opsForHash().entries(key); 102 | } 103 | 104 | /** 105 | * @param key 键 106 | * @param map 对应多个键值 107 | * @return 108 | */ 109 | public boolean hmset(String key, Map map) { 110 | try { 111 | redisTemplate.opsForHash().putAll(key, map); 112 | return true; 113 | } catch (Exception e) { 114 | log.error("redis hmset eror,key:{},value:{},exception:{}", key, map, e); 115 | return false; 116 | } 117 | } 118 | 119 | 120 | /** 121 | * @param key 键 122 | * @param map 对应多个键值 123 | * @param seconds 过期时间(秒) 124 | * @return 125 | */ 126 | public boolean hmset(String key, Map map, long seconds) { 127 | try { 128 | redisTemplate.opsForHash().putAll(key, map); 129 | expire(key, seconds); 130 | return true; 131 | } catch (Exception e) { 132 | log.error("redis hmset eror,key:{},value:{},expireTime,exception:{}", key, map, seconds, e); 133 | return false; 134 | } 135 | } 136 | 137 | /** 138 | * 删除key中的属性 139 | * 140 | * @param key 141 | * @param fields 142 | */ 143 | public void hdel(String key, Object... fields) { 144 | redisTemplate.opsForHash().delete(key, fields); 145 | } 146 | 147 | /** 148 | * 判断key中是否存在某属性 149 | * 150 | * @param key 151 | * @param field 152 | * @return 153 | */ 154 | public boolean hHashKey(String key, String field) { 155 | return redisTemplate.opsForHash().hasKey(key, field); 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /inbound/src/main/java/cn/ch3nnn/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.utils; 2 | 3 | 4 | import cn.ch3nnn.dto.OutboundParam; 5 | 6 | /** 7 | * @Author ChenTong 8 | * @Date 2021/10/28 17:59 9 | */ 10 | public class Utils { 11 | 12 | 13 | /** 14 | * 检测手机号分配线路 15 | * 16 | * @param outboundParam 17 | * @param model 18 | */ 19 | public static void checkPhoneAndSetLine(OutboundParam outboundParam, String model) { 20 | 21 | } 22 | 23 | 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /inbound/src/main/resources/LineTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "trunk_id": 0, 4 | "trunk_state": 0, 5 | "app_id": "test", 6 | "trunk_phone_number": "99699994", 7 | "trunk_line_type": 1, 8 | "src_ips": "0.0.0.0", 9 | "gateway_name": "99699994", 10 | "call_uuid": 0, 11 | "USE_OSS": "True", 12 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 13 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts" 14 | }, 15 | "1": { 16 | "trunk_id": 1, 17 | "trunk_state": 0, 18 | "app_id": "test", 19 | "trunk_phone_number": "96999994", 20 | "trunk_line_type": 1, 21 | "src_ips": "0.0.0.0", 22 | "gateway_name": "96999994", 23 | "web_host": "192.168.0.231", 24 | "web_port": "8099", 25 | "call_uuid": 0, 26 | "USE_OSS": "True", 27 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 28 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts" 29 | }, 30 | "2": { 31 | "trunk_id": 2, 32 | "trunk_state": 0, 33 | "app_id": "test", 34 | "trunk_phone_number": "96999994", 35 | "trunk_line_type": 1, 36 | "src_ips": "0.0.0.0", 37 | "gateway_name": "96999994", 38 | "web_host": "192.168.0.231", 39 | "web_port": "8099", 40 | "call_uuid": 0, 41 | "USE_OSS": "True", 42 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 43 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts" 44 | }, 45 | "3": { 46 | "trunk_id": 3, 47 | "trunk_state": 0, 48 | "app_id": "test", 49 | "trunk_phone_number": "96999994", 50 | "trunk_line_type": 1, 51 | "src_ips": "0.0.0.0", 52 | "gateway_name": "96999994", 53 | "web_host": "192.168.0.231", 54 | "web_port": "8099", 55 | "call_uuid": 0, 56 | "USE_OSS": "True", 57 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 58 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts" 59 | }, 60 | "4": { 61 | "trunk_id": 4, 62 | "trunk_state": 0, 63 | "app_id": "test", 64 | "trunk_phone_number": "96999994", 65 | "trunk_line_type": 1, 66 | "src_ips": "0.0.0.0", 67 | "gateway_name": "96999994", 68 | "USE_OSS": "True", 69 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 70 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts", 71 | "web_host": "192.168.0.231", 72 | "web_port": "8099", 73 | "call_uuid": 0 74 | }, 75 | "5": { 76 | "trunk_id": 5, 77 | "trunk_state": 0, 78 | "app_id": "test", 79 | "trunk_phone_number": "96999994", 80 | "trunk_line_type": 1, 81 | "src_ips": "0.0.0.0", 82 | "gateway_name": "96999994", 83 | "web_host": "192.168.0.231", 84 | "web_port": "8099", 85 | "call_uuid": 0, 86 | "USE_OSS": "True", 87 | "REMOTE_RECORD_PATH": "test/VISIT_WAV", 88 | "REMOTE_TTS_PATH": "test/VISIT_WAV/tts" 89 | } 90 | } -------------------------------------------------------------------------------- /inbound/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | redis: 3 | database: 0 4 | # Redis服务器地址 写你的ip 5 | host: 127.0.0.1 6 | # Redis服务器连接端口 7 | port: 6379 8 | # Redis服务器连接密码(默认为空) 9 | password: 10 | # 连接池最大连接数(使用负值表示没有限制 类似于mysql的连接池 11 | jedis: 12 | pool: 13 | max-active: 200 14 | # 连接池最大阻塞等待时间(使用负值表示没有限制) 表示连接池的链接拿完了 现在去申请需要等待的时间 15 | max-wait: -1 16 | # 连接池中的最大空闲连接 17 | max-idle: 10 18 | # 连接池中的最小空闲连接 19 | min-idle: 0 20 | # 连接超时时间(毫秒) 去链接redis服务端 21 | timeout: 6000 22 | 23 | 24 | 25 | link: 26 | thingscloud: 27 | freeswitch: 28 | esl: 29 | inbound: 30 | defaultPassword: ClueCon 31 | performance: false 32 | performanceCostTime: 200 33 | servers: 34 | - host: 127.0.0.1 35 | port: 8021 36 | timeoutSeconds: 5 37 | events: 38 | - all 39 | 40 | 41 | logging: 42 | level: 43 | link: DEBUG 44 | 45 | 46 | server: 47 | port: 8002 48 | 49 | -------------------------------------------------------------------------------- /outbound/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 22 | 23 | freeswitch-esl-all 24 | link.thingscloud 25 | 1.6.4.RELEASE 26 | 27 | 4.0.0 28 | 29 | freeswitch-esl 30 | freeswitch-esl-${project.version} 31 | Freeswitch Esl 32 | 33 | 34 | 35 | 36 | io.netty 37 | netty-all 38 | 39 | 40 | 41 | 42 | org.slf4j 43 | slf4j-api 44 | 45 | 46 | ch.qos.logback 47 | logback-core 48 | 49 | 50 | ch.qos.logback 51 | logback-classic 52 | 53 | 54 | 55 | org.freeswitch.esl.client 56 | org.freeswitch.esl.client 57 | 0.9.2 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/AbstractOutboundClientEventHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 david varnes. 3 | * 4 | * Licensed under the Apache License, version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cn.ch3nnn; 17 | 18 | import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler; 19 | import org.freeswitch.esl.client.transport.CommandResponse; 20 | import org.freeswitch.esl.client.transport.message.EslMessage; 21 | import org.jboss.netty.channel.ChannelHandlerContext; 22 | 23 | 24 | /** 25 | * 抽象出站客户端事件处理程序 26 | */ 27 | public abstract class AbstractOutboundClientEventHandler extends AbstractOutboundClientHandler { 28 | 29 | 30 | /** 31 | * 注册监听事件 32 | * 33 | * @param ctx 通道处理程序上下文 34 | * @param format 格式 35 | * @param events 事件名称 36 | * @return 37 | */ 38 | public CommandResponse setEventSubscriptions(ChannelHandlerContext ctx, String format, String events) { 39 | if (!format.equals("plain")) { 40 | throw new IllegalStateException("Only 'plain' event format is supported at present"); 41 | } 42 | StringBuilder sb = new StringBuilder(); 43 | sb.append("event "); 44 | sb.append(format); 45 | if (events != null && !events.isEmpty()) { 46 | sb.append(' '); 47 | sb.append(events); 48 | } 49 | EslMessage response = sendSyncSingleLineCommand(ctx.getChannel(), sb.toString()); 50 | 51 | return new CommandResponse(sb.toString(), response); 52 | } 53 | 54 | 55 | /** 56 | * 添加事件过滤器 57 | * 58 | * @param ctx 通道处理程序上下文 59 | * @param eventHeader 过滤名称 60 | * @param valueToFilter 过滤值 61 | * @return 62 | */ 63 | public CommandResponse addEventFilter(ChannelHandlerContext ctx, String eventHeader, String valueToFilter) { 64 | StringBuilder sb = new StringBuilder(); 65 | sb.append("filter "); 66 | if (eventHeader != null && !eventHeader.isEmpty()) { 67 | sb.append(eventHeader); 68 | } 69 | if (valueToFilter != null && !valueToFilter.isEmpty()) { 70 | sb.append(' '); 71 | sb.append(valueToFilter); 72 | } 73 | 74 | EslMessage response = sendSyncSingleLineCommand(ctx.getChannel(), sb.toString()); 75 | return new CommandResponse(sb.toString(), response); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/OutboundApplication.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn; 2 | 3 | import org.freeswitch.esl.client.outbound.SocketClient; 4 | 5 | public class OutboundApplication { 6 | 7 | public static void main(String[] args) throws InterruptedException { 8 | 9 | new Thread(() -> { 10 | SocketClient socketClient = new SocketClient(8040, new PipelineFactory()); 11 | socketClient.start(); 12 | }).start(); 13 | 14 | while (true) { 15 | Thread.sleep(500); 16 | } 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/OutboundHandler.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn; 2 | 3 | import cn.ch3nnn.handle.OutBoundEventHandler; 4 | import cn.ch3nnn.service.BridgeCallService; 5 | import lombok.SneakyThrows; 6 | import org.freeswitch.esl.client.transport.event.EslEvent; 7 | import org.freeswitch.esl.client.transport.message.EslMessage; 8 | import org.jboss.netty.channel.ChannelHandlerContext; 9 | import org.jboss.netty.channel.ChannelStateEvent; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * 出站处理程序 16 | * 17 | * @Author ChenTong 18 | * @Date 2021/11/1 15:14 19 | */ 20 | public class OutboundHandler extends AbstractOutboundClientEventHandler { 21 | 22 | private final Map>> handlerTable; 23 | 24 | public OutboundHandler(Map>> handlerTable) { 25 | this.handlerTable = handlerTable; 26 | } 27 | 28 | /** 29 | * 通道已连接 30 | * 31 | * @param ctx 通道处理程序上下文 32 | * @param e 通道状态事件对象 33 | */ 34 | @Override 35 | public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { 36 | this.log.debug("Received new connection from server, sending connect message"); 37 | EslMessage response = this.sendSyncSingleLineCommand(ctx.getChannel(), "connect"); 38 | EslEvent channelDataEvent = new EslEvent(response, true); 39 | this.handleConnectResponse(ctx, channelDataEvent); 40 | 41 | // 注册监听事件 42 | setEventSubscriptions(ctx, "plain", "PLAYBACK_START"); 43 | setEventSubscriptions(ctx, "plain", "PLAYBACK_STOP"); 44 | setEventSubscriptions(ctx, "plain", "CHANNEL_HANGUP"); 45 | setEventSubscriptions(ctx, "plain", "CHANNEL_HANGUP_COMPLETE"); 46 | setEventSubscriptions(ctx, "plain", "RECORD_START"); 47 | setEventSubscriptions(ctx, "plain", "RECORD_STOP"); 48 | setEventSubscriptions(ctx, "plain", "CHANNEL_ANSWER"); 49 | /* 50 | TODO 加上下面两个代码 监听不到事件 (过滤指定通话) 51 | final Map eventHeaders = channelDataEvent.getEventHeaders(); 52 | final CommandResponse commandResponse = addEventFilter(ctx, "Unique-ID", eventHeaders.get("Unique-ID")); 53 | */ 54 | 55 | } 56 | 57 | 58 | /** 59 | * 处理连接响应 60 | * 61 | * @param ctx 通道处理程序上下文 62 | * @param event 事件对象 63 | */ 64 | @Override 65 | protected void handleConnectResponse(ChannelHandlerContext ctx, EslEvent event) { 66 | System.out.println("Received connect response :" + event); 67 | System.out.println("EventName :" + event.getEventName()); 68 | if (event.getEventName().equalsIgnoreCase("CHANNEL_DATA")) { 69 | // 初始连接的响应 70 | System.out.println("======================= incoming channel data ============================="); 71 | System.out.println("Event-Date-Local: " + event.getEventDateLocal()); 72 | System.out.println("Unique-ID: " + event.getEventHeaders().get("Unique-ID")); 73 | System.out.println("Channel-ANI: " + event.getEventHeaders().get("Channel-ANI")); 74 | System.out.println("Answer-State: " + event.getEventHeaders().get("Answer-State")); 75 | System.out.println("Caller-Destination-Number: " + event.getEventHeaders().get("Caller-Destination-Number")); 76 | System.out.println("======================= = = = = = = = = = = = ============================="); 77 | 78 | // TODO 处理业务逻辑 读取wav文件 切割chunk wav文件 识别语音文件 79 | // 桥接电话 80 | new BridgeCallService().bridgeCall(ctx.getChannel(), event, this); 81 | 82 | } else { 83 | throw new IllegalStateException("Unexpected event after connect: [" + event.getEventName() + ']'); 84 | } 85 | } 86 | 87 | /** 88 | * 处理 Esl 事件 89 | * 90 | * @param ctx 通道处理程序上下文 91 | * @param event 事件对象 92 | */ 93 | @SneakyThrows 94 | @Override 95 | protected void handleEslEvent(ChannelHandlerContext ctx, EslEvent event) { 96 | final List> classes = handlerTable.get(event.getEventName()); 97 | if (classes != null) for (Class clazz : classes) { 98 | final OutBoundEventHandler instance = (OutBoundEventHandler) clazz.newInstance(); 99 | try { 100 | // 执行每个事件任务 101 | instance.handle(ctx, event, this); 102 | log.info("Success EventHandle Name: {}", event.getEventName()); 103 | } catch (Exception e) { 104 | log.error("Error EventHandle Name: {} -> {}", event.getEventName(), e.getMessage()); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * 处理断线通知 111 | */ 112 | @Override 113 | protected void handleDisconnectionNotice() { 114 | super.handleDisconnectionNotice(); 115 | log.info("Received disconnection notice"); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/PipelineFactory.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn; 2 | 3 | import cn.ch3nnn.annotation.OutBoundEventName; 4 | import cn.ch3nnn.utils.ClassUtil; 5 | import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler; 6 | import org.freeswitch.esl.client.outbound.AbstractOutboundPipelineFactory; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * 连接管道工厂类 15 | * 16 | * @Author ChenTong 17 | * @Date 2021/11/1 15:16 18 | */ 19 | public class PipelineFactory extends AbstractOutboundPipelineFactory { 20 | 21 | 22 | @Override 23 | protected AbstractOutboundClientHandler makeHandler() { 24 | 25 | final Map>> handlerTable = new HashMap<>(16); 26 | 27 | // 通过反射查找包下所有全类名 28 | String packageName = "cn.ch3nnn.handle"; 29 | final List> classes = ClassUtil.getClasses(packageName); 30 | for (Class clazz: classes){ 31 | final OutBoundEventName eventName = clazz.getAnnotation(OutBoundEventName.class); 32 | if (eventName != null) for (String value : eventName.value()) { 33 | handlerTable.computeIfAbsent(value, k -> new ArrayList<>(4)).add(clazz); 34 | } 35 | } 36 | return new OutboundHandler(handlerTable); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/annotation/OutBoundEventName.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cn.ch3nnn.annotation; 19 | 20 | import java.lang.annotation.*; 21 | 22 | 23 | /** 24 | * 添加监听事件注解 25 | * 通过反射获取所有装饰此注解类 26 | * 27 | */ 28 | @Target({ElementType.TYPE}) 29 | @Retention(RetentionPolicy.RUNTIME) 30 | @Documented 31 | public @interface OutBoundEventName { 32 | 33 | String[] value(); 34 | } 35 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/config/OutboundServerConfig.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.config; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | /** 7 | * @Author ChenTong 8 | * @Date 2021/11/8 10:18 9 | */ 10 | public class OutboundServerConfig { 11 | 12 | /** 13 | * 项目基础路径 14 | */ 15 | public static String BASE_DIR; 16 | 17 | static { 18 | try { 19 | BASE_DIR = new File("").getCanonicalPath(); 20 | } catch (IOException e) { 21 | e.printStackTrace(); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/handle/ChannelAnswerOutboundEventHandler.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.handle; 2 | 3 | 4 | import cn.ch3nnn.OutboundHandler; 5 | import cn.ch3nnn.annotation.OutBoundEventName; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.freeswitch.esl.client.transport.SendMsg; 8 | import org.freeswitch.esl.client.transport.event.EslEvent; 9 | import org.jboss.netty.channel.ChannelHandlerContext; 10 | 11 | import static cn.ch3nnn.config.OutboundServerConfig.BASE_DIR; 12 | 13 | /** 14 | * 应答事件 15 | * 16 | * @Author ChenTong 17 | * @Date 2021/11/2 18:22 18 | */ 19 | @Slf4j 20 | @OutBoundEventName("CHANNEL_ANSWER") 21 | public class ChannelAnswerOutboundEventHandler implements OutBoundEventHandler { 22 | 23 | @Override 24 | public void handle(ChannelHandlerContext ctx, EslEvent event, OutboundHandler handler) { 25 | 26 | SendMsg bridgeMsg = new SendMsg(); 27 | bridgeMsg.addCallCommand("execute"); 28 | bridgeMsg.addExecuteAppName("playback"); 29 | bridgeMsg.addExecuteAppArg(BASE_DIR + "/outbound/src/main/resources/test.wav"); 30 | handler.sendSyncMultiLineCommand(ctx.getChannel(), bridgeMsg.getMsgLines()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/handle/OutBoundEventHandler.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.handle; 2 | 3 | import cn.ch3nnn.OutboundHandler; 4 | import org.freeswitch.esl.client.transport.event.EslEvent; 5 | import org.jboss.netty.channel.ChannelHandlerContext; 6 | 7 | /** 8 | * 出站事件处理程序 9 | * 10 | * @Author ChenTong 11 | * @Date 2021/11/3 15:05 12 | */ 13 | public interface OutBoundEventHandler { 14 | 15 | /** 16 | * 处理事件方法 17 | * 18 | * @param ctx 通道处理程序上下文 19 | * @param event 事件对象 20 | * @param handler 出站处理器 (可以获取发送命令方法) 21 | */ 22 | void handle(ChannelHandlerContext ctx, EslEvent event, OutboundHandler handler); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/handle/PlayBackSTOPOutBoundEventHandler.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.handle; 2 | 3 | 4 | import cn.ch3nnn.OutboundHandler; 5 | import cn.ch3nnn.annotation.OutBoundEventName; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.freeswitch.esl.client.transport.SendMsg; 8 | import org.freeswitch.esl.client.transport.event.EslEvent; 9 | import org.jboss.netty.channel.ChannelHandlerContext; 10 | 11 | /** 12 | * 放音结束事件 13 | * 14 | * @Author ChenTong 15 | * @Date 2021/11/2 18:22 16 | */ 17 | @Slf4j 18 | @OutBoundEventName("PLAYBACK_STOP") 19 | public class PlayBackSTOPOutBoundEventHandler implements OutBoundEventHandler { 20 | 21 | @Override 22 | public void handle(ChannelHandlerContext ctx, EslEvent event, OutboundHandler handler) { 23 | SendMsg bridgeMsg1 = new SendMsg(); 24 | bridgeMsg1.addCallCommand("execute"); 25 | bridgeMsg1.addExecuteAppName("hangup"); 26 | handler.sendSyncMultiLineCommand(ctx.getChannel(), bridgeMsg1.getMsgLines()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/service/BridgeCallService.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.service; 2 | 3 | import cn.ch3nnn.OutboundHandler; 4 | import org.freeswitch.esl.client.transport.SendMsg; 5 | import org.freeswitch.esl.client.transport.event.EslEvent; 6 | import org.freeswitch.esl.client.transport.message.EslHeaders; 7 | import org.freeswitch.esl.client.transport.message.EslMessage; 8 | import org.jboss.netty.channel.Channel; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * 例子: 桥接呼叫 15 | * 16 | * @Author ChenTong 17 | * @Date 2021/11/3 23:39 18 | */ 19 | public class BridgeCallService { 20 | 21 | public void bridgeCall(Channel channel, EslEvent event, OutboundHandler handler) { 22 | List extNums = new ArrayList<>(2); 23 | extNums.add("1000"); 24 | extNums.add("1010"); 25 | // 随机找1个目标(注:这里只是演示目的,真正分配时,应该考虑到客服的忙闲情况,通常应该分给最空闲的客服) 26 | // String destNumber = extNums.get((int) Math.abs(System.currentTimeMillis() % 2)); 27 | String destNumber = "1010"; 28 | 29 | SendMsg bridgeMsg = new SendMsg(); 30 | bridgeMsg.addCallCommand("execute"); 31 | bridgeMsg.addExecuteAppName("bridge"); 32 | bridgeMsg.addExecuteAppArg("user/" + destNumber); 33 | 34 | // 同步发送bridge命令接通 35 | EslMessage response = handler.sendSyncMultiLineCommand(channel, bridgeMsg.getMsgLines()); 36 | if (response.getHeaderValue(EslHeaders.Name.REPLY_TEXT).startsWith("+OK")) { 37 | String originCall = event.getEventHeaders().get("Caller-Destination-Number"); 38 | System.out.println(originCall + " bridge to " + destNumber + " successful"); 39 | } else { 40 | System.out.println("Call bridge failed: " + response.getHeaderValue(EslHeaders.Name.REPLY_TEXT)); 41 | } 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /outbound/src/main/java/cn/ch3nnn/utils/ClassUtil.java: -------------------------------------------------------------------------------- 1 | package cn.ch3nnn.utils; 2 | 3 | 4 | import java.io.File; 5 | import java.io.FileFilter; 6 | import java.io.IOException; 7 | import java.net.JarURLConnection; 8 | import java.net.URL; 9 | import java.net.URLDecoder; 10 | import java.util.ArrayList; 11 | import java.util.Enumeration; 12 | import java.util.List; 13 | import java.util.jar.JarEntry; 14 | import java.util.jar.JarFile; 15 | 16 | /** 17 | * 类相关的工具类 18 | * 19 | * @author ohergal 20 | */ 21 | public class ClassUtil { 22 | 23 | public static void main(String[] args) throws Exception { 24 | final List> classes = ClassUtil.getClasses("com.myweimai.outbound.handle"); 25 | for (Class clas : classes) { 26 | System.out.println(clas.getName()); 27 | } 28 | } 29 | 30 | /** 31 | * 取得某个接口下所有实现这个接口的类 32 | */ 33 | public static List getAllClassByInterface(Class c) { 34 | List returnClassList = null; 35 | 36 | if (c.isInterface()) { 37 | // 获取当前的包名 38 | String packageName = c.getPackage().getName(); 39 | // 获取当前包下以及子包下所以的类 40 | List> allClass = getClasses(packageName); 41 | if (allClass != null) { 42 | returnClassList = new ArrayList(); 43 | for (Class classes : allClass) { 44 | // 判断是否是同一个接口 45 | if (c.isAssignableFrom(classes)) { 46 | // 本身不加入进去 47 | if (!c.equals(classes)) { 48 | returnClassList.add(classes); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | return returnClassList; 56 | } 57 | 58 | 59 | /* 60 | * 取得某一类所在包的所有类名 不含迭代 61 | */ 62 | public static String[] getPackageAllClassName(String classLocation, String packageName) { 63 | //将packageName分解 64 | String[] packagePathSplit = packageName.split("[.]"); 65 | String realClassLocation = classLocation; 66 | int packageLength = packagePathSplit.length; 67 | for (int i = 0; i < packageLength; i++) { 68 | realClassLocation = realClassLocation + File.separator + packagePathSplit[i]; 69 | } 70 | File packeageDir = new File(realClassLocation); 71 | if (packeageDir.isDirectory()) { 72 | String[] allClassName = packeageDir.list(); 73 | return allClassName; 74 | } 75 | return null; 76 | } 77 | 78 | /** 79 | * 从包package中获取所有的Class 80 | * 81 | * @return 82 | */ 83 | public static List> getClasses(String packageName) { 84 | 85 | //第一个class类的集合 86 | List> classes = new ArrayList>(); 87 | //是否循环迭代 88 | boolean recursive = true; 89 | //获取包的名字 并进行替换 90 | String packageDirName = packageName.replace('.', '/'); 91 | //定义一个枚举的集合 并进行循环来处理这个目录下的things 92 | Enumeration dirs; 93 | try { 94 | dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); 95 | //循环迭代下去 96 | while (dirs.hasMoreElements()) { 97 | //获取下一个元素 98 | URL url = dirs.nextElement(); 99 | //得到协议的名称 100 | String protocol = url.getProtocol(); 101 | //如果是以文件的形式保存在服务器上 102 | if ("file".equals(protocol)) { 103 | //获取包的物理路径 104 | String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); 105 | //以文件的方式扫描整个包下的文件 并添加到集合中 106 | findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); 107 | } else if ("jar".equals(protocol)) { 108 | //如果是jar包文件 109 | //定义一个JarFile 110 | JarFile jar; 111 | try { 112 | //获取jar 113 | jar = ((JarURLConnection) url.openConnection()).getJarFile(); 114 | //从此jar包 得到一个枚举类 115 | Enumeration entries = jar.entries(); 116 | //同样的进行循环迭代 117 | while (entries.hasMoreElements()) { 118 | //获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 119 | JarEntry entry = entries.nextElement(); 120 | String name = entry.getName(); 121 | //如果是以/开头的 122 | if (name.charAt(0) == '/') { 123 | //获取后面的字符串 124 | name = name.substring(1); 125 | } 126 | //如果前半部分和定义的包名相同 127 | if (name.startsWith(packageDirName)) { 128 | int idx = name.lastIndexOf('/'); 129 | //如果以"/"结尾 是一个包 130 | if (idx != -1) { 131 | //获取包名 把"/"替换成"." 132 | packageName = name.substring(0, idx).replace('/', '.'); 133 | } 134 | //如果可以迭代下去 并且是一个包 135 | if ((idx != -1) || recursive) { 136 | //如果是一个.class文件 而且不是目录 137 | if (name.endsWith(".class") && !entry.isDirectory()) { 138 | //去掉后面的".class" 获取真正的类名 139 | String className = name.substring(packageName.length() + 1, name.length() - 6); 140 | try { 141 | //添加到classes 142 | classes.add(Class.forName(packageName + '.' + className)); 143 | } catch (ClassNotFoundException e) { 144 | e.printStackTrace(); 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } catch (IOException e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | } 155 | } catch (IOException e) { 156 | e.printStackTrace(); 157 | } 158 | 159 | return classes; 160 | } 161 | 162 | /** 163 | * 以文件的形式来获取包下的所有Class 164 | * 165 | * @param packageName 166 | * @param packagePath 167 | * @param recursive 168 | * @param classes 169 | */ 170 | public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, List> classes) { 171 | //获取此包的目录 建立一个File 172 | File dir = new File(packagePath); 173 | //如果不存在或者 也不是目录就直接返回 174 | if (!dir.exists() || !dir.isDirectory()) { 175 | return; 176 | } 177 | //如果存在 就获取包下的所有文件 包括目录 178 | File[] dirfiles = dir.listFiles(new FileFilter() { 179 | //自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) 180 | public boolean accept(File file) { 181 | return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); 182 | } 183 | }); 184 | //循环所有文件 185 | for (File file : dirfiles) { 186 | //如果是目录 则继续扫描 187 | if (file.isDirectory()) { 188 | findAndAddClassesInPackageByFile(packageName + "." + file.getName(), 189 | file.getAbsolutePath(), 190 | recursive, 191 | classes); 192 | } else { 193 | //如果是java类文件 去掉后面的.class 只留下类名 194 | String className = file.getName().substring(0, file.getName().length() - 6); 195 | try { 196 | //添加到集合中去 197 | classes.add(Class.forName(packageName + '.' + className)); 198 | } catch (ClassNotFoundException e) { 199 | e.printStackTrace(); 200 | } 201 | } 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /outbound/src/main/resources/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/springboot-freeswitch/d3ed2a9d55da36fdbb56398ae2cdd86b1e34f64e/outbound/src/main/resources/test.wav --------------------------------------------------------------------------------