init v1.0.0

This commit is contained in:
ageer
2024-02-27 20:52:19 +08:00
parent 1f7f97e86a
commit a079ef44e5
602 changed files with 163057 additions and 95 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 139 KiB

28
pom.xml
View File

@@ -10,7 +10,7 @@
<name>ruoyi-ai</name>
<url>https://gitee.com/ageerle/ruoyi-ai</url>
<description>AI助手后台管理系统</description>
<description>AI助手</description>
<properties>
<revision>1.0.0</revision>
@@ -242,16 +242,16 @@
<version>${tencent.sms.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>de.codecentric</groupId>-->
<!-- <artifactId>spring-boot-admin-starter-server</artifactId>-->
<!-- <version>${spring-boot-admin.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>de.codecentric</groupId>-->
<!-- <artifactId>spring-boot-admin-starter-client</artifactId>-->
<!-- <version>${spring-boot-admin.version}</version>-->
<!-- </dependency>-->
<!--redisson-->
<dependency>
@@ -318,6 +318,12 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-midjourney</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-generator</artifactId>

View File

@@ -59,6 +59,11 @@
<artifactId>ruoyi-job</artifactId>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-midjourney</artifactId>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.xmzs</groupId>
@@ -71,10 +76,10 @@
<artifactId>ruoyi-demo</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>de.codecentric</groupId>-->
<!-- <artifactId>spring-boot-admin-starter-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -94,7 +94,7 @@ public class CaptchaController {
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailUtils.sendText(emailRequest.getUsername(), "GPT助手】登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
MailUtils.sendText(emailRequest.getUsername(), "熊猫助手】登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
return R.fail(e.getMessage());

View File

@@ -3,7 +3,10 @@ package com.xmzs.controller;
import com.xmzs.common.chat.domain.request.ChatRequest;
import com.xmzs.common.chat.domain.request.Dall3Request;
import com.xmzs.common.chat.domain.request.MjTaskRequest;
import com.xmzs.common.chat.entity.Tts.TextToSpeech;
import com.xmzs.common.chat.entity.images.Item;
import com.xmzs.common.chat.entity.whisper.WhisperResponse;
import com.xmzs.common.core.domain.R;
import com.xmzs.common.core.domain.model.LoginUser;
import com.xmzs.common.core.exception.base.BaseException;
@@ -13,19 +16,30 @@ import com.xmzs.common.satoken.utils.LoginHelper;
import com.xmzs.system.domain.bo.ChatMessageBo;
import com.xmzs.system.domain.vo.ChatMessageVo;
import com.xmzs.system.service.IChatMessageService;
import com.xmzs.system.service.SseService;
import com.xmzs.system.service.ISseService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import retrofit2.Response;
/**
* 描述:
*
@@ -37,38 +51,58 @@ import java.util.List;
@RequiredArgsConstructor
public class ChatController {
private final SseService sseService;
private final ISseService ISseService;
private final IChatMessageService chatMessageService;
private final IChatMessageService chatMessageService;
/**
* 聊天接口
*/
@PostMapping("/chat")
@ResponseBody
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) {
if("gpt-4-all".equals(chatRequest.getModel())
|| chatRequest.getModel().startsWith("gpt-4-gizmo")
|| chatRequest.getModel().startsWith("net-")
){
return sseService.transitChat(chatRequest);
}
if("azure-gpt-3.5".equals(chatRequest.getModel())){
return sseService.azureChat(chatRequest);
}
return sseService.sseChat(chatRequest);
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletResponse response) {
return ISseService.sseChat(chatRequest);
}
/**
* 语音转文本
*
* @param file
*/
@PostMapping("/audio")
@ResponseBody
public WhisperResponse audio(@RequestParam("file") MultipartFile file) {
WhisperResponse whisperResponse = ISseService.speechToTextTranscriptionsV2(file);
return whisperResponse;
}
/**
* 文本转语音
*
* @param textToSpeech
*/
@PostMapping("/speech")
@ResponseBody
public ResponseEntity<Resource> speech(@RequestBody TextToSpeech textToSpeech) {
return ISseService.textToSpeed(textToSpeech);
}
@PostMapping("/dall3")
@ResponseBody
public R<List<Item>> dall3(@RequestBody @Valid Dall3Request request) {
return R.ok(sseService.dall3(request));
return R.ok(ISseService.dall3(request));
}
/**
* 扣除mj绘图费用
*
* @return
*/
@PostMapping("/mjTask")
@ResponseBody
public R<String> mjTask() {
sseService.mjTask();
public R<String> mjTask(@RequestBody MjTaskRequest mjTaskRequest) {
ISseService.mjTask(mjTaskRequest);
return R.ok();
}
@@ -77,7 +111,7 @@ public class ChatController {
*/
@PostMapping("/chatList")
@ResponseBody
public R<TableDataInfo<ChatMessageVo>> list(@RequestBody @Valid ChatMessageBo chatRequest,@RequestBody PageQuery pageQuery) {
public R<TableDataInfo<ChatMessageVo>> list(@RequestBody @Valid ChatMessageBo chatRequest, @RequestBody PageQuery pageQuery) {
// 默认查询当前登录用户消息记录
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null) {

View File

@@ -73,7 +73,6 @@ public class PayController {
return R.ok(paymentOrdersVo);
}
/**
* 跳转通知地址
*
@@ -138,6 +137,9 @@ public class PayController {
BeanUtil.copyProperties(paymentOrdersVo,paymentOrdersBo);
paymentOrdersService.updateByBo(paymentOrdersBo);
SysUserVo sysUserVo = userService.selectUserById(paymentOrdersVo.getUserId());
if(money>9.9){
money = money*2;
}
sysUserVo.setUserBalance(sysUserVo.getUserBalance()+money);
SysUserBo sysUserBo = new SysUserBo();
BeanUtil.copyProperties(sysUserVo,sysUserBo);

View File

@@ -0,0 +1,51 @@
package com.xmzs.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import com.xmzs.common.wechat.Wechat;
import com.xmzs.system.cofing.KeywordConfig;
import com.xmzs.system.cofing.QqConfig;
import com.xmzs.system.cofing.WechatConfig;
import com.xmzs.system.handler.WechatMessageHandler;
import com.xmzs.system.service.ISseService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 个人微信扩展控制器
*
* @author WangLe
*/
@SaIgnore
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
public class WeChatController {
@Getter
private Wechat wechatBot;
private final WechatConfig wechatConfig;
private final ISseService sseService;
private final KeywordConfig keywordConfig;
/**
* 获取微信登录二维码
*
*/
@PostMapping("/getQr")
public void getQr() {
//微信
if (wechatConfig.getEnable()){
log.info("正在登录微信,请按提示操作:");
wechatBot = new Wechat(new WechatMessageHandler(sseService, keywordConfig), wechatConfig.getQrPath());
wechatBot.start();
}
}
}

View File

@@ -51,7 +51,7 @@ spring:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://127.0.0.1:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: ry-vue
password: ry-vue
password: xxx
# 从库数据源
# slave:

View File

@@ -220,11 +220,11 @@ mail:
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
from: ageerle@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
user: ageerle@163.com
# 密码(填写授权码)
pass: pass
pass: TOGXBVPYFVPFRQMQ
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
@@ -318,14 +318,10 @@ websocket:
path: ''
# 设置访问源地址
allowedOrigins: '*'
# AI助手配置信息
# chatgpt配置信息
chat:
apiKey: ''
apiHost: ''
# 中转接口
transit:
apiKey: ''
apiHost: 'https://api.gptgod.online/'
apiKey: 'sk-uMCP3lTg1dQ9L7Xs2bF352Fa216a4c9280577b205dE67e12'
apiHost: 'https://api.pandarobot.chat/'
# 微信小程序配置信息
wx:
miniapp:
@@ -343,3 +339,37 @@ baidu:
apiKey: '' # apiKey
secretKey: '' # secretKey
wechat:
# 是否使用微信 true/false
enable: true
# 生成的登录二维码路径 默认与项目同级
qrPath: "./"
keyword:
# 重置会话指令
reset: "重置会话"
# ai画图指令(DALL·E模型 https://platform.openai.com/docs/models/dall-e)
# generation 根据关键词生成图片(https://platform.openai.com/docs/guides/images/generations)
image: "ai画图"
# ai语音指令(TTS模型 https://platform.openai.com/docs/api-reference/audio)
audio: "ai语音"
mj:
api-secret:
task-store:
type: in_memory
timeout: 30d
translate-way: gpt
# proxy:
# host: 127.0.0.1
# port: 10809
ng-discord:
server: https://discord.pandarobot.chat/
cdn: https://app.pandarobot.chat/
wss: https://gateway.pandarobot.chat/
openai:
gpt-api-url: 'https://api.pandarobot.chat/'
gpt-api-key: 'sk-xxx'
accounts:
- guild-id: 'xxxxx'
channel-id: 'xxxxx'
user-token: 'xx.xx'

View File

@@ -34,6 +34,7 @@
<module>ruoyi-common-tenant</module>
<module>ruoyi-common-chat</module>
<module>ruoyi-common-pay</module>
<module>ruoyi-common-wechat</module>
</modules>
<artifactId>ruoyi-common</artifactId>

View File

@@ -166,6 +166,13 @@
<version>${revision}</version>
</dependency>
<!-- 微信模块 -->
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-common-wechat</artifactId>
<version>${revision}</version>
</dependency>
<!-- 支付模块 -->
<dependency>
<groupId>com.xmzs</groupId>

View File

@@ -70,13 +70,6 @@
<version>0.5.0</version>
</dependency>
<!-- azure-ai -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-ai-openai</artifactId>
<version>1.0.0-beta.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>

View File

@@ -12,15 +12,17 @@ public class OpenAIConst {
public final static int SUCCEED_CODE = 200;
public final static double GPT3_COST = 0.03;
public final static double GPT3_COST = 0.05;
public final static double GPT4_COST = 0.3;
public final static double GPT4_ALL_COST = 0.3;
/** 绘图费用 */
public final static double DALL3_COST = 0.3;
public final static double DALL3_COST = 0.4;
/** 绘图费用-高清 */
public final static double DALL3_HD_COST = 0.6;
public final static double DALL3_HD_COST = 0.8;
/** mdjourney绘图费用 */
public final static double MJ_COST = 0.3;

View File

@@ -0,0 +1,15 @@
package com.xmzs.common.chat.domain.request;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* mj任务请求实体类
*
* @author WangLe
*/
@Data
public class MjTaskRequest {
private String prompt;
}

View File

@@ -0,0 +1,48 @@
package com.xmzs.common.chat.entity.Tts;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
@AllArgsConstructor
public class TextToSpeech {
@Builder.Default
private String model = Model.TTS_1.getName();
/**
* 音频声音源
*
* @see TtsVoice
*/
private String voice;
/**
* 输入内容
*/
private String input;
/**
* 输出音频文件格式
*
* @see TtsFormat
*/
@JsonProperty("response_format")
private String responseFormat;
/**
* 速度调节默认是1取值范围0.25——4.0
*/
private Double speed;
@Getter
@AllArgsConstructor
public enum Model {
TTS_1("tts-1"),
TTS_1_HD("tts-1-hd"),
;
private final String name;
}
}

View File

@@ -0,0 +1,15 @@
package com.xmzs.common.chat.entity.Tts;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum TtsFormat {
MP3("mp3"),
OPUS("opus"),
AAC("aac"),
FLAC("flac"),
;
private final String name;
}

View File

@@ -0,0 +1,23 @@
package com.xmzs.common.chat.entity.Tts;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 生成不同声音的音频
* <p>具体语音效果参考https://platform.openai.com/docs/guides/text-to-speech</p>
*/
@Getter
@AllArgsConstructor
public enum TtsVoice {
ALLOY("alloy"),
ECHO("echo"),
FABLE("fable"),
ONYX("onyx"),
NOVA("nova"),
SHIMMER("shimmer"),
;
private final String name;
}

View File

@@ -174,6 +174,10 @@ public class BaseChatCompletion implements Serializable {
* gpt-3.5-turbo-16k-0613 超长上下文 支持函数
*/
GPT_3_5_TURBO_16K_0613("gpt-3.5-turbo-16k-0613"),
/**
* gpt-3.5-turbo-0125 超长上下文 支持函数
*/
GPT_3_5_TURBO_0125("gpt-3.5-turbo-0125"),
/**
* GPT4.0
*/
@@ -209,6 +213,10 @@ public class BaseChatCompletion implements Serializable {
* 支持图片
*/
GPT_4_VISION_PREVIEW("gpt-4-vision-preview"),
/**
* gpt-4-0613支持函数
*/
GPT_4_0125_PREVIEW("gpt-4-0125-preview"),
;
private final String name;
}

View File

@@ -1,5 +1,6 @@
package com.xmzs.common.chat.openai;
import com.xmzs.common.chat.entity.Tts.TextToSpeech;
import com.xmzs.common.chat.entity.chat.ChatCompletionWithPicture;
import io.reactivex.Single;
import okhttp3.MultipartBody;
@@ -33,6 +34,7 @@ import com.xmzs.common.chat.entity.models.ModelResponse;
import com.xmzs.common.chat.entity.moderations.Moderation;
import com.xmzs.common.chat.entity.moderations.ModerationResponse;
import com.xmzs.common.chat.entity.whisper.WhisperResponse;
import retrofit2.Call;
import retrofit2.http.*;
import java.time.LocalDate;
@@ -340,4 +342,15 @@ public interface OpenAiApi {
*/
@POST("v1/chat/completions")
Single<ChatCompletionResponse> chatCompletionWithPicture(@Body ChatCompletionWithPicture chatCompletion);
/**
* 文本转语音
*
* @param textToSpeech 参数
* @return ResponseBody body
* @since 1.1.2
*/
@POST("v1/audio/speech")
@Streaming
Call<ResponseBody> textToSpeech(@Body TextToSpeech textToSpeech);
}

View File

@@ -668,7 +668,6 @@ public class OpenAiClient {
return this.chatCompletion(chatCompletion);
}
/**
* 语音翻译:目前仅支持翻译为英文
*

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xmzs.common.chat.config.LocalCache;
import com.xmzs.common.chat.entity.Tts.TextToSpeech;
import com.xmzs.common.chat.entity.billing.BillingUsage;
import com.xmzs.common.chat.entity.billing.KeyInfo;
import com.xmzs.common.chat.entity.billing.Subscription;
@@ -37,10 +38,12 @@ import com.xmzs.common.chat.entity.chat.ChatCompletion;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -344,6 +347,67 @@ public class OpenAiStreamClient {
Transcriptions transcriptions = Transcriptions.builder().build();
return this.speechToTextTranscriptions(file, transcriptions);
}
/**
* 文本转语音(异步)
*
* @param textToSpeech 参数
* @param callback 返回值接收
* @since 1.1.2
*/
public void textToSpeech(TextToSpeech textToSpeech, retrofit2.Callback callback) {
retrofit2.Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
responseBody.enqueue(callback);
}
/**
* 文本转语音(同步)
*
* @param textToSpeech 参数
* @since 1.1.3
*/
public ResponseBody textToSpeech(TextToSpeech textToSpeech){
Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
try {
return responseBody.execute().body();
} catch (IOException e) {
throw new BaseException("文本转语音(同步)失败: "+e.getMessage());
}
}
/**
* 文本转语音(克隆)
*
* @param textToSpeech
* @return
*/
public ResponseBody textToSpeechClone(TextToSpeech textToSpeech) {
String baseUrl = "http://localhost:8081";
String spk = "三月七";
String text = textToSpeech.getInput();
String lang = "zh";
// 创建OkHttpClient实例
OkHttpClient client = new OkHttpClient();
// 构建请求URL
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
urlBuilder.addQueryParameter("spk", spk);
urlBuilder.addQueryParameter("text", text);
urlBuilder.addQueryParameter("lang", lang);
String url = urlBuilder.build().toString();
// 创建请求对象
Request request = new Request.Builder()
.url(url)
.build();
// 发送请求并处理响应
try {
return client.newCall(request).execute().body();
} catch (IOException e) {
throw new BaseException("语音克隆失败!{}",e.getMessage());
}
}
/**
* 构造

View File

@@ -6,6 +6,7 @@ import com.knuddels.jtokkit.api.Encoding;
import com.knuddels.jtokkit.api.EncodingRegistry;
import com.knuddels.jtokkit.api.EncodingType;
import com.knuddels.jtokkit.api.ModelType;
import com.xmzs.common.chat.entity.chat.BaseChatCompletion;
import lombok.extern.slf4j.Slf4j;
import com.xmzs.common.chat.entity.chat.ChatCompletion;
@@ -39,11 +40,13 @@ public class TikTokensUtil {
modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_0125.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
modelMap.put(ChatCompletion.Model.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4));
modelMap.put(ChatCompletion.Model.GPT_4_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
modelMap.put(ChatCompletion.Model.GPT_4_32K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
modelMap.put(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
modelMap.put(ChatCompletion.Model.GPT_4_VISION_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
modelMap.put(ChatCompletion.Model.GPT_4_0125_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
}
/**

View File

@@ -10,28 +10,33 @@ public class PayConfig {
/**
* 商户ID
*/
public static String pid = "xx";
public static String pid = "xxx";
/**
* 接口地址
*/
public static String payUrl = "https://pay-cloud.vip/mapi.php";
/**
* 私钥
*/
public static String key = "xxx";
/**
* 服务器异步通知地址
*/
public static String notify_url = "https://www.pandarobot.chat/pay/returnUrl";
/**
* 页面跳转通知地址
*/
public static String return_url = "https://www.pandarobot.chat/pay/notifyUrl";
/**
* 支付方式
*/
public static String type = "wxpay";
/**
* 接口地址
*/
public static String payUrl = "https://pay.bluetuo.com/mapi.php";
/**
* 服务器异步通知地址
*/
public static String notify_url = "http://xx/pay/returnUrl";
/**
* 页面跳转通知地址
*/
public static String return_url = "http://xx/pay/notifyUrl";
/**
* 设备类型
*/
@@ -40,12 +45,6 @@ public class PayConfig {
/**
* 加密方式默认MD5
*/
public static String sign_type = "MD5";
/**
* 私钥
*/
public static String key = "xx";
}

View File

@@ -19,5 +19,4 @@ public interface PayService {
* @return String
**/
String getPayUrl(String orderNo, String name, double money, String clientIp);
}

View File

@@ -40,5 +40,4 @@ public class PayServiceImpl implements PayService {
JSONObject jsonObject = new JSONObject(body);
return (String) jsonObject.get("qrcode");
}
}

View File

@@ -30,8 +30,10 @@ public class AllUrlHandler implements InitializingBean {
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(info -> {
// 获取注解上边的 path 替代 path variable 为 *
Objects.requireNonNull(info.getPathPatternsCondition().getPatterns())
if(info.getPathPatternsCondition()!=null){
Objects.requireNonNull(info.getPathPatternsCondition().getPatterns())
.forEach(url -> set.add(ReUtil.replaceAll(url.getPatternString(), PATTERN, "*")));
}
});
urls.addAll(set);
}

View File

@@ -0,0 +1,51 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-wechat</artifactId>
<description>
ruoyi-common-wechat 微信服务
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- emoji -->
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>3.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- qq -->
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-jvm</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,34 @@
package com.xmzs.common.wechat;
import com.xmzs.common.wechat.controller.LoginController;
import com.xmzs.common.wechat.core.MsgCenter;
import com.xmzs.common.wechat.face.IMsgHandlerFace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Wechat {
private static final Logger LOG = LoggerFactory.getLogger(Wechat.class);
private IMsgHandlerFace msgHandler;
public Wechat(IMsgHandlerFace msgHandler, String qrPath) {
System.setProperty("jsse.enableSNIExtension", "false"); // 防止SSL错误
this.msgHandler = msgHandler;
// 登陆
LoginController login = new LoginController();
login.login(qrPath);
}
public void start() {
LOG.info("+++++++++++++++++++开始消息处理+++++++++++++++++++++");
new Thread(new Runnable() {
@Override
public void run() {
MsgCenter.handleMsg(msgHandler);
}
}).start();
}
}

View File

@@ -0,0 +1,43 @@
package com.xmzs.common.wechat.api;
import java.io.File;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 辅助工具类,该类暂时未用,请忽略
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月22日 下午10:34:46
* @version 1.0
*
*/
public class AssistTools {
private static OkHttpClient client = new OkHttpClient();
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
public static boolean sendQrPicToServer(String username, String password, String uploadUrl, String localPath)
throws IOException {
File file = new File(localPath);
RequestBody requestBody = new MultipartBody.Builder().addFormDataPart("username", username)
.addFormDataPart("password", password)
.addFormDataPart("file", file.getName(), RequestBody.create(MEDIA_TYPE_PNG, file)).build();
Request request = new Request.Builder().url(uploadUrl).post(requestBody).build();
Call call = client.newCall(request);
try {
Response response = call.execute();
System.out.println(response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
}

View File

@@ -0,0 +1,424 @@
package com.xmzs.common.wechat.api;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.activation.MimetypesFileTypeMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xmzs.common.wechat.beans.BaseMsg;
import com.xmzs.common.wechat.beans.RecommendInfo;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.utils.Config;
import com.xmzs.common.wechat.utils.MyHttpClient;
import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
import com.xmzs.common.wechat.utils.enums.URLEnum;
import com.xmzs.common.wechat.utils.enums.VerifyFriendEnum;
/**
* 消息处理类
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月23日 下午2:30:37
* @version 1.0
*
*/
public class MessageTools {
private static Logger LOG = LoggerFactory.getLogger(MessageTools.class);
private static Core core = Core.getInstance();
private static MyHttpClient myHttpClient = core.getMyHttpClient();
/**
* 根据UserName发送文本消息
*
* @author https://github.com/yaphone
* @date 2017年5月4日 下午11:17:38
* @param msg
* @param toUserName
*/
private static void sendMsg(String text, String toUserName) {
if (text == null) {
return;
}
LOG.info(String.format("发送消息 %s: %s", toUserName, text));
webWxSendMsg(1, text, toUserName);
}
/**
* 根据ID发送文本消息
*
* @author https://github.com/yaphone
* @date 2017年5月6日 上午11:45:51
* @param text
* @param id
*/
public static void sendMsgById(String text, String id) {
if (text == null) {
return;
}
sendMsg(text, id);
}
/**
* 根据NickName发送文本消息
*
* @author https://github.com/yaphone
* @date 2017年5月4日 下午11:17:38
* @param text
* @param nickName
*/
public static boolean sendMsgByNickName(String text, String nickName) {
if (nickName != null) {
String toUserName = WechatTools.getUserNameByNickName(nickName);
if (toUserName != null) {
webWxSendMsg(1, text, toUserName);
return true;
}
}
return false;
}
/**
* 消息发送
*
* @author https://github.com/yaphone
* @date 2017年4月23日 下午2:32:02
* @param msgType
* @param content
* @param toUserName
*/
public static void webWxSendMsg(int msgType, String content, String toUserName) {
String url = String.format(URLEnum.WEB_WX_SEND_MSG.getUrl(), core.getLoginInfo().get("url"));
Map<String, Object> msgMap = new HashMap<String, Object>();
msgMap.put("Type", msgType);
msgMap.put("Content", content);
msgMap.put("FromUserName", core.getUserName());
msgMap.put("ToUserName", toUserName == null ? core.getUserName() : toUserName);
msgMap.put("LocalID", new Date().getTime() * 10);
msgMap.put("ClientMsgId", new Date().getTime() * 10);
Map<String, Object> paramMap = core.getParamMap();
paramMap.put("Msg", msgMap);
paramMap.put("Scene", 0);
try {
String paramStr = JSON.toJSONString(paramMap);
HttpEntity entity = myHttpClient.doPost(url, paramStr);
EntityUtils.toString(entity, Consts.UTF_8);
} catch (Exception e) {
LOG.error("webWxSendMsg", e);
}
}
/**
* 上传多媒体文件到 微信服务器目前应该支持3种类型: 1. pic 直接显示,包含图片,表情 2.video 3.doc 显示为文件包含PDF等
*
* @author https://github.com/yaphone
* @date 2017年5月7日 上午12:41:13
* @param filePath
* @return
*/
private static JSONObject webWxUploadMedia(String filePath) {
File file = new File(filePath);
if (!file.exists() && file.isFile()) {
LOG.info("file is not exist");
return null;
}
String url = String.format(URLEnum.WEB_WX_UPLOAD_MEDIA.getUrl(), core.getLoginInfo().get("fileUrl"));
String mimeType = new MimetypesFileTypeMap().getContentType(file);
String mediaType = "";
if (mimeType == null) {
mimeType = "text/plain";
} else {
mediaType = mimeType.split("/")[0].equals("image") ? "pic" : "doc";
}
String lastModifieDate = new SimpleDateFormat("yyyy MM dd HH:mm:ss").format(new Date());
long fileSize = file.length();
String passTicket = (String) core.getLoginInfo().get("pass_ticket");
String clientMediaId = new Date().getTime()
+ String.valueOf(new Random().nextLong()).substring(0, 4);
String webwxDataTicket = MyHttpClient.getCookie("webwx_data_ticket");
if (webwxDataTicket == null) {
LOG.error("get cookie webwx_data_ticket error");
return null;
}
Map<String, Object> paramMap = core.getParamMap();
paramMap.put("ClientMediaId", clientMediaId);
paramMap.put("TotalLen", fileSize);
paramMap.put("StartPos", 0);
paramMap.put("DataLen", fileSize);
paramMap.put("MediaType", 4);
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
builder.addTextBody("id", "WU_FILE_0", ContentType.TEXT_PLAIN);
builder.addTextBody("name", filePath, ContentType.TEXT_PLAIN);
builder.addTextBody("type", mimeType, ContentType.TEXT_PLAIN);
builder.addTextBody("lastModifieDate", lastModifieDate, ContentType.TEXT_PLAIN);
builder.addTextBody("size", String.valueOf(fileSize), ContentType.TEXT_PLAIN);
builder.addTextBody("mediatype", mediaType, ContentType.TEXT_PLAIN);
builder.addTextBody("uploadmediarequest", JSON.toJSONString(paramMap), ContentType.TEXT_PLAIN);
builder.addTextBody("webwx_data_ticket", webwxDataTicket, ContentType.TEXT_PLAIN);
builder.addTextBody("pass_ticket", passTicket, ContentType.TEXT_PLAIN);
builder.addBinaryBody("filename", file, ContentType.create(mimeType), filePath);
HttpEntity reqEntity = builder.build();
HttpEntity entity = myHttpClient.doPostFile(url, reqEntity);
if (entity != null) {
try {
String result = EntityUtils.toString(entity, Consts.UTF_8);
return JSON.parseObject(result);
} catch (Exception e) {
LOG.error("webWxUploadMedia 错误: ", e);
}
}
return null;
}
/**
* 根据NickName发送图片消息
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午10:32:45
* @param nackName
* @return
*/
public static boolean sendPicMsgByNickName(String nickName, String filePath) {
String toUserName = WechatTools.getUserNameByNickName(nickName);
if (toUserName != null) {
return sendPicMsgByUserId(toUserName, filePath);
}
return false;
}
/**
* 根据用户id发送图片消息
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午10:34:24
* @param userId
* @param filePath
* @return
*/
public static boolean sendPicMsgByUserId(String userId, String filePath) {
JSONObject responseObj = webWxUploadMedia(filePath);
if (responseObj != null) {
String mediaId = responseObj.getString("MediaId");
if (mediaId != null) {
return webWxSendMsgImg(userId, mediaId);
}
}
return false;
}
/**
* 发送图片消息,内部调用
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午10:38:55
* @return
*/
private static boolean webWxSendMsgImg(String userId, String mediaId) {
String url = String.format("%s/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s", core.getLoginInfo().get("url"),
core.getLoginInfo().get("pass_ticket"));
Map<String, Object> msgMap = new HashMap<String, Object>();
msgMap.put("Type", 3);
msgMap.put("MediaId", mediaId);
msgMap.put("FromUserName", core.getUserSelf().getString("UserName"));
msgMap.put("ToUserName", userId);
String clientMsgId = String.valueOf(new Date().getTime())
+ String.valueOf(new Random().nextLong()).substring(1, 5);
msgMap.put("LocalID", clientMsgId);
msgMap.put("ClientMsgId", clientMsgId);
Map<String, Object> paramMap = core.getParamMap();
paramMap.put("BaseRequest", core.getParamMap().get("BaseRequest"));
paramMap.put("Msg", msgMap);
String paramStr = JSON.toJSONString(paramMap);
HttpEntity entity = myHttpClient.doPost(url, paramStr);
if (entity != null) {
try {
String result = EntityUtils.toString(entity, Consts.UTF_8);
return JSON.parseObject(result).getJSONObject("BaseResponse").getInteger("Ret") == 0;
} catch (Exception e) {
LOG.error("webWxSendMsgImg 错误: ", e);
}
}
return false;
}
/**
* 根据用户id发送文件
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午11:57:36
* @param userId
* @param filePath
* @return
*/
public static boolean sendFileMsgByUserId(String userId, String filePath) {
String title = new File(filePath).getName();
Map<String, String> data = new HashMap<String, String>();
data.put("appid", Config.API_WXAPPID);
data.put("title", title);
data.put("totallen", "");
data.put("attachid", "");
data.put("type", "6"); // APPMSGTYPE_ATTACH
data.put("fileext", title.split("\\.")[1]); // 文件后缀
JSONObject responseObj = webWxUploadMedia(filePath);
if (responseObj != null) {
data.put("totallen", responseObj.getString("StartPos"));
data.put("attachid", responseObj.getString("MediaId"));
} else {
LOG.error("sednFileMsgByUserId 错误: ", data);
}
return webWxSendAppMsg(userId, data);
}
/**
* 根据用户昵称发送文件消息
*
* @author https://github.com/yaphone
* @date 2017年5月10日 下午10:59:27
* @param nickName
* @param filePath
* @return
*/
public static boolean sendFileMsgByNickName(String nickName, String filePath) {
String toUserName = WechatTools.getUserNameByNickName(nickName);
if (toUserName != null) {
return sendFileMsgByUserId(toUserName, filePath);
}
return false;
}
/**
* 内部调用
*
* @author https://github.com/yaphone
* @date 2017年5月10日 上午12:21:28
* @param userId
* @param data
* @return
*/
private static boolean webWxSendAppMsg(String userId, Map<String, String> data) {
String url = String.format("%s/webwxsendappmsg?fun=async&f=json&pass_ticket=%s", core.getLoginInfo().get("url"),
core.getLoginInfo().get("pass_ticket"));
String clientMsgId = String.valueOf(new Date().getTime())
+ String.valueOf(new Random().nextLong()).substring(1, 5);
String content = "<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>" + data.get("title")
+ "</title><des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>"
+ "<appattach><totallen>" + data.get("totallen") + "</totallen><attachid>" + data.get("attachid")
+ "</attachid><fileext>" + data.get("fileext") + "</fileext></appattach><extinfo></extinfo></appmsg>";
Map<String, Object> msgMap = new HashMap<String, Object>();
msgMap.put("Type", data.get("type"));
msgMap.put("Content", content);
msgMap.put("FromUserName", core.getUserSelf().getString("UserName"));
msgMap.put("ToUserName", userId);
msgMap.put("LocalID", clientMsgId);
msgMap.put("ClientMsgId", clientMsgId);
/*
* Map<String, Object> paramMap = new HashMap<String, Object>();
*
* @SuppressWarnings("unchecked") Map<String, Map<String, String>>
* baseRequestMap = (Map<String, Map<String, String>>)
* core.getLoginInfo() .get("baseRequest"); paramMap.put("BaseRequest",
* baseRequestMap.get("BaseRequest"));
*/
Map<String, Object> paramMap = core.getParamMap();
paramMap.put("Msg", msgMap);
paramMap.put("Scene", 0);
String paramStr = JSON.toJSONString(paramMap);
HttpEntity entity = myHttpClient.doPost(url, paramStr);
if (entity != null) {
try {
String result = EntityUtils.toString(entity, Consts.UTF_8);
return JSON.parseObject(result).getJSONObject("BaseResponse").getInteger("Ret") == 0;
} catch (Exception e) {
LOG.error("错误: ", e);
}
}
return false;
}
/**
* 被动添加好友
*
* @date 2017年6月29日 下午10:08:43
* @param msg
* @param accept
* true 接受 false 拒绝
*/
public static void addFriend(BaseMsg msg, boolean accept) {
if (!accept) { // 不添加
return;
}
int status = VerifyFriendEnum.ACCEPT.getCode(); // 接受好友请求
RecommendInfo recommendInfo = msg.getRecommendInfo();
String userName = recommendInfo.getUserName();
String ticket = recommendInfo.getTicket();
// 更新好友列表
// TODO 此处需要更新好友列表
// core.getContactList().add(msg.getJSONObject("RecommendInfo"));
String url = String.format(URLEnum.WEB_WX_VERIFYUSER.getUrl(), core.getLoginInfo().get("url"),
String.valueOf(System.currentTimeMillis() / 3158L), core.getLoginInfo().get("pass_ticket"));
List<Map<String, Object>> verifyUserList = new ArrayList<Map<String, Object>>();
Map<String, Object> verifyUser = new HashMap<String, Object>();
verifyUser.put("Value", userName);
verifyUser.put("VerifyUserTicket", ticket);
verifyUserList.add(verifyUser);
List<Integer> sceneList = new ArrayList<Integer>();
sceneList.add(33);
JSONObject body = new JSONObject();
body.put("BaseRequest", core.getParamMap().get("BaseRequest"));
body.put("Opcode", status);
body.put("VerifyUserListSize", 1);
body.put("VerifyUserList", verifyUserList);
body.put("VerifyContent", "");
body.put("SceneListCount", 1);
body.put("SceneList", sceneList);
body.put("skey", core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()));
String result = null;
try {
String paramStr = JSON.toJSONString(body);
HttpEntity entity = myHttpClient.doPost(url, paramStr);
result = EntityUtils.toString(entity, Consts.UTF_8);
} catch (Exception e) {
LOG.error("webWxSendMsg", e);
}
if (StringUtils.isBlank(result)) {
LOG.error("被动添加好友失败");
}
LOG.debug(result);
}
}

View File

@@ -0,0 +1,215 @@
package com.xmzs.common.wechat.api;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
import com.xmzs.common.wechat.utils.enums.URLEnum;
/**
* 微信小工具,如获好友列表等
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月4日 下午10:49:16
* @version 1.0
*
*/
public class WechatTools {
private static Logger LOG = LoggerFactory.getLogger(WechatTools.class);
private static Core core = Core.getInstance();
/**
* 根据用户名发送文本消息
*
* @author https://github.com/yaphone
* @date 2017年5月4日 下午10:43:14
* @param msg
* @param toUserName
*/
public static void sendMsgByUserName(String msg, String toUserName) {
MessageTools.sendMsgById(msg, toUserName);
}
/**
* <p>
* 通过RealName获取本次UserName
* </p>
* <p>
* 如NickName为"yaphone"则获取UserName=
* "@1212d3356aea8285e5bbe7b91229936bc183780a8ffa469f2d638bf0d2e4fc63"
* 可通过UserName发送消息
* </p>
*
* @author https://github.com/yaphone
* @date 2017年5月4日 下午10:56:31
* @param name
* @return
*/
public static String getUserNameByNickName(String nickName) {
for (JSONObject o : core.getContactList()) {
if (o.getString("NickName").equals(nickName)) {
return o.getString("UserName");
}
}
return null;
}
/**
* 返回好友昵称列表
*
* @author https://github.com/yaphone
* @date 2017年5月4日 下午11:37:20
* @return
*/
public static List<String> getContactNickNameList() {
List<String> contactNickNameList = new ArrayList<String>();
for (JSONObject o : core.getContactList()) {
contactNickNameList.add(o.getString("NickName"));
}
return contactNickNameList;
}
/**
* 返回好友完整信息列表
*
* @date 2017年6月26日 下午9:45:39
* @return
*/
public static List<JSONObject> getContactList() {
return core.getContactList();
}
/**
* 返回群列表
*
* @author https://github.com/yaphone
* @date 2017年5月5日 下午9:55:21
* @return
*/
public static List<JSONObject> getGroupList() {
return core.getGroupList();
}
/**
* 获取群ID列表
*
* @date 2017年6月21日 下午11:42:56
* @return
*/
public static List<String> getGroupIdList() {
return core.getGroupIdList();
}
/**
* 获取群NickName列表
*
* @date 2017年6月21日 下午11:43:38
* @return
*/
public static List<String> getGroupNickNameList() {
return core.getGroupNickNameList();
}
/**
* 根据groupIdList返回群成员列表
*
* @date 2017年6月13日 下午11:12:31
* @param groupId
* @return
*/
public static JSONArray getMemberListByGroupId(String groupId) {
return core.getGroupMemeberMap().get(groupId);
}
/**
* 退出微信
*
* @author https://github.com/yaphone
* @date 2017年5月18日 下午11:56:54
*/
public static void logout() {
webWxLogout();
}
private static boolean webWxLogout() {
String url = String.format(URLEnum.WEB_WX_LOGOUT.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()));
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
params.add(new BasicNameValuePair("redirect", "1"));
params.add(new BasicNameValuePair("type", "1"));
params.add(
new BasicNameValuePair("skey", (String) core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey())));
try {
HttpEntity entity = core.getMyHttpClient().doGet(url, params, false, null);
String text = EntityUtils.toString(entity, Consts.UTF_8); // 无消息
return true;
} catch (Exception e) {
LOG.debug(e.getMessage());
}
return false;
}
public static void setUserInfo() {
for (JSONObject o : core.getContactList()) {
core.getUserInfoMap().put(o.getString("NickName"), o);
core.getUserInfoMap().put(o.getString("UserName"), o);
}
}
/**
*
* 根据用户昵称设置备注名称
*
* @date 2017年5月27日 上午12:21:40
* @param userName
* @param remName
*/
public static void remarkNameByNickName(String nickName, String remName) {
String url = String.format(URLEnum.WEB_WX_REMARKNAME.getUrl(), core.getLoginInfo().get("url"),
core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
Map<String, Object> msgMap = new HashMap<String, Object>();
Map<String, Object> msgMap_BaseRequest = new HashMap<String, Object>();
msgMap.put("CmdId", 2);
msgMap.put("RemarkName", remName);
msgMap.put("UserName", core.getUserInfoMap().get(nickName).get("UserName"));
msgMap_BaseRequest.put("Uin", core.getLoginInfo().get(StorageLoginInfoEnum.wxuin.getKey()));
msgMap_BaseRequest.put("Sid", core.getLoginInfo().get(StorageLoginInfoEnum.wxsid.getKey()));
msgMap_BaseRequest.put("Skey", core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()));
msgMap_BaseRequest.put("DeviceID", core.getLoginInfo().get(StorageLoginInfoEnum.deviceid.getKey()));
msgMap.put("BaseRequest", msgMap_BaseRequest);
try {
String paramStr = JSON.toJSONString(msgMap);
HttpEntity entity = core.getMyHttpClient().doPost(url, paramStr);
// String result = EntityUtils.toString(entity, Consts.UTF_8);
LOG.info("修改备注" + remName);
} catch (Exception e) {
LOG.error("remarkNameByUserName", e);
}
}
/**
* 获取微信在线状态
*
* @date 2017年6月16日 上午12:47:46
* @return
*/
public static boolean getWechatStatus() {
return core.isAlive();
}
}

View File

@@ -0,0 +1,37 @@
package com.xmzs.common.wechat.beans;
import java.io.Serializable;
/**
* AppInfo
*
* @author https://github.com/yaphone
* @date 创建时间2017年7月3日 下午10:38:14
* @version 1.0
*
*/
public class AppInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private int type;
private String appId;
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,293 @@
package com.xmzs.common.wechat.beans;
import java.io.Serializable;
/**
* 收到的微信消息
*
* @author https://github.com/yaphone
* @date 创建时间2017年7月3日 下午10:28:06
* @version 1.0
*
*/
public class BaseMsg implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private int subMsgType;
private int voiceLength;
private String fileName;
private int imgHeight;
private String toUserName;
private int hasProductId;
private int imgStatus;
private String url;
private int imgWidth;
private int forwardFlag;
private int status;
private String Ticket;
/** 推荐消息报文 **/
private RecommendInfo recommendInfo;
private long createTime;
private String newMsgId;
/** 文本消息内容 **/
private String text;
/** 消息类型 **/
private int msgType;
/** 是否为群消息 **/
private boolean groupMsg;
private String msgId;
private int statusNotifyCode;
private AppInfo appInfo;
private int appMsgType;
private String Type;
private int playLength;
private String mediaId;
private String content;
private String statusNotifyUserName;
/** 消息发送者ID **/
private String fromUserName;
private String oriContent;
private String fileSize;
public int getSubMsgType() {
return subMsgType;
}
public void setSubMsgType(int subMsgType) {
this.subMsgType = subMsgType;
}
public int getVoiceLength() {
return voiceLength;
}
public void setVoiceLength(int voiceLength) {
this.voiceLength = voiceLength;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public int getImgHeight() {
return imgHeight;
}
public void setImgHeight(int imgHeight) {
this.imgHeight = imgHeight;
}
public String getToUserName() {
return toUserName;
}
public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}
public int getHasProductId() {
return hasProductId;
}
public void setHasProductId(int hasProductId) {
this.hasProductId = hasProductId;
}
public int getImgStatus() {
return imgStatus;
}
public void setImgStatus(int imgStatus) {
this.imgStatus = imgStatus;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getImgWidth() {
return imgWidth;
}
public void setImgWidth(int imgWidth) {
this.imgWidth = imgWidth;
}
public int getForwardFlag() {
return forwardFlag;
}
public void setForwardFlag(int forwardFlag) {
this.forwardFlag = forwardFlag;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getTicket() {
return Ticket;
}
public void setTicket(String ticket) {
Ticket = ticket;
}
public RecommendInfo getRecommendInfo() {
return recommendInfo;
}
public void setRecommendInfo(RecommendInfo recommendInfo) {
this.recommendInfo = recommendInfo;
}
public long getCreateTime() {
return createTime;
}
public void setCreateTime(long createTime) {
this.createTime = createTime;
}
public String getNewMsgId() {
return newMsgId;
}
public void setNewMsgId(String newMsgId) {
this.newMsgId = newMsgId;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getMsgType() {
return msgType;
}
public void setMsgType(int msgType) {
this.msgType = msgType;
}
public boolean isGroupMsg() {
return groupMsg;
}
public void setGroupMsg(boolean groupMsg) {
this.groupMsg = groupMsg;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
public int getStatusNotifyCode() {
return statusNotifyCode;
}
public void setStatusNotifyCode(int statusNotifyCode) {
this.statusNotifyCode = statusNotifyCode;
}
public AppInfo getAppInfo() {
return appInfo;
}
public void setAppInfo(AppInfo appInfo) {
this.appInfo = appInfo;
}
public int getAppMsgType() {
return appMsgType;
}
public void setAppMsgType(int appMsgType) {
this.appMsgType = appMsgType;
}
public String getType() {
return Type;
}
public void setType(String type) {
Type = type;
}
public int getPlayLength() {
return playLength;
}
public void setPlayLength(int playLength) {
this.playLength = playLength;
}
public String getMediaId() {
return mediaId;
}
public void setMediaId(String mediaId) {
this.mediaId = mediaId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getStatusNotifyUserName() {
return statusNotifyUserName;
}
public void setStatusNotifyUserName(String statusNotifyUserName) {
this.statusNotifyUserName = statusNotifyUserName;
}
public String getFromUserName() {
return fromUserName;
}
public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}
public String getOriContent() {
return oriContent;
}
public void setOriContent(String oriContent) {
this.oriContent = oriContent;
}
public String getFileSize() {
return fileSize;
}
public void setFileSize(String fileSize) {
this.fileSize = fileSize;
}
}

View File

@@ -0,0 +1,146 @@
package com.xmzs.common.wechat.beans;
import java.io.Serializable;
/**
* RecommendInfo
*
* @author https://github.com/yaphone
* @date 创建时间2017年7月3日 下午10:35:14
* @version 1.0
*
*/
public class RecommendInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String ticket;
private String userName;
private int sex;
private int attrStatus;
private String city;
private String nickName;
private int scene;
private String province;
private String content;
private String alias;
private String signature;
private int opCode;
private int qQNum;
private int verifyFlag;
public String getTicket() {
return ticket;
}
public void setTicket(String ticket) {
this.ticket = ticket;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public int getAttrStatus() {
return attrStatus;
}
public void setAttrStatus(int attrStatus) {
this.attrStatus = attrStatus;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getScene() {
return scene;
}
public void setScene(int scene) {
this.scene = scene;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public int getOpCode() {
return opCode;
}
public void setOpCode(int opCode) {
this.opCode = opCode;
}
public int getqQNum() {
return qQNum;
}
public void setqQNum(int qQNum) {
this.qQNum = qQNum;
}
public int getVerifyFlag() {
return verifyFlag;
}
public void setVerifyFlag(int verifyFlag) {
this.verifyFlag = verifyFlag;
}
}

View File

@@ -0,0 +1,90 @@
package com.xmzs.common.wechat.controller;
import com.xmzs.common.wechat.utils.SleepUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xmzs.common.wechat.api.WechatTools;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.service.ILoginService;
import com.xmzs.common.wechat.service.impl.LoginServiceImpl;
import com.xmzs.common.wechat.thread.CheckLoginStatusThread;
import com.xmzs.common.wechat.utils.tools.CommonTools;
/**
* 登陆控制器
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月13日 下午12:56:07
* @version 1.0
*
*/
public class LoginController {
private static Logger LOG = LoggerFactory.getLogger(LoginController.class);
private ILoginService loginService = new LoginServiceImpl();
private static Core core = Core.getInstance();
public void login(String qrPath) {
if (core.isAlive()) { // 已登陆
LOG.info("itchat4j已登陆");
return;
}
while (true) {
for (int count = 0; count < 10; count++) {
LOG.info("获取UUID");
while (loginService.getUuid() == null) {
LOG.info("1. 获取微信UUID");
while (loginService.getUuid() == null) {
LOG.warn("1.1. 获取微信UUID失败两秒后重新获取");
SleepUtils.sleep(2000);
}
}
LOG.info("2. 获取登陆二维码图片");
if (loginService.getQR(qrPath)) {
break;
} else if (count == 10) {
LOG.error("2.2. 获取登陆二维码图片失败,系统退出");
System.exit(0);
}
}
LOG.info("3. 请扫描二维码图片,并在手机上确认");
if (!core.isAlive()) {
loginService.login();
core.setAlive(true);
LOG.info(("登陆成功"));
break;
}
LOG.info("4. 登陆超时,请重新扫描二维码图片");
}
LOG.info("5. 登陆成功,微信初始化");
if (!loginService.webWxInit()) {
LOG.info("6. 微信初始化异常");
System.exit(0);
}
LOG.info("6. 开启微信状态通知");
loginService.wxStatusNotify();
LOG.info("7. 清除。。。。");
CommonTools.clearScreen();
LOG.info(String.format("欢迎回来, %s", core.getNickName()));
LOG.info("8. 开始接收消息");
loginService.startReceiving();
LOG.info("9. 获取联系人信息");
loginService.webWxGetContact();
LOG.info("10. 获取群好友及群好友列表");
loginService.WebWxBatchGetContact();
LOG.info("11. 缓存本次登陆好友相关消息");
WechatTools.setUserInfo(); // 登陆成功后缓存本次登陆好友相关消息NickName, UserName
LOG.info("12.开启微信状态检测线程");
new Thread(new CheckLoginStatusThread()).start();
}
}

View File

@@ -0,0 +1,276 @@
package com.xmzs.common.wechat.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xmzs.common.wechat.beans.BaseMsg;
import com.xmzs.common.wechat.utils.MyHttpClient;
import com.xmzs.common.wechat.utils.enums.parameters.BaseParaEnum;
/**
* 核心存储类,全局只保存一份,单例模式
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月23日 下午2:33:56
* @version 1.0
*
*/
public class Core {
private static Core instance;
private Core() {
}
public static Core getInstance() {
if (instance == null) {
synchronized (Core.class) {
instance = new Core();
}
}
return instance;
}
boolean alive = false;
private int memberCount = 0;
private String indexUrl;
private String userName;
private String nickName;
private List<BaseMsg> msgList = new ArrayList<BaseMsg>();
private JSONObject userSelf; // 登陆账号自身信息
private List<JSONObject> memberList = new ArrayList<JSONObject>(); // 好友+群聊+公众号+特殊账号
private List<JSONObject> contactList = new ArrayList<JSONObject>();// 好友
private List<JSONObject> groupList = new ArrayList<JSONObject>();; // 群
private Map<String, JSONArray> groupMemeberMap = new HashMap<String, JSONArray>(); // 群聊成员字典
private List<JSONObject> publicUsersList = new ArrayList<JSONObject>();;// 公众号/服务号
private List<JSONObject> specialUsersList = new ArrayList<JSONObject>();;// 特殊账号
private List<String> groupIdList = new ArrayList<String>(); // 群ID列表
private List<String> groupNickNameList = new ArrayList<String>(); // 群NickName列表
private Map<String, JSONObject> userInfoMap = new HashMap<String, JSONObject>();
Map<String, Object> loginInfo = new HashMap<String, Object>();
// CloseableHttpClient httpClient = HttpClients.createDefault();
MyHttpClient myHttpClient = MyHttpClient.getInstance();
String uuid = null;
boolean useHotReload = false;
String hotReloadDir = "itchat.pkl";
int receivingRetryCount = 5;
private long lastNormalRetcodeTime; // 最后一次收到正常retcode的时间秒为单位
/**
* 请求参数
*/
public Map<String, Object> getParamMap() {
return new HashMap<String, Object>(1) {
/**
*
*/
private static final long serialVersionUID = 1L;
{
Map<String, String> map = new HashMap<String, String>();
for (BaseParaEnum baseRequest : BaseParaEnum.values()) {
map.put(baseRequest.para(), getLoginInfo().get(baseRequest.value()).toString());
}
put("BaseRequest", map);
}
};
}
public boolean isAlive() {
return alive;
}
public void setAlive(boolean alive) {
this.alive = alive;
}
public List<JSONObject> getMemberList() {
return memberList;
}
public void setMemberList(List<JSONObject> memberList) {
this.memberList = memberList;
}
public Map<String, Object> getLoginInfo() {
return loginInfo;
}
public void setLoginInfo(Map<String, Object> loginInfo) {
this.loginInfo = loginInfo;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public int getMemberCount() {
return memberCount;
}
public void setMemberCount(int memberCount) {
this.memberCount = memberCount;
}
public boolean isUseHotReload() {
return useHotReload;
}
public void setUseHotReload(boolean useHotReload) {
this.useHotReload = useHotReload;
}
public String getHotReloadDir() {
return hotReloadDir;
}
public void setHotReloadDir(String hotReloadDir) {
this.hotReloadDir = hotReloadDir;
}
public int getReceivingRetryCount() {
return receivingRetryCount;
}
public void setReceivingRetryCount(int receivingRetryCount) {
this.receivingRetryCount = receivingRetryCount;
}
public MyHttpClient getMyHttpClient() {
return myHttpClient;
}
public List<BaseMsg> getMsgList() {
return msgList;
}
public void setMsgList(List<BaseMsg> msgList) {
this.msgList = msgList;
}
public void setMyHttpClient(MyHttpClient myHttpClient) {
this.myHttpClient = myHttpClient;
}
public List<String> getGroupIdList() {
return groupIdList;
}
public void setGroupIdList(List<String> groupIdList) {
this.groupIdList = groupIdList;
}
public List<JSONObject> getContactList() {
return contactList;
}
public void setContactList(List<JSONObject> contactList) {
this.contactList = contactList;
}
public List<JSONObject> getGroupList() {
return groupList;
}
public void setGroupList(List<JSONObject> groupList) {
this.groupList = groupList;
}
public List<JSONObject> getPublicUsersList() {
return publicUsersList;
}
public void setPublicUsersList(List<JSONObject> publicUsersList) {
this.publicUsersList = publicUsersList;
}
public List<JSONObject> getSpecialUsersList() {
return specialUsersList;
}
public void setSpecialUsersList(List<JSONObject> specialUsersList) {
this.specialUsersList = specialUsersList;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public JSONObject getUserSelf() {
return userSelf;
}
public void setUserSelf(JSONObject userSelf) {
this.userSelf = userSelf;
}
public Map<String, JSONObject> getUserInfoMap() {
return userInfoMap;
}
public void setUserInfoMap(Map<String, JSONObject> userInfoMap) {
this.userInfoMap = userInfoMap;
}
public synchronized long getLastNormalRetcodeTime() {
return lastNormalRetcodeTime;
}
public synchronized void setLastNormalRetcodeTime(long lastNormalRetcodeTime) {
this.lastNormalRetcodeTime = lastNormalRetcodeTime;
}
public List<String> getGroupNickNameList() {
return groupNickNameList;
}
public void setGroupNickNameList(List<String> groupNickNameList) {
this.groupNickNameList = groupNickNameList;
}
public Map<String, JSONArray> getGroupMemeberMap() {
return groupMemeberMap;
}
public void setGroupMemeberMap(Map<String, JSONArray> groupMemeberMap) {
this.groupMemeberMap = groupMemeberMap;
}
public String getIndexUrl() {
return indexUrl;
}
public void setIndexUrl(String indexUrl) {
this.indexUrl = indexUrl;
}
}

View File

@@ -0,0 +1,171 @@
package com.xmzs.common.wechat.core;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import com.xmzs.common.wechat.utils.enums.MsgCodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xmzs.common.wechat.api.MessageTools;
import com.xmzs.common.wechat.beans.BaseMsg;
import com.xmzs.common.wechat.face.IMsgHandlerFace;
import com.xmzs.common.wechat.utils.enums.MsgTypeEnum;
import com.xmzs.common.wechat.utils.tools.CommonTools;
/**
* 消息处理中心
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月14日 下午12:47:50
* @version 1.0
*
*/
public class MsgCenter {
private static Logger LOG = LoggerFactory.getLogger(MsgCenter.class);
private static Core core = Core.getInstance();
/**
* 接收消息,放入队列
*
* @author https://github.com/yaphone
* @date 2017年4月23日 下午2:30:48
* @param msgList
* @return
*/
public static JSONArray produceMsg(JSONArray msgList) {
JSONArray result = new JSONArray();
for (int i = 0; i < msgList.size(); i++) {
JSONObject msg = new JSONObject();
JSONObject m = msgList.getJSONObject(i);
m.put("groupMsg", false);// 是否是群消息
if (m.getString("FromUserName").contains("@@") || m.getString("ToUserName").contains("@@")) { // 群聊消息
if (m.getString("FromUserName").contains("@@")
&& !core.getGroupIdList().contains(m.getString("FromUserName"))) {
core.getGroupIdList().add((m.getString("FromUserName")));
} else if (m.getString("ToUserName").contains("@@")
&& !core.getGroupIdList().contains(m.getString("ToUserName"))) {
core.getGroupIdList().add((m.getString("ToUserName")));
}
// 群消息与普通消息不同的是在其消息体Content中会包含发送者id及":<br/>"消息,这里需要处理一下,去掉多余信息,只保留消息内容
if (m.getString("Content").contains("<br/>")) {
String content = m.getString("Content").substring(m.getString("Content").indexOf("<br/>") + 5);
m.put("Content", content);
m.put("groupMsg", true);
}
} else {
CommonTools.msgFormatter(m, "Content");
}
if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_TEXT.getCode())) { // words
// 文本消息
if (m.getString("Url").length() != 0) {
String regEx = "(.+?\\(.+?\\))";
Matcher matcher = CommonTools.getMatcher(regEx, m.getString("Content"));
String data = "Map";
if (matcher.find()) {
data = matcher.group(1);
}
msg.put("Type", "Map");
msg.put("Text", data);
} else {
msg.put("Type", MsgTypeEnum.TEXT.getType());
msg.put("Text", m.getString("Content"));
}
m.put("Type", msg.getString("Type"));
m.put("Text", msg.getString("Text"));
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_IMAGE.getCode())
|| m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_EMOTICON.getCode())) { // 图片消息
m.put("Type", MsgTypeEnum.PIC.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VOICE.getCode())) { // 语音消息
m.put("Type", MsgTypeEnum.VOICE.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VERIFYMSG.getCode())) {// friends
// 好友确认消息
// MessageTools.addFriend(core, userName, 3, ticket); // 确认添加好友
m.put("Type", MsgTypeEnum.VERIFYMSG.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_SHARECARD.getCode())) { // 共享名片
m.put("Type", MsgTypeEnum.NAMECARD.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VIDEO.getCode())
|| m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_MICROVIDEO.getCode())) {// viedo
m.put("Type", MsgTypeEnum.VIEDO.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_MEDIA.getCode())) { // 多媒体消息
m.put("Type", MsgTypeEnum.MEDIA.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_STATUSNOTIFY.getCode())) {// phone
// init
// 微信初始化消息
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_SYS.getCode())) {// 系统消息
m.put("Type", MsgTypeEnum.SYS.getType());
} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_RECALLED.getCode())) { // 撤回消息
} else {
LOG.info("Useless msg");
}
LOG.info("收到消息一条,来自: " + m.getString("FromUserName"));
result.add(m);
}
return result;
}
/**
* 消息处理
*
* @author https://github.com/yaphone
* @date 2017年5月14日 上午10:52:34
* @param msgHandler
*/
public static void handleMsg(IMsgHandlerFace msgHandler) {
while (true) {
if (core.getMsgList().size() > 0 && core.getMsgList().get(0).getContent() != null) {
if (core.getMsgList().get(0).getContent().length() > 0) {
BaseMsg msg = core.getMsgList().get(0);
if (msg.getType() != null) {
try {
if (msg.getType().equals(MsgTypeEnum.TEXT.getType())) {
String result = msgHandler.textMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
} else if (msg.getType().equals(MsgTypeEnum.PIC.getType())) {
String result = msgHandler.picMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
} else if (msg.getType().equals(MsgTypeEnum.VOICE.getType())) {
String result = msgHandler.voiceMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
} else if (msg.getType().equals(MsgTypeEnum.VIEDO.getType())) {
String result = msgHandler.viedoMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
} else if (msg.getType().equals(MsgTypeEnum.NAMECARD.getType())) {
String result = msgHandler.nameCardMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
} else if (msg.getType().equals(MsgTypeEnum.SYS.getType())) { // 系统消息
msgHandler.sysMsgHandle(msg);
} else if (msg.getType().equals(MsgTypeEnum.VERIFYMSG.getType())) { // 确认添加好友消息
String result = msgHandler.verifyAddFriendMsgHandle(msg);
MessageTools.sendMsgById(result,
core.getMsgList().get(0).getRecommendInfo().getUserName());
} else if (msg.getType().equals(MsgTypeEnum.MEDIA.getType())) { // 多媒体消息
String result = msgHandler.mediaMsgHandle(msg);
MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
core.getMsgList().remove(0);
}
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,91 @@
package com.xmzs.common.wechat.face;
import com.xmzs.common.wechat.beans.BaseMsg;
/**
* 消息处理接口
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月20日 上午12:13:49
* @version 1.0
*
*/
public interface IMsgHandlerFace {
/**
*
* @author https://github.com/yaphone
* @date 2017年4月20日 上午12:15:00
* @param msg
* @return
*/
public String textMsgHandle(BaseMsg msg);
/**
* 处理图片消息
*
* @author https://github.com/yaphone
* @date 2017年4月21日 下午11:07:06
* @param msg
* @return
*/
public String picMsgHandle(BaseMsg msg);
/**
* 处理声音消息
*
* @author https://github.com/yaphone
* @date 2017年4月22日 上午12:09:44
* @param msg
* @return
*/
public String voiceMsgHandle(BaseMsg msg);
/**
* 处理小视频消息
*
* @author https://github.com/yaphone
* @date 2017年4月23日 下午12:19:50
* @param msg
* @return
*/
public String viedoMsgHandle(BaseMsg msg);
/**
* 处理名片消息
*
* @author https://github.com/yaphone
* @date 2017年5月1日 上午12:50:50
* @param msg
* @return
*/
public String nameCardMsgHandle(BaseMsg msg);
/**
* 处理系统消息
*
* @author Relyn
* @date 2017年6月21日17:43:51
* @param msg
* @return
*/
public void sysMsgHandle(BaseMsg msg);
/**
* 处理确认添加好友消息
*
* @date 2017年6月28日 下午10:15:30
* @param msg
* @return
*/
public String verifyAddFriendMsgHandle(BaseMsg msg);
/**
* 处理收到的文件消息
*
* @date 2017年7月21日 下午11:59:14
* @param msg
* @return
*/
public String mediaMsgHandle(BaseMsg msg);
}

View File

@@ -0,0 +1,82 @@
package com.xmzs.common.wechat.service;
/**
* 登陆服务接口
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月13日 上午12:07:21
* @version 1.0
*
*/
public interface ILoginService {
/**
* 登陆
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:14:07
* @return
*/
boolean login();
/**
* 获取UUID
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:21:40
* @param qrPath
* @return
*/
String getUuid();
/**
* 获取二维码图片
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:13:51
* @param qrPath
* @return
*/
boolean getQR(String qrPath);
/**
* web初始化
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:14:13
* @return
*/
boolean webWxInit();
/**
* 微信状态通知
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:14:24
*/
void wxStatusNotify();
/**
* 接收消息
*
* @author https://github.com/yaphone
* @date 2017年5月13日 上午12:14:37
*/
void startReceiving();
/**
* 获取微信联系人
*
* @author https://github.com/yaphone
* @date 2017年5月13日 下午2:26:18
*/
void webWxGetContact();
/**
* 批量获取联系人信息
*
* @date 2017年6月22日 下午11:24:35
*/
void WebWxBatchGetContact();
}

View File

@@ -0,0 +1,687 @@
package com.xmzs.common.wechat.service.impl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.regex.Matcher;
import com.xmzs.common.wechat.utils.SleepUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xmzs.common.wechat.beans.BaseMsg;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.core.MsgCenter;
import com.xmzs.common.wechat.service.ILoginService;
import com.xmzs.common.wechat.utils.Config;
import com.xmzs.common.wechat.utils.MyHttpClient;
import com.xmzs.common.wechat.utils.enums.ResultEnum;
import com.xmzs.common.wechat.utils.enums.RetCodeEnum;
import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
import com.xmzs.common.wechat.utils.enums.URLEnum;
import com.xmzs.common.wechat.utils.enums.parameters.BaseParaEnum;
import com.xmzs.common.wechat.utils.enums.parameters.LoginParaEnum;
import com.xmzs.common.wechat.utils.enums.parameters.StatusNotifyParaEnum;
import com.xmzs.common.wechat.utils.enums.parameters.UUIDParaEnum;
import com.xmzs.common.wechat.utils.tools.CommonTools;
/**
* 登陆服务实现类
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月13日 上午12:09:35
* @version 1.0
*
*/
public class LoginServiceImpl implements ILoginService {
private static Logger LOG = LoggerFactory.getLogger(LoginServiceImpl.class);
private Core core = Core.getInstance();
private MyHttpClient httpClient = core.getMyHttpClient();
private MyHttpClient myHttpClient = core.getMyHttpClient();
public LoginServiceImpl() {
}
@Override
public boolean login() {
boolean isLogin = false;
// 组装参数和URL
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
params.add(new BasicNameValuePair(LoginParaEnum.LOGIN_ICON.para(), LoginParaEnum.LOGIN_ICON.value()));
params.add(new BasicNameValuePair(LoginParaEnum.UUID.para(), core.getUuid()));
params.add(new BasicNameValuePair(LoginParaEnum.TIP.para(), LoginParaEnum.TIP.value()));
// long time = 4000;
while (!isLogin) {
// SleepUtils.sleep(time += 1000);
long millis = System.currentTimeMillis();
params.add(new BasicNameValuePair(LoginParaEnum.R.para(), String.valueOf(millis / 1579L)));
params.add(new BasicNameValuePair(LoginParaEnum._1.para(), String.valueOf(millis)));
HttpEntity entity = httpClient.doGet(URLEnum.LOGIN_URL.getUrl(), params, true, null);
try {
String result = EntityUtils.toString(entity);
String status = checklogin(result);
if (ResultEnum.SUCCESS.getCode().equals(status)) {
processLoginInfo(result); // 处理结果
isLogin = true;
core.setAlive(isLogin);
break;
}
if (ResultEnum.WAIT_CONFIRM.getCode().equals(status)) {
LOG.info("请点击微信确认按钮,进行登陆");
}
} catch (Exception e) {
LOG.error("微信登陆异常!", e);
}
}
return isLogin;
}
@Override
public String getUuid() {
// 组装参数和URL
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
params.add(new BasicNameValuePair(UUIDParaEnum.APP_ID.para(), UUIDParaEnum.APP_ID.value()));
params.add(new BasicNameValuePair(UUIDParaEnum.REDIRECT_URL.para(),
UUIDParaEnum.REDIRECT_URL.value()));
params.add(new BasicNameValuePair(UUIDParaEnum.FUN.para(), UUIDParaEnum.FUN.value()));
params.add(new BasicNameValuePair(UUIDParaEnum.LANG.para(), UUIDParaEnum.LANG.value()));
params.add(new BasicNameValuePair(UUIDParaEnum._1.para(), String.valueOf(System.currentTimeMillis())));
HttpEntity entity = httpClient.doGet(URLEnum.UUID_URL.getUrl(), params, true, null);
try {
String result = EntityUtils.toString(entity);
String regEx = "window.QRLogin.code = (\\d+); window.QRLogin.uuid = \"(\\S+?)\";";
Matcher matcher = CommonTools.getMatcher(regEx, result);
if (matcher.find()) {
if ((ResultEnum.SUCCESS.getCode().equals(matcher.group(1)))) {
core.setUuid(matcher.group(2));
}
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
return core.getUuid();
}
@Override
public boolean getQR(String qrPath) {
qrPath = qrPath + File.separator + "QR.jpg";
String qrUrl = URLEnum.QRCODE_URL.getUrl() + core.getUuid();
HttpEntity entity = myHttpClient.doGet(qrUrl, null, true, null);
try {
OutputStream out = new FileOutputStream(qrPath);
byte[] bytes = EntityUtils.toByteArray(entity);
out.write(bytes);
out.flush();
out.close();
try {
CommonTools.printQr(qrPath); // 打开登陆二维码图片
} catch (Exception e) {
LOG.info(e.getMessage());
}
} catch (Exception e) {
LOG.info(e.getMessage());
return false;
}
return true;
}
@Override
public boolean webWxInit() {
core.setAlive(true);
core.setLastNormalRetcodeTime(System.currentTimeMillis());
// 组装请求URL和参数
String url = String.format(URLEnum.INIT_URL.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()),
String.valueOf(System.currentTimeMillis() / 3158L),
core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
Map<String, Object> paramMap = core.getParamMap();
// 请求初始化接口
HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
try {
String result = EntityUtils.toString(entity, Consts.UTF_8);
JSONObject obj = JSON.parseObject(result);
JSONObject user = obj.getJSONObject(StorageLoginInfoEnum.User.getKey());
JSONObject syncKey = obj.getJSONObject(StorageLoginInfoEnum.SyncKey.getKey());
core.getLoginInfo().put(StorageLoginInfoEnum.InviteStartCount.getKey(),
obj.getInteger(StorageLoginInfoEnum.InviteStartCount.getKey()));
core.getLoginInfo().put(StorageLoginInfoEnum.SyncKey.getKey(), syncKey);
JSONArray syncArray = syncKey.getJSONArray("List");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < syncArray.size(); i++) {
sb.append(syncArray.getJSONObject(i).getString("Key") + "_"
+ syncArray.getJSONObject(i).getString("Val") + "|");
}
// 1_661706053|2_661706420|3_661706415|1000_1494151022|
String synckey = sb.toString();
// 1_661706053|2_661706420|3_661706415|1000_1494151022
core.getLoginInfo().put(StorageLoginInfoEnum.synckey.getKey(), synckey.substring(0, synckey.length() - 1));// 1_656161336|2_656161626|3_656161313|11_656159955|13_656120033|201_1492273724|1000_1492265953|1001_1492250432|1004_1491805192
core.setUserName(user.getString("UserName"));
core.setNickName(user.getString("NickName"));
core.setUserSelf(obj.getJSONObject("User"));
String chatSet = obj.getString("ChatSet");
String[] chatSetArray = chatSet.split(",");
for (int i = 0; i < chatSetArray.length; i++) {
if (chatSetArray[i].indexOf("@@") != -1) {
// 更新GroupIdList
core.getGroupIdList().add(chatSetArray[i]); //
}
}
// JSONArray contactListArray = obj.getJSONArray("ContactList");
// for (int i = 0; i < contactListArray.size(); i++) {
// JSONObject o = contactListArray.getJSONObject(i);
// if (o.getString("UserName").indexOf("@@") != -1) {
// core.getGroupIdList().add(o.getString("UserName")); //
// // 更新GroupIdList
// core.getGroupList().add(o); // 更新GroupList
// core.getGroupNickNameList().add(o.getString("NickName"));
// }
// }
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
public void wxStatusNotify() {
// 组装请求URL和参数
String url = String.format(URLEnum.STATUS_NOTIFY_URL.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
Map<String, Object> paramMap = core.getParamMap();
paramMap.put(StatusNotifyParaEnum.CODE.para(), StatusNotifyParaEnum.CODE.value());
paramMap.put(StatusNotifyParaEnum.FROM_USERNAME.para(), core.getUserName());
paramMap.put(StatusNotifyParaEnum.TO_USERNAME.para(), core.getUserName());
paramMap.put(StatusNotifyParaEnum.CLIENT_MSG_ID.para(), System.currentTimeMillis());
String paramStr = JSON.toJSONString(paramMap);
try {
HttpEntity entity = httpClient.doPost(url, paramStr);
EntityUtils.toString(entity, Consts.UTF_8);
} catch (Exception e) {
LOG.error("微信状态通知接口失败!", e);
}
}
@Override
public void startReceiving() {
core.setAlive(true);
new Thread(new Runnable() {
int retryCount = 0;
@Override
public void run() {
while (core.isAlive()) {
try {
Map<String, String> resultMap = syncCheck();
LOG.info(JSONObject.toJSONString(resultMap));
String retcode = resultMap.get("retcode");
String selector = resultMap.get("selector");
if (retcode.equals(RetCodeEnum.UNKOWN.getCode())) {
LOG.info(RetCodeEnum.UNKOWN.getType());
continue;
} else if (retcode.equals(RetCodeEnum.LOGIN_OUT.getCode())) { // 退出
LOG.info(RetCodeEnum.LOGIN_OUT.getType());
break;
} else if (retcode.equals(RetCodeEnum.LOGIN_OTHERWHERE.getCode())) { // 其它地方登陆
LOG.info(RetCodeEnum.LOGIN_OTHERWHERE.getType());
break;
} else if (retcode.equals(RetCodeEnum.MOBILE_LOGIN_OUT.getCode())) { // 移动端退出
LOG.info(RetCodeEnum.MOBILE_LOGIN_OUT.getType());
break;
} else if (retcode.equals(RetCodeEnum.NORMAL.getCode())) {
core.setLastNormalRetcodeTime(System.currentTimeMillis()); // 最后收到正常报文时间
JSONObject msgObj = webWxSync();
if (selector.equals("2")) {
if (msgObj != null) {
try {
JSONArray msgList = new JSONArray();
msgList = msgObj.getJSONArray("AddMsgList");
msgList = MsgCenter.produceMsg(msgList);
for (int j = 0; j < msgList.size(); j++) {
BaseMsg baseMsg = JSON.toJavaObject(msgList.getJSONObject(j),
BaseMsg.class);
core.getMsgList().add(baseMsg);
}
} catch (Exception e) {
LOG.info(e.getMessage());
}
}
} else if (selector.equals("7")) {
webWxSync();
} else if (selector.equals("4")) {
continue;
} else if (selector.equals("3")) {
continue;
} else if (selector.equals("6")) {
if (msgObj != null) {
try {
JSONArray msgList = new JSONArray();
msgList = msgObj.getJSONArray("AddMsgList");
JSONArray modContactList = msgObj.getJSONArray("ModContactList"); // 存在删除或者新增的好友信息
msgList = MsgCenter.produceMsg(msgList);
for (int j = 0; j < msgList.size(); j++) {
JSONObject userInfo = modContactList.getJSONObject(j);
// 存在主动加好友之后的同步联系人到本地
core.getContactList().add(userInfo);
}
} catch (Exception e) {
LOG.info(e.getMessage());
}
}
}
} else {
JSONObject obj = webWxSync();
}
} catch (Exception e) {
LOG.info(e.getMessage());
retryCount += 1;
if (core.getReceivingRetryCount() < retryCount) {
core.setAlive(false);
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
LOG.info(e.getMessage());
}
}
}
}
}
}).start();
}
@Override
public void webWxGetContact() {
String url = String.format(URLEnum.WEB_WX_GET_CONTACT.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()));
Map<String, Object> paramMap = core.getParamMap();
HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
try {
String result = EntityUtils.toString(entity, Consts.UTF_8);
JSONObject fullFriendsJsonList = JSON.parseObject(result);
// 查看seq是否为00表示好友列表已全部获取完毕若大于0则表示好友列表未获取完毕当前的字节数断点续传
long seq = 0;
long currentTime = 0L;
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
if (fullFriendsJsonList.get("Seq") != null) {
seq = fullFriendsJsonList.getLong("Seq");
currentTime = new Date().getTime();
}
core.setMemberCount(fullFriendsJsonList.getInteger(StorageLoginInfoEnum.MemberCount.getKey()));
JSONArray member = fullFriendsJsonList.getJSONArray(StorageLoginInfoEnum.MemberList.getKey());
// 循环获取seq直到为0即获取全部好友列表 ==0好友获取完毕 >0好友未获取完毕此时seq为已获取的字节数
while (seq > 0) {
// 设置seq传参
params.add(new BasicNameValuePair("r", String.valueOf(currentTime)));
params.add(new BasicNameValuePair("seq", String.valueOf(seq)));
entity = httpClient.doGet(url, params, false, null);
params.remove(new BasicNameValuePair("r", String.valueOf(currentTime)));
params.remove(new BasicNameValuePair("seq", String.valueOf(seq)));
result = EntityUtils.toString(entity, Consts.UTF_8);
fullFriendsJsonList = JSON.parseObject(result);
if (fullFriendsJsonList.get("Seq") != null) {
seq = fullFriendsJsonList.getLong("Seq");
currentTime = new Date().getTime();
}
// 累加好友列表
member.addAll(fullFriendsJsonList.getJSONArray(StorageLoginInfoEnum.MemberList.getKey()));
}
core.setMemberCount(member.size());
for (Iterator<?> iterator = member.iterator(); iterator.hasNext();) {
JSONObject o = (JSONObject) iterator.next();
if ((o.getInteger("VerifyFlag") & 8) != 0) { // 公众号/服务号
core.getPublicUsersList().add(o);
} else if (Config.API_SPECIAL_USER.contains(o.getString("UserName"))) { // 特殊账号
core.getSpecialUsersList().add(o);
} else if (o.getString("UserName").indexOf("@@") != -1) { // 群聊
if (!core.getGroupIdList().contains(o.getString("UserName"))) {
core.getGroupNickNameList().add(o.getString("NickName"));
core.getGroupIdList().add(o.getString("UserName"));
core.getGroupList().add(o);
}
} else if (o.getString("UserName").equals(core.getUserSelf().getString("UserName"))) { // 自己
core.getContactList().remove(o);
} else { // 普通联系人
core.getContactList().add(o);
}
}
return;
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
return;
}
@Override
public void WebWxBatchGetContact() {
String url = String.format(URLEnum.WEB_WX_BATCH_GET_CONTACT.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()), new Date().getTime(),
core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
Map<String, Object> paramMap = core.getParamMap();
paramMap.put("Count", core.getGroupIdList().size());
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
for (int i = 0; i < core.getGroupIdList().size(); i++) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("UserName", core.getGroupIdList().get(i));
map.put("EncryChatRoomId", "");
list.add(map);
}
paramMap.put("List", list);
HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
try {
String text = EntityUtils.toString(entity, Consts.UTF_8);
JSONObject obj = JSON.parseObject(text);
JSONArray contactList = obj.getJSONArray("ContactList");
for (int i = 0; i < contactList.size(); i++) { // 群好友
if (contactList.getJSONObject(i).getString("UserName").indexOf("@@") > -1) { // 群
core.getGroupNickNameList().add(contactList.getJSONObject(i).getString("NickName")); // 更新群昵称列表
core.getGroupList().add(contactList.getJSONObject(i)); // 更新群信息(所有)列表
core.getGroupMemeberMap().put(contactList.getJSONObject(i).getString("UserName"),
contactList.getJSONObject(i).getJSONArray("MemberList")); // 更新群成员Map
}
}
} catch (Exception e) {
LOG.info(e.getMessage());
}
}
/**
* 检查登陆状态
*
* @param result
* @return
*/
public String checklogin(String result) {
String regEx = "window.code=(\\d+)";
Matcher matcher = CommonTools.getMatcher(regEx, result);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* 处理登陆信息
*
* @author https://github.com/yaphone
* @date 2017年4月9日 下午12:16:26
* @param loginContent
*/
private void processLoginInfo(String loginContent) {
String regEx = "window.redirect_uri=\"(\\S+)\";";
Matcher matcher = CommonTools.getMatcher(regEx, loginContent);
if (matcher.find()) {
String originalUrl = matcher.group(1);
String url = originalUrl.substring(0, originalUrl.lastIndexOf('/')); // https://wx2.qq.com/cgi-bin/mmwebwx-bin
core.getLoginInfo().put("url", url);
Map<String, List<String>> possibleUrlMap = this.getPossibleUrlMap();
Iterator<Entry<String, List<String>>> iterator = possibleUrlMap.entrySet().iterator();
Map.Entry<String, List<String>> entry;
String fileUrl;
String syncUrl;
while (iterator.hasNext()) {
entry = iterator.next();
String indexUrl = entry.getKey();
fileUrl = "https://" + entry.getValue().get(0) + "/cgi-bin/mmwebwx-bin";
syncUrl = "https://" + entry.getValue().get(1) + "/cgi-bin/mmwebwx-bin";
if (core.getLoginInfo().get("url").toString().contains(indexUrl)) {
core.setIndexUrl(indexUrl);
core.getLoginInfo().put("fileUrl", fileUrl);
core.getLoginInfo().put("syncUrl", syncUrl);
break;
}
}
if (core.getLoginInfo().get("fileUrl") == null && core.getLoginInfo().get("syncUrl") == null) {
core.getLoginInfo().put("fileUrl", url);
core.getLoginInfo().put("syncUrl", url);
}
core.getLoginInfo().put("deviceid", "e" + String.valueOf(new Random().nextLong()).substring(1, 16)); // 生成15位随机数
core.getLoginInfo().put("BaseRequest", new ArrayList<String>());
String text = "";
try {
HttpEntity entity = myHttpClient.doGet(originalUrl, null, false, null);
text = EntityUtils.toString(entity);
} catch (Exception e) {
LOG.info(e.getMessage());
return;
}
//add by 默非默 2017-08-01 22:28:09
//如果登录被禁止时则登录返回的message内容不为空下面代码则判断登录内容是否为空不为空则退出程序
String msg = getLoginMessage(text);
if (!"".equals(msg)){
LOG.info(msg);
System.exit(0);
}
Document doc = CommonTools.xmlParser(text);
if (doc != null) {
core.getLoginInfo().put(StorageLoginInfoEnum.skey.getKey(),
doc.getElementsByTagName(StorageLoginInfoEnum.skey.getKey()).item(0).getFirstChild()
.getNodeValue());
core.getLoginInfo().put(StorageLoginInfoEnum.wxsid.getKey(),
doc.getElementsByTagName(StorageLoginInfoEnum.wxsid.getKey()).item(0).getFirstChild()
.getNodeValue());
core.getLoginInfo().put(StorageLoginInfoEnum.wxuin.getKey(),
doc.getElementsByTagName(StorageLoginInfoEnum.wxuin.getKey()).item(0).getFirstChild()
.getNodeValue());
core.getLoginInfo().put(StorageLoginInfoEnum.pass_ticket.getKey(),
doc.getElementsByTagName(StorageLoginInfoEnum.pass_ticket.getKey()).item(0).getFirstChild()
.getNodeValue());
}
}
}
private Map<String, List<String>> getPossibleUrlMap() {
Map<String, List<String>> possibleUrlMap = new HashMap<String, List<String>>();
possibleUrlMap.put("wx.qq.com", new ArrayList<String>() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add("file.wx.qq.com");
add("webpush.wx.qq.com");
}
});
possibleUrlMap.put("wx2.qq.com", new ArrayList<String>() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add("file.wx2.qq.com");
add("webpush.wx2.qq.com");
}
});
possibleUrlMap.put("wx8.qq.com", new ArrayList<String>() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add("file.wx8.qq.com");
add("webpush.wx8.qq.com");
}
});
possibleUrlMap.put("web2.wechat.com", new ArrayList<String>() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add("file.web2.wechat.com");
add("webpush.web2.wechat.com");
}
});
possibleUrlMap.put("wechat.com", new ArrayList<String>() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add("file.web.wechat.com");
add("webpush.web.wechat.com");
}
});
return possibleUrlMap;
}
/**
* 同步消息 sync the messages
*
* @author https://github.com/yaphone
* @date 2017年5月12日 上午12:24:55
* @return
*/
private JSONObject webWxSync() {
JSONObject result = null;
String url = String.format(URLEnum.WEB_WX_SYNC_URL.getUrl(),
core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()),
core.getLoginInfo().get(StorageLoginInfoEnum.wxsid.getKey()),
core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()),
core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
Map<String, Object> paramMap = core.getParamMap();
paramMap.put(StorageLoginInfoEnum.SyncKey.getKey(),
core.getLoginInfo().get(StorageLoginInfoEnum.SyncKey.getKey()));
paramMap.put("rr", -new Date().getTime() / 1000);
String paramStr = JSON.toJSONString(paramMap);
try {
HttpEntity entity = myHttpClient.doPost(url, paramStr);
String text = EntityUtils.toString(entity, Consts.UTF_8);
JSONObject obj = JSON.parseObject(text);
if (obj.getJSONObject("BaseResponse").getInteger("Ret") != 0) {
result = null;
} else {
result = obj;
core.getLoginInfo().put(StorageLoginInfoEnum.SyncKey.getKey(), obj.getJSONObject("SyncCheckKey"));
JSONArray syncArray = obj.getJSONObject(StorageLoginInfoEnum.SyncKey.getKey()).getJSONArray("List");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < syncArray.size(); i++) {
sb.append(syncArray.getJSONObject(i).getString("Key") + "_"
+ syncArray.getJSONObject(i).getString("Val") + "|");
}
String synckey = sb.toString();
core.getLoginInfo().put(StorageLoginInfoEnum.synckey.getKey(),
synckey.substring(0, synckey.length() - 1));// 1_656161336|2_656161626|3_656161313|11_656159955|13_656120033|201_1492273724|1000_1492265953|1001_1492250432|1004_1491805192
}
} catch (Exception e) {
LOG.info(e.getMessage());
}
return result;
}
/**
* 检查是否有新消息 check whether there's a message
*
* @author https://github.com/yaphone
* @date 2017年4月16日 上午11:11:34
* @return
*
*/
private Map<String, String> syncCheck() {
Map<String, String> resultMap = new HashMap<String, String>();
// 组装请求URL和参数
String url = core.getLoginInfo().get(StorageLoginInfoEnum.syncUrl.getKey()) + URLEnum.SYNC_CHECK_URL.getUrl();
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
for (BaseParaEnum baseRequest : BaseParaEnum.values()) {
params.add(new BasicNameValuePair(baseRequest.para().toLowerCase(),
core.getLoginInfo().get(baseRequest.value()).toString()));
}
params.add(new BasicNameValuePair("r", String.valueOf(new Date().getTime())));
params.add(new BasicNameValuePair("synckey", (String) core.getLoginInfo().get("synckey")));
params.add(new BasicNameValuePair("_", String.valueOf(new Date().getTime())));
SleepUtils.sleep(7);
try {
HttpEntity entity = myHttpClient.doGet(url, params, true, null);
if (entity == null) {
resultMap.put("retcode", "9999");
resultMap.put("selector", "9999");
return resultMap;
}
String text = EntityUtils.toString(entity);
String regEx = "window.synccheck=\\{retcode:\"(\\d+)\",selector:\"(\\d+)\"\\}";
Matcher matcher = CommonTools.getMatcher(regEx, text);
if (!matcher.find() || matcher.group(1).equals("2")) {
LOG.info(String.format("Unexpected sync check result: %s", text));
} else {
resultMap.put("retcode", matcher.group(1));
resultMap.put("selector", matcher.group(2));
}
} catch (Exception e) {
e.printStackTrace();
}
return resultMap;
}
/**
* 解析登录返回的消息如果成功登录则message为空
* @param result
* @return
*/
public String getLoginMessage(String result){
String[] strArr = result.split("<message>");
String[] rs = strArr[1].split("</message>");
if (rs!=null && rs.length>1) {
return rs[0];
}
return "";
}
}

View File

@@ -0,0 +1,38 @@
package com.xmzs.common.wechat.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.utils.SleepUtils;
/**
* 检查微信在线状态
* <p>
* 如何来感知微信状态?
* 微信会有心跳包LoginServiceImpl.syncCheck()正常在线情况下返回的消息中retcode报文应该为"0"心跳间隔一般在25秒
* 那么可以通过最后收到正常报文的时间来作为判断是否在线的依据。若报文间隔大于60秒则认为已掉线。
* </p>
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月17日 下午10:53:15
* @version 1.0
*
*/
public class CheckLoginStatusThread implements Runnable {
private static Logger LOG = LoggerFactory.getLogger(CheckLoginStatusThread.class);
private Core core = Core.getInstance();
@Override
public void run() {
while (core.isAlive()) {
long t1 = System.currentTimeMillis(); // 秒为单位
if (t1 - core.getLastNormalRetcodeTime() > 60 * 1000) { // 超过60秒判为离线
core.setAlive(false);
LOG.info("微信已离线");
}
SleepUtils.sleep(10 * 1000); // 休眠10秒
}
}
}

View File

@@ -0,0 +1,81 @@
package com.xmzs.common.wechat.utils;
import com.xmzs.common.wechat.utils.enums.OsNameEnum;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* 配置信息
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月23日 下午2:26:21
* @version 1.0
*
*/
public class Config {
public static final String API_WXAPPID = "API_WXAPPID";
public static final String picDir = "D://itchat4j";
public static final String VERSION = "1.4.1";
public static final String BASE_URL = "https://login.weixin.qq.com";
public static final String REFERER = "https://wx.qq.com/?&lang=zh_CN&target=t";
public static final String OS = "";
public static final String DIR = "";
public static final String DEFAULT_QR = "QR.jpg";
public static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36";
public static final String UOS_PATCH_CLIENT_VERSION = "2.0.0";
public static final String UOS_PATCH_EXTSPAM =
"Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==";
public static final ArrayList<String> API_SPECIAL_USER = new ArrayList<String>(Arrays.asList("filehelper", "weibo",
"qqmail", "fmessage", "tmessage", "qmessage", "qqsync", "floatbottle", "lbsapp", "shakeapp", "medianote",
"qqfriend", "readerapp", "blogapp", "facebookapp", "masssendapp", "meishiapp", "feedsapp", "voip",
"blogappweixin", "brandsessionholder", "weixin", "weixinreminder", "officialaccounts", "wxitil",
"notification_messages", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "userexperience_alarm"));
/**
* 获取文件目录
*
* @author https://github.com/yaphone
* @date 2017年4月8日 下午10:27:42
* @return
*/
public static String getLocalPath() {
String localPath = null;
try {
localPath = new File("").getCanonicalPath();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return localPath;
}
/**
* 获取系统平台
*
* @author https://github.com/yaphone
* @date 2017年4月8日 下午10:27:53
*/
public static OsNameEnum getOsNameEnum() {
String os = System.getProperty("os.name").toUpperCase();
if (os.indexOf(OsNameEnum.DARWIN.toString()) >= 0) {
return OsNameEnum.DARWIN;
} else if (os.indexOf(OsNameEnum.WINDOWS.toString()) >= 0) {
return OsNameEnum.WINDOWS;
} else if (os.indexOf(OsNameEnum.LINUX.toString()) >= 0) {
return OsNameEnum.LINUX;
} else if (os.indexOf(OsNameEnum.MAC.toString()) >= 0) {
return OsNameEnum.MAC;
}
return OsNameEnum.OTHER;
}
}

View File

@@ -0,0 +1,34 @@
package com.xmzs.common.wechat.utils;
/**
* 常量
*
* @author https=//github.com/yaphone
* @date 创建时间2017年5月5日 下午11=29=04
* @version 1.0
*
*/
public class ConstantConfigEnum {
public static final int APPMSGTYPE_TEXT = 1;
public static final int APPMSGTYPE_IMG = 2;
public static final int APPMSGTYPE_AUDIO = 3;
public static final int APPMSGTYPE_VIDEO = 4;
public static final int APPMSGTYPE_URL = 5;
public static final int APPMSGTYPE_ATTACH = 6;
public static final int APPMSGTYPE_OPEN = 7;
public static final int APPMSGTYPE_EMOJI = 8;
public static final int APPMSGTYPE_VOICE_REMIND = 9;
public static final int APPMSGTYPE_SCAN_GOOD = 10;
public static final int APPMSGTYPE_GOOD = 13;
public static final int APPMSGTYPE_EMOTION = 15;
public static final int APPMSGTYPE_CARD_TICKET = 16;
public static final int APPMSGTYPE_REALTIME_SHARE_LOCATION = 17;
// public static final int APPMSGTYPE_TRANSFERS = 2e3;
public static final int APPMSGTYPE_RED_ENVELOPES = 2001;
public static final int APPMSGTYPE_READER_TYPE = 100001;
public static final int UPLOAD_MEDIA_TYPE_IMAGE = 1;
public static final int UPLOAD_MEDIA_TYPE_VIDEO = 2;
public static final int UPLOAD_MEDIA_TYPE_AUDIO = 3;
public static final int UPLOAD_MEDIA_TYPE_ATTACHMENT = 4;
}

View File

@@ -0,0 +1,6 @@
package com.xmzs.common.wechat.utils;
public class MsgKeywords {
public static String newFriendStr = "我通过了你的朋友验证请求";
}

View File

@@ -0,0 +1,194 @@
package com.xmzs.common.wechat.utils;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
/**
* HTTP访问类对Apache HttpClient进行简单封装适配器模式
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月9日 下午7:05:04
* @version 1.0
*
*/
public class MyHttpClient {
private Logger logger = Logger.getLogger("MyHttpClient");
private static CloseableHttpClient httpClient = HttpClients.createDefault();
private static MyHttpClient instance = null;
private static CookieStore cookieStore;
static {
cookieStore = new BasicCookieStore();
// 将CookieStore设置到httpClient中
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
}
public static String getCookie(String name) {
List<Cookie> cookies = cookieStore.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(name)) {
return cookie.getValue();
}
}
return null;
}
private MyHttpClient() {
}
/**
* 获取cookies
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午8:37:17
* @return
*/
public static MyHttpClient getInstance() {
if (instance == null) {
synchronized (MyHttpClient.class) {
if (instance == null) {
instance = new MyHttpClient();
}
}
}
return instance;
}
/**
* 处理GET请求
*
* @author https://github.com/yaphone
* @date 2017年4月9日 下午7:06:19
* @param url
* @param params
* @return
*/
public HttpEntity doGet(String url, List<BasicNameValuePair> params, boolean redirect,
Map<String, String> headerMap) {
HttpEntity entity = null;
HttpGet httpGet = new HttpGet();
try {
if (params != null) {
String paramStr = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
httpGet = new HttpGet(url + "?" + paramStr);
} else {
httpGet = new HttpGet(url);
}
if (!redirect) {
httpGet.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); // 禁止重定向
}
httpGet.setHeader("User-Agent", Config.USER_AGENT);
httpGet.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
httpGet.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
httpGet.setHeader("referer", Config.REFERER);
if (headerMap != null) {
Set<Entry<String, String>> entries = headerMap.entrySet();
for (Entry<String, String> entry : entries) {
httpGet.setHeader(entry.getKey(), entry.getValue());
}
}
CloseableHttpResponse response = httpClient.execute(httpGet);
entity = response.getEntity();
} catch (ClientProtocolException e) {
logger.info(e.getMessage());
} catch (IOException e) {
logger.info(e.getMessage());
}
return entity;
}
/**
* 处理POST请求
*
* @author https://github.com/yaphone
* @date 2017年4月9日 下午7:06:35
* @param url
* @param params
* @return
*/
public HttpEntity doPost(String url, String paramsStr) {
HttpEntity entity = null;
HttpPost httpPost = new HttpPost();
try {
StringEntity params = new StringEntity(paramsStr, Consts.UTF_8);
httpPost = new HttpPost(url);
httpPost.setEntity(params);
httpPost.setHeader("Content-type", "application/json; charset=utf-8");
httpPost.setHeader("User-Agent", Config.USER_AGENT);
httpPost.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
httpPost.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
httpPost.setHeader("referer", Config.REFERER);
CloseableHttpResponse response = httpClient.execute(httpPost);
entity = response.getEntity();
} catch (ClientProtocolException e) {
logger.info(e.getMessage());
} catch (IOException e) {
logger.info(e.getMessage());
}
return entity;
}
/**
* 上传文件到服务器
*
* @author https://github.com/yaphone
* @date 2017年5月7日 下午9:19:23
* @param url
* @param reqEntity
* @return
*/
public HttpEntity doPostFile(String url, HttpEntity reqEntity) {
HttpEntity entity = null;
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("User-Agent", Config.USER_AGENT);
httpPost.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
httpPost.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
httpPost.setHeader("referer", Config.REFERER);
httpPost.setEntity(reqEntity);
try {
CloseableHttpResponse response = httpClient.execute(httpPost);
entity = response.getEntity();
} catch (Exception e) {
logger.info(e.getMessage());
}
return entity;
}
public static CloseableHttpClient getHttpClient() {
return httpClient;
}
}

View File

@@ -0,0 +1,20 @@
package com.xmzs.common.wechat.utils;
/**
* Created by xiaoxiaomo on 2017/5/6.
*/
public class SleepUtils {
/**
* 毫秒为单位
* @param time
*/
public static void sleep( long time ){
try {
Thread.sleep( time );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,68 @@
package com.xmzs.common.wechat.utils.enums;
/**
* 消息类型
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月23日 下午12:15:00
* @version 1.0
*
*/
public enum MsgCodeEnum {
// public static final int MSGTYPE_TEXT = 1; // 文本消息类型
// public static final int MSGTYPE_IMAGE = 3; // 图片消息
// public static final int MSGTYPE_VOICE = 34; // 语音消息
// public static final int MSGTYPE_VIDEO = 43; // 小视频消息
// public static final int MSGTYPE_MICROVIDEO = 62; // 短视频消息
// public static final int MSGTYPE_EMOTICON = 47; // 表情消息
// public static final int MSGTYPE_APP = 49;
// public static final int MSGTYPE_VOIPMSG = 50;
// public static final int MSGTYPE_VOIPNOTIFY = 52;
// public static final int MSGTYPE_VOIPINVITE = 53;
// public static final int MSGTYPE_LOCATION = 48;
// public static final int MSGTYPE_STATUSNOTIFY = 51;
// public static final int MSGTYPE_SYSNOTICE = 9999;
// public static final int MSGTYPE_POSSIBLEFRIEND_MSG = 40;
// public static final int MSGTYPE_VERIFYMSG = 37;
// public static final int MSGTYPE_SHARECARD = 42;
// public static final int MSGTYPE_SYS = 10000;
// public static final int MSGTYPE_RECALLED = 10002;
MSGTYPE_TEXT(1, "文本消息类型"),
MSGTYPE_IMAGE(3, "图片消息"),
MSGTYPE_VOICE(34, "语音消息"),
MSGTYPE_VIDEO(43, "小视频消息"),
MSGTYPE_MICROVIDEO(62, "短视频消息"),
MSGTYPE_EMOTICON(47, "表情消息"),
MSGTYPE_MEDIA(49, "多媒体消息"),
MSGTYPE_VOIPMSG(50, ""),
MSGTYPE_VOIPNOTIFY(52, ""),
MSGTYPE_VOIPINVITE(53, ""),
MSGTYPE_LOCATION(48, ""),
MSGTYPE_STATUSNOTIFY(51, ""),
MSGTYPE_SYSNOTICE(9999, ""),
MSGTYPE_POSSIBLEFRIEND_MSG(40, ""),
MSGTYPE_VERIFYMSG(37, "好友请求"),
MSGTYPE_SHARECARD(42, ""),
MSGTYPE_SYS(10000, "系统消息"),
MSGTYPE_RECALLED(10002, "")
;
private int code;
private String type;
MsgCodeEnum(int code, String type) {
this.code = code;
this.type = type;
}
public int getCode() {
return code;
}
public String getType() {
return type;
}
}

View File

@@ -0,0 +1,38 @@
package com.xmzs.common.wechat.utils.enums;
/**
* 消息类型枚举类
*
* @author https://github.com/yaphone
* @date 创建时间2017年5月13日 下午11:53:00
* @version 1.0
*
*/
public enum MsgTypeEnum {
TEXT("Text", "文本消息"),
PIC("Pic", "图片消息"),
VOICE("Voice", "语音消息"),
VIEDO("Viedo", "小视频消息"),
NAMECARD("NameCard", "名片消息"),
SYS("Sys", "系统消息"),
VERIFYMSG("VerifyMsg", "添加好友"),
MEDIA("app", "文件消息");
private String type;
private String code;
MsgTypeEnum(String type, String code) {
this.type = type;
this.code = code;
}
public String getType() {
return type;
}
public String getCode() {
return code;
}
}

View File

@@ -0,0 +1,13 @@
package com.xmzs.common.wechat.utils.enums;
/**
* 系统平台
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月8日 下午10:36:28
* @version 1.0
*
*/
public enum OsNameEnum {
WINDOWS, LINUX, DARWIN, MAC, OTHER
}

View File

@@ -0,0 +1,35 @@
package com.xmzs.common.wechat.utils.enums;
/**
* 返回结构枚举类
* <p>
* Created by xiaoxiaomo on 2017/5/6.
*/
public enum ResultEnum {
SUCCESS("200", "成功"),
WAIT_CONFIRM("201", "请在手机上点击确认"),
WAIT_SCAN("400", "请扫描二维码");
private String code;
private String msg;
ResultEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
// public static MsgInfoEnum getCode(String code) {
// switch (code) {
// case "Text":
// return MsgInfoEnum.TEXT;
// default:
// return MsgInfoEnum.VIDEO;
// }
// }
}

View File

@@ -0,0 +1,30 @@
package com.xmzs.common.wechat.utils.enums;
public enum RetCodeEnum {
NORMAL("0", "普通"),
LOGIN_OUT("1102", "退出"),
LOGIN_OTHERWHERE("1101", "其它地方登陆"),
MOBILE_LOGIN_OUT("1102", "移动端退出"),
UNKOWN("9999", "未知")
;
private String code;
private String type;
RetCodeEnum(String code, String type) {
this.code = code;
this.type = type;
}
public String getCode() {
return code;
}
public String getType() {
return type;
}
}

View File

@@ -0,0 +1,59 @@
package com.xmzs.common.wechat.utils.enums;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* Created by xiaoxiaomo on 2017/5/7.
*/
public enum StorageLoginInfoEnum {
//URL
url("url",new String()),
fileUrl("fileUrl",new String()),
syncUrl("syncUrl",new String()),
deviceid("deviceid",new String()), //生成15位随机数
//baseRequest
skey("skey",new String()),
wxsid("wxsid",new String()),
wxuin("wxuin",new String()),
pass_ticket("pass_ticket",new String()),
InviteStartCount("InviteStartCount",0),
User("User",new JSONObject()),
SyncKey("SyncKey",new JSONObject()),
synckey("synckey",new String()),
MemberCount("MemberCount",new String()),
MemberList("MemberList",new JSONArray()),
;
private String key;
private Object type;
StorageLoginInfoEnum(String key, Object type) {
this.key = key;
this.type = type;
}
public String getKey() {
return key;
}
public Object getType() {
return type;
}
}

View File

@@ -0,0 +1,49 @@
package com.xmzs.common.wechat.utils.enums;
/**
* URL
* Created by xiaoxiaomo on 2017/5/6.
*/
public enum URLEnum {
BASE_URL("https://login.weixin.qq.com","基本的URL"),
UUID_URL(BASE_URL.url+"/jslogin","UUIDLURL"),
QRCODE_URL(BASE_URL.url+"/qrcode/","初始化URL"),
STATUS_NOTIFY_URL(BASE_URL.url+"/webwxstatusnotify?lang=zh_CN&pass_ticket=%s","微信状态通知"),
LOGIN_URL(BASE_URL.url+"/cgi-bin/mmwebwx-bin/login","登陆URL"),
INIT_URL("%s/webwxinit?r=%s&pass_ticket=%s","初始化URL"),
SYNC_CHECK_URL("/synccheck","检查心跳URL"),
WEB_WX_SYNC_URL("%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s","web微信消息同步URL"),
WEB_WX_GET_CONTACT("%s/webwxgetcontact","web微信获取联系人信息URL"),
WEB_WX_SEND_MSG("%s/webwxsendmsg","发送消息URL"),
WEB_WX_UPLOAD_MEDIA("%s/webwxuploadmedia?f=json", "上传文件到服务器"),
WEB_WX_GET_MSG_IMG("%s/webwxgetmsgimg", "下载图片消息"),
WEB_WX_GET_VOICE("%s/webwxgetvoice", "下载语音消息"),
WEB_WX_GET_VIEDO("%s/webwxgetvideo", "下载语音消息"),
WEB_WX_PUSH_LOGIN("%s/webwxpushloginurl", "不扫码登陆"),
WEB_WX_LOGOUT("%s/webwxlogout", "退出微信"),
WEB_WX_BATCH_GET_CONTACT("%s/webwxbatchgetcontact?type=ex&r=%s&lang=zh_CN&pass_ticket=%s", "查询群信息"),
WEB_WX_REMARKNAME("%s/webwxoplog?lang=zh_CN&pass_ticket=%s", "修改好友备注"),
WEB_WX_VERIFYUSER("%s/webwxverifyuser?r=%s&lang=zh_CN&pass_ticket=%s", "被动添加好友"),
WEB_WX_GET_MEDIA("%s/webwxgetmedia", "下载文件")
;
private String url;
private String msg;
URLEnum(String url, String msg) {
this.url = url;
this.msg = msg;
}
public String getUrl() {
return url;
}
}

View File

@@ -0,0 +1,28 @@
package com.xmzs.common.wechat.utils.enums;
/**
* 确认添加好友Enum
*
* @author https://github.com/yaphone
* @date 创建时间2017年6月29日 下午9:47:14
* @version 1.0
*
*/
public enum VerifyFriendEnum {
ADD(2, "添加"),
ACCEPT(3, "接受");
private int code;
private String desc;
private VerifyFriendEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
}

View File

@@ -0,0 +1,36 @@
package com.xmzs.common.wechat.utils.enums.parameters;
/**
*
* 基本请求参数
* 1. webWxInit 初始化
* 2. wxStatusNotify 微信状态通知
*
* <p>
* Created by xiaoxiaomo on 2017/5/7.
*/
public enum BaseParaEnum {
Uin("Uin", "wxuin"),
Sid("Sid", "wxsid"),
Skey("Skey", "skey"),
DeviceID("DeviceID", "pass_ticket");
private String para;
private String value;
BaseParaEnum(String para, String value) {
this.para = para;
this.value = value;
}
public String para() {
return para;
}
public Object value() {
return value;
}
}

View File

@@ -0,0 +1,31 @@
package com.xmzs.common.wechat.utils.enums.parameters;
/**
* 登陆
* <p>
* Created by xiaoxiaomo on 2017/5/7.
*/
public enum LoginParaEnum {
LOGIN_ICON("loginicon", "true"),
UUID("uuid", ""),
TIP("tip", "0"),
R("r", ""),
_1("_", "");
private String para;
private String value;
LoginParaEnum(String para, String value) {
this.para = para;
this.value = value;
}
public String para() {
return para;
}
public String value() {
return value;
}
}

View File

@@ -0,0 +1,30 @@
package com.xmzs.common.wechat.utils.enums.parameters;
/**
* 状态通知
* <p>
* Created by xiaoxiaomo on 2017/5/7.
*/
public enum StatusNotifyParaEnum {
CODE("Code", "3"),
FROM_USERNAME("FromUserName", ""),
TO_USERNAME("ToUserName", ""),
CLIENT_MSG_ID("ClientMsgId", ""); //时间戳
private String para;
private String value;
StatusNotifyParaEnum(String para, String value) {
this.para = para;
this.value = value;
}
public String para() {
return para;
}
public String value() {
return value;
}
}

View File

@@ -0,0 +1,33 @@
package com.xmzs.common.wechat.utils.enums.parameters;
/**
* UUID
* <p>
* Created by xiaoxiaomo on 2017/5/7.
*/
public enum UUIDParaEnum {
APP_ID("appid", "wx782c26e4c19acffb"),
FUN("fun", "new"),
LANG("lang", "zh_CN"),
REDIRECT_URL("redirect_uri",
"https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop"),
_1("_", "时间戳");
private String para;
private String value;
UUIDParaEnum(String para, String value) {
this.para = para;
this.value = value;
}
public String para() {
return para;
}
public String value() {
return value;
}
}

View File

@@ -0,0 +1,243 @@
package com.xmzs.common.wechat.utils.tools;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.vdurmont.emoji.EmojiParser;
import com.xmzs.common.wechat.utils.Config;
import com.xmzs.common.wechat.utils.enums.OsNameEnum;
/**
* 常用工具类
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月8日 下午10:59:55
* @version 1.0
*
*/
public class CommonTools {
public static boolean printQr(String qrPath) {
switch (Config.getOsNameEnum()) {
case WINDOWS:
if (Config.getOsNameEnum().equals(OsNameEnum.WINDOWS)) {
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("cmd /c start " + qrPath);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case MAC:
if (Config.getOsNameEnum().equals(OsNameEnum.MAC)) {
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("open " + qrPath);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
default:
break;
}
return true;
}
public static boolean clearScreen() {
switch (Config.getOsNameEnum()) {
case WINDOWS:
if (Config.getOsNameEnum().equals(OsNameEnum.WINDOWS)) {
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("cmd /c " + "cls");
} catch (Exception e) {
e.printStackTrace();
}
}
break;
default:
break;
}
return true;
}
/**
* 正则表达式处理工具
*
* @author https://github.com/yaphone
* @date 2017年4月9日 上午12:27:10
* @return
*/
public static Matcher getMatcher(String regEx, String text) {
Pattern pattern = Pattern.compile(regEx);
Matcher matcher = pattern.matcher(text);
return matcher;
}
/**
* xml解析器
*
* @author https://github.com/yaphone
* @date 2017年4月9日 下午6:24:25
* @param text
* @return
*/
public static Document xmlParser(String text) {
Document doc = null;
StringReader sr = new StringReader(text);
InputSource is = new InputSource(sr);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
doc = builder.parse(is);
} catch (Exception e) {
e.printStackTrace();
}
return doc;
}
public static JSONObject structFriendInfo(JSONObject userObj) {
Map<String, Object> friendInfoTemplate = new HashMap<String, Object>();
friendInfoTemplate.put("UserName", "");
friendInfoTemplate.put("City", "");
friendInfoTemplate.put("DisplayName", "");
friendInfoTemplate.put("PYQuanPin", "");
friendInfoTemplate.put("RemarkPYInitial", "");
friendInfoTemplate.put("Province", "");
friendInfoTemplate.put("KeyWord", "");
friendInfoTemplate.put("RemarkName", "");
friendInfoTemplate.put("PYInitial", "");
friendInfoTemplate.put("EncryChatRoomId", "");
friendInfoTemplate.put("Alias", "");
friendInfoTemplate.put("Signature", "");
friendInfoTemplate.put("NickName", "");
friendInfoTemplate.put("RemarkPYQuanPin", "");
friendInfoTemplate.put("HeadImgUrl", "");
friendInfoTemplate.put("UniFriend", 0);
friendInfoTemplate.put("Sex", 0);
friendInfoTemplate.put("AppAccountFlag", 0);
friendInfoTemplate.put("VerifyFlag", 0);
friendInfoTemplate.put("ChatRoomId", 0);
friendInfoTemplate.put("HideInputBarFlag", 0);
friendInfoTemplate.put("AttrStatus", 0);
friendInfoTemplate.put("SnsFlag", 0);
friendInfoTemplate.put("MemberCount", 0);
friendInfoTemplate.put("OwnerUin", 0);
friendInfoTemplate.put("ContactFlag", 0);
friendInfoTemplate.put("Uin", 0);
friendInfoTemplate.put("StarFriend", 0);
friendInfoTemplate.put("Statues", 0);
friendInfoTemplate.put("MemberList", new ArrayList<Object>());
JSONObject r = new JSONObject();
Set<String> keySet = friendInfoTemplate.keySet();
for (String key : keySet) {
if (userObj.containsKey(key)) {
r.put(key, userObj.get(key));
} else {
r.put(key, friendInfoTemplate.get(key));
}
}
return r;
}
public static String getSynckey(JSONObject obj) {
JSONArray obj2 = obj.getJSONArray("List");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < obj2.size(); i++) {
JSONObject obj3 = (JSONObject) JSON.toJSON(obj2.get(i));
sb.append(obj3.get("Val") + "|");
}
return sb.substring(0, sb.length() - 1); // 656159784|656159911|656159873|1491905341
}
public static JSONObject searchDictList(List<JSONObject> list, String key, String value) {
JSONObject r = null;
for (JSONObject i : list) {
if (i.getString(key).equals(value)) {
r = i;
break;
}
}
return r;
}
/**
* 处理emoji表情
*
* @author https://github.com/yaphone
* @date 2017年4月23日 下午2:39:04
* @param d
* @param k
*/
public static void emojiFormatter(JSONObject d, String k) {
Matcher matcher = getMatcher("<span class=\"emoji emoji(.{1,10})\"></span>", d.getString(k));
StringBuilder sb = new StringBuilder();
String content = d.getString(k);
int lastStart = 0;
while (matcher.find()) {
String str = matcher.group(1);
if (str.length() == 6) {
} else if (str.length() == 10) {
} else {
str = "&#x" + str + ";";
String tmp = content.substring(lastStart, matcher.start());
sb.append(tmp + str);
lastStart = matcher.end();
}
}
if (lastStart < content.length()) {
sb.append(content.substring(lastStart));
}
if (sb.length() != 0) {
d.put(k, EmojiParser.parseToUnicode(sb.toString()));
} else {
d.put(k, content);
}
}
/**
* 消息格式化
*
* @author https://github.com/yaphone
* @date 2017年4月23日 下午4:19:08
* @param d
* @param k
*/
public static void msgFormatter(JSONObject d, String k) {
d.put(k, d.getString(k).replace("<br/>", "\n"));
emojiFormatter(d, k);
// TODO 与emoji表情有部分兼容问题目前暂未处理解码处理 d.put(k,
// StringEscapeUtils.unescapeHtml4(d.getString(k)));
}
}

View File

@@ -0,0 +1,80 @@
package com.xmzs.common.wechat.utils.tools;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.apache.http.HttpEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import com.xmzs.common.wechat.beans.BaseMsg;
import com.xmzs.common.wechat.core.Core;
import com.xmzs.common.wechat.utils.MyHttpClient;
import com.xmzs.common.wechat.utils.enums.MsgTypeEnum;
import com.xmzs.common.wechat.utils.enums.URLEnum;
/**
* 下载工具类
*
* @author https://github.com/yaphone
* @date 创建时间2017年4月21日 下午11:18:46
* @version 1.0
*
*/
public class DownloadTools {
private static Logger logger = Logger.getLogger("DownloadTools");
private static Core core = Core.getInstance();
private static MyHttpClient myHttpClient = core.getMyHttpClient();
/**
* 处理下载任务
*
* @author https://github.com/yaphone
* @date 2017年4月21日 下午11:00:25
* @param url
* @param msgId
* @param path
* @return
*/
public static Object getDownloadFn(BaseMsg msg, String type, String path) {
Map<String, String> headerMap = new HashMap<String, String>();
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
String url = "";
if (type.equals(MsgTypeEnum.PIC.getType())) {
url = String.format(URLEnum.WEB_WX_GET_MSG_IMG.getUrl(), (String) core.getLoginInfo().get("url"));
} else if (type.equals(MsgTypeEnum.VOICE.getType())) {
url = String.format(URLEnum.WEB_WX_GET_VOICE.getUrl(), (String) core.getLoginInfo().get("url"));
} else if (type.equals(MsgTypeEnum.VIEDO.getType())) {
headerMap.put("Range", "bytes=0-");
url = String.format(URLEnum.WEB_WX_GET_VIEDO.getUrl(), (String) core.getLoginInfo().get("url"));
} else if (type.equals(MsgTypeEnum.MEDIA.getType())) {
headerMap.put("Range", "bytes=0-");
url = String.format(URLEnum.WEB_WX_GET_MEDIA.getUrl(), (String) core.getLoginInfo().get("fileUrl"));
params.add(new BasicNameValuePair("sender", msg.getFromUserName()));
params.add(new BasicNameValuePair("mediaid", msg.getMediaId()));
params.add(new BasicNameValuePair("filename", msg.getFileName()));
}
params.add(new BasicNameValuePair("msgid", msg.getNewMsgId()));
params.add(new BasicNameValuePair("skey", (String) core.getLoginInfo().get("skey")));
HttpEntity entity = myHttpClient.doGet(url, params, true, headerMap);
try {
OutputStream out = new FileOutputStream(path);
byte[] bytes = EntityUtils.toByteArray(entity);
out.write(bytes);
out.flush();
out.close();
// Tools.printQr(path);
} catch (Exception e) {
logger.info(e.getMessage());
return false;
}
return null;
};
}

View File

@@ -14,7 +14,9 @@
<module>ruoyi-demo</module>
<module>ruoyi-generator</module>
<module>ruoyi-job</module>
<module>ruoyi-midjourney</module>
<module>ruoyi-system</module>
<module>ruoyi-live</module>
</modules>
<artifactId>ruoyi-modules</artifactId>

View File

@@ -0,0 +1,50 @@
<!--
~ MIT License
~
~ Copyright (c) 2023 OrdinaryRoad
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons</artifactId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<artifactId>live-chat-client-commons-base</artifactId>
<name>ordinaryroad-live-chat-client-commons-base</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,32 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.constant;
/**
* @author mjz
* @date 2023/8/26
*/
public class Constants {
}

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.exception;
/**
* @author mjz
* @date 2023/9/5
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String message) {
super(message);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 连接回调
*
* @author mjz
* @date 2023/8/26
*/
public interface IBaseConnectionListener<T> {
/**
* 连接建立成功
*/
default void onConnected(T t) {
// ignore
}
/**
* 连接建立失败
*
* @param t
*/
default void onConnectFailed(T t) {
// ignore
}
/**
* 连接断开
*
* @param t
*/
default void onDisconnected(T t) {
// ignore
}
}

View File

@@ -0,0 +1,149 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
import tech.ordinaryroad.live.chat.client.commons.base.msg.BaseCmdMsg;
import tech.ordinaryroad.live.chat.client.commons.base.msg.BaseMsg;
import tech.ordinaryroad.live.chat.client.commons.base.msg.ICmdMsg;
import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
/**
* Base消息回调
*
* @author mjz
* @date 2023/8/26
*/
public interface IBaseMsgListener<T, CmdEnum extends Enum<CmdEnum>> {
/**
* 收到消息(所有消息)
*
* @param msg IMsg
*/
default void onMsg(T t, IMsg msg) {
this.onMsg(msg);
}
default void onMsg(IMsg msg) {
// ignore
}
/**
* 收到cmd消息所有cmd
*
* @param cmd CmdEnum
* @param cmdMsg BaseCmdMsg
*/
default void onCmdMsg(T t, CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
this.onCmdMsg(cmd, cmdMsg);
}
default void onCmdMsg(CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
// ignore
}
/**
* 收到其他cmd消息存在Enum但Listener没有对应的回调
*
* @param cmd CmdEnum
* @param cmdMsg BaseCmdMsg
*/
default void onOtherCmdMsg(T t, CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
this.onOtherCmdMsg(cmd, cmdMsg);
}
default void onOtherCmdMsg(CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
// ignore
}
/**
* 收到未知cmd消息
*
* @param cmdString 实际收到的cmd字符串
* @param msg BaseMsg
*/
default void onUnknownCmd(T t, String cmdString, IMsg msg) {
this.onUnknownCmd(cmdString, msg);
}
default void onUnknownCmd(String cmdString, IMsg msg) {
// ignore
}
/**
* 收到cmd消息所有cmd
*
* @param cmd CmdEnum
* @param cmdMsg BaseCmdMsg
* @deprecated use {@link #onCmdMsg(T, Enum, ICmdMsg)}
*/
default void onCmdMsg(T t, CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
this.onCmdMsg(cmd, cmdMsg);
}
/**
* @deprecated use {@link #onCmdMsg(Enum, ICmdMsg)}
*/
default void onCmdMsg(CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
// ignore
}
/**
* 收到其他cmd消息存在Enum但Listener没有对应的回调
*
* @param cmd CmdEnum
* @param cmdMsg BaseCmdMsg
* @deprecated use {@link #onOtherCmdMsg(T, Enum, ICmdMsg)}
*/
default void onOtherCmdMsg(T t, CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
this.onOtherCmdMsg(cmd, cmdMsg);
}
/**
* @deprecated use {@link #onOtherCmdMsg(Enum, ICmdMsg)}
*/
default void onOtherCmdMsg(CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
// ignore
}
/**
* 收到未知cmd消息
*
* @param cmdString 实际收到的cmd字符串
* @param msg BaseMsg
* @deprecated use {@link #onUnknownCmd(T, String, IMsg)}
*/
default void onUnknownCmd(T t, String cmdString, BaseMsg msg) {
this.onUnknownCmd(cmdString, msg);
}
/**
* @deprecated use {@link #onUnknownCmd(String, IMsg)}
*/
default void onUnknownCmd(String cmdString, BaseMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 弹幕消息回调
*
* @author mjz
* @since 0.0.6
*/
public interface IDanmuMsgListener<T, DanmuMsg> {
/**
* 收到弹幕
*/
default void onDanmuMsg(T t, DanmuMsg msg) {
this.onDanmuMsg(msg);
}
default void onDanmuMsg(DanmuMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 进入房间消息回调
*
* @author mjz
* @date 2023/12/14
* @since 0.0.16
*/
public interface IEnterRoomMsgListener<T, EnterRoomMsg> {
/**
* 用户进入房间
*/
default void onEnterRoomMsg(T t, EnterRoomMsg msg) {
this.onEnterRoomMsg(msg);
}
default void onEnterRoomMsg(EnterRoomMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 礼物消息回调
*
* @author mjz
* @since 0.0.8
*/
public interface IGiftMsgListener<T, GiftMsg> {
/**
* 收到礼物
*/
default void onGiftMsg(T t, GiftMsg msg) {
this.onGiftMsg(msg);
}
default void onGiftMsg(GiftMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 点赞消息回调
*
* @author mjz
* @since 0.2.0
*/
public interface ILikeMsgListener<T, LikeMsg> {
/**
* 收到点赞
*/
default void onLikeMsg(T t, LikeMsg msg) {
this.onLikeMsg(msg);
}
default void onLikeMsg(LikeMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.listener;
/**
* 醒目留言消息回调
*
* @author mjz
* @date 2023/9/24
* @since 0.0.11
*/
public interface ISuperChatMsgListener<T, SuperChatMsg> {
/**
* 收到醒目留言
*/
default void onSuperChatMsg(T t, SuperChatMsg msg) {
this.onSuperChatMsg(msg);
}
default void onSuperChatMsg(SuperChatMsg msg) {
// ignore
}
}

View File

@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* @author mjz
* @date 2023/8/26
*/
public abstract class BaseCmdMsg<CmdEnum extends Enum<CmdEnum>> extends BaseMsg
implements ICmdMsg<CmdEnum> {
}

View File

@@ -0,0 +1,74 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
import java.util.HashMap;
import java.util.Map;
/**
* @author mjz
* @date 2023/8/26
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public abstract class BaseMsg implements IMsg {
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
/**
* 未知属性都放在这
*/
private final Map<String, JsonNode> unknownProperties = new HashMap<>();
@JsonAnyGetter
public Map<String, JsonNode> getUnknownProperties() {
return unknownProperties;
}
@JsonAnySetter
public void setOther(String key, JsonNode value) {
this.unknownProperties.put(key, value);
}
@Override
public String toString() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new BaseException(e);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* @author mjz
* @date 2023/10/2
*/
public interface ICmdMsg<CmdEnum extends Enum<CmdEnum>> extends IMsg {
String getCmd();
void setCmd(String cmd);
CmdEnum getCmdEnum();
}

View File

@@ -0,0 +1,67 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* @author mjz
* @date 2023/9/8
*/
public interface IDanmuMsg extends IMsg {
/**
* 粉丝牌名称
*/
String getBadgeName();
/**
* 粉丝牌等级
*/
byte getBadgeLevel();
/**
* 弹幕发送者id
*/
String getUid();
/**
* 弹幕发送者用户名
*/
String getUsername();
/**
* 弹幕发送者头像地址
*
* @since 0.0.11
*/
default String getUserAvatar() {
return null;
}
/**
* 弹幕内容
*/
String getContent();
}

View File

@@ -0,0 +1,62 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* 入房消息
*
* @author mjz
* @date 2023/12/26
* @since 0.0.16
*/
public interface IEnterRoomMsg extends IMsg {
/**
* 粉丝牌名称
*/
String getBadgeName();
/**
* 粉丝牌等级
*/
byte getBadgeLevel();
/**
* 用户id
*/
String getUid();
/**
* 用户名
*/
String getUsername();
/**
* 头像地址
*/
default String getUserAvatar() {
return null;
}
}

View File

@@ -0,0 +1,100 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* @author mjz
* @date 2023/9/8
*/
public interface IGiftMsg extends IMsg {
/**
* 粉丝牌名称
*/
default String getBadgeName() {
return "";
}
/**
* 粉丝牌等级
*/
default byte getBadgeLevel() {
return 0;
}
/**
* 发送方id
*/
String getUid();
/**
* 发送方用户名
*/
String getUsername();
/**
* 发送方头像地址
*
* @since 0.0.11
*/
default String getUserAvatar() {
return null;
}
/**
* 礼物名称
*/
String getGiftName();
/**
* 礼物图像地址
*/
String getGiftImg();
/**
* 礼物id
*/
String getGiftId();
/**
* 礼物数量
*/
int getGiftCount();
/**
* 单个礼物价格
*/
int getGiftPrice();
/**
* 接收方id
*/
String getReceiveUid();
/**
* 接收方用户名
*/
String getReceiveUsername();
}

View File

@@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* @author mjz
* @date 2024/1/31
* @since 0.2.0
*/
public interface ILikeMsg extends IMsg {
/**
* 粉丝牌名称
*/
default String getBadgeName(){
return "";
}
/**
* 粉丝牌等级
*/
default byte getBadgeLevel(){
return 0;
}
/**
* 点赞者id
*/
String getUid();
/**
* 点赞者用户名
*/
String getUsername();
/**
* 点赞者头像地址
*/
default String getUserAvatar() {
return null;
}
/**
* 点赞数
*/
default int getClickCount() {
return 1;
}
}

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
import java.io.Serializable;
/**
* @author mjz
* @date 2023/8/26
*/
public interface IMsg extends Serializable {
}

View File

@@ -0,0 +1,49 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.base.msg;
/**
* 醒目留言
*
* @author mjz
* @date 2023/9/22
*/
public interface ISuperChatMsg extends IDanmuMsg {
/**
* 醒目留言持续时间,单位秒
*/
int getDuration();
@Override
default String getBadgeName() {
return "";
}
@Override
default byte getBadgeLevel() {
return 0;
}
}

View File

@@ -0,0 +1,54 @@
<!--
~ MIT License
~
~ Copyright (c) 2023 OrdinaryRoad
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons</artifactId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<artifactId>live-chat-client-commons-client</artifactId>
<name>live-chat-client-commons-client</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons-base</artifactId>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons-util</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,200 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.client;
import lombok.Getter;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
import tech.ordinaryroad.live.chat.client.commons.client.config.BaseLiveChatClientConfig;
import tech.ordinaryroad.live.chat.client.commons.client.enums.ClientStatusEnums;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* @author mjz
* @date 2023/8/26
*/
public abstract class BaseLiveChatClient<
Config extends BaseLiveChatClientConfig,
MsgListener extends IBaseMsgListener<?, ?>
> implements IBaseLiveChatClient<MsgListener> {
private final Config config;
@Getter
private volatile ClientStatusEnums status = ClientStatusEnums.NEW;
protected PropertyChangeSupport statusChangeSupport = new PropertyChangeSupport(status);
protected volatile boolean cancelReconnect = false;
protected final List<MsgListener> msgListeners = Collections.synchronizedList(new ArrayList<>());
protected BaseLiveChatClient(Config config) {
this.config = config;
}
public Config getConfig() {
return config;
}
@Override
public void connect(Runnable success) {
this.connect(success, null);
}
@Override
public void connect() {
this.connect(null, null);
}
@Override
public void disconnect(boolean cancelReconnect) {
this.cancelReconnect = cancelReconnect;
this.disconnect();
}
@Override
public void send(Object msg) {
this.send(msg, null, null);
}
@Override
public void send(Object msg, Runnable success) {
this.send(msg, success, null);
}
@Override
public void send(Object msg, Consumer<Throwable> failed) {
this.send(msg, null, failed);
}
@Override
public void sendDanmu(Object danmu) {
this.sendDanmu(danmu, null, null);
}
@Override
public void sendDanmu(Object danmu, Runnable success) {
this.sendDanmu(danmu, success, null);
}
@Override
public void sendDanmu(Object danmu, Consumer<Throwable> failed) {
this.sendDanmu(danmu, null, failed);
}
@Override
public void clickLike(int count) {
this.clickLike(count, null, null);
}
@Override
public void clickLike(int count, Runnable success) {
this.clickLike(count, success, null);
}
@Override
public void clickLike(int count, Consumer<Throwable> failed) {
this.clickLike(count, null, failed);
}
protected abstract void tryReconnect();
protected abstract String getWebSocketUriString();
/**
* 判断是否处于某个状态,或者处于后续状态
*
* @param status {@link ClientStatusEnums}
* @return false: 还没有到达该状态
*/
protected boolean checkStatus(ClientStatusEnums status) {
return this.status.getCode() >= Objects.requireNonNull(status).getCode();
}
protected void setStatus(ClientStatusEnums status) {
ClientStatusEnums oldStatus = this.status;
if (oldStatus != status) {
this.status = status;
this.statusChangeSupport.firePropertyChange("status", oldStatus, status);
}
}
public void addStatusChangeListener(PropertyChangeListener listener) {
this.statusChangeSupport.addPropertyChangeListener(listener);
}
public void removeStatusChangeListener(PropertyChangeListener listener) {
this.statusChangeSupport.removePropertyChangeListener(listener);
}
@Override
public void destroy() {
for (PropertyChangeListener propertyChangeListener : this.statusChangeSupport.getPropertyChangeListeners()) {
this.statusChangeSupport.removePropertyChangeListener(propertyChangeListener);
}
this.msgListeners.clear();
}
@Override
public boolean addMsgListener(MsgListener msgListener) {
if (msgListener == null) {
return false;
}
return this.msgListeners.add(msgListener);
}
@Override
public boolean addMsgListeners(List<MsgListener> msgListeners) {
if (msgListeners == null || msgListeners.isEmpty()) {
return false;
}
return this.msgListeners.addAll(msgListeners);
}
@Override
public boolean removeMsgListener(MsgListener msgListener) {
if (msgListener == null) {
return false;
}
return this.msgListeners.remove(msgListener);
}
@Override
public boolean removeMsgListeners(List<MsgListener> msgListeners) {
if (msgListeners == null || msgListeners.isEmpty()) {
return false;
}
return this.msgListeners.removeAll(msgListeners);
}
@Override
public void removeAllMsgListeners() {
this.msgListeners.clear();
}
}

View File

@@ -0,0 +1,135 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.client;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
import java.util.List;
import java.util.function.Consumer;
/**
* @author mjz
* @date 2023/9/5
*/
public interface IBaseLiveChatClient<MsgListener extends IBaseMsgListener<?, ?>> {
void init();
boolean addMsgListener(MsgListener msgListener);
boolean addMsgListeners(List<MsgListener> msgListeners);
boolean removeMsgListener(MsgListener msgListener);
boolean removeMsgListeners(List<MsgListener> msgListeners);
void removeAllMsgListeners();
void connect(Runnable success, Consumer<Throwable> failed);
void connect(Runnable success);
void connect();
/**
* 手动断开连接
*
* @param cancelReconnect 取消本次的自动重连(如果启用自动重连)
*/
void disconnect(boolean cancelReconnect);
void disconnect();
void destroy();
void send(Object msg);
void send(Object msg, Runnable success, Consumer<Throwable> failed);
void send(Object msg, Runnable success);
void send(Object msg, Consumer<Throwable> failed);
/**
* 发送弹幕
*
* @param danmu 弹幕内容
* @since 0.0.6
*/
void sendDanmu(Object danmu);
/**
* 发送弹幕
*
* @param danmu 弹幕内容
* @since 0.0.6
*/
void sendDanmu(Object danmu, Runnable success, Consumer<Throwable> failed);
/**
* 发送弹幕
*
* @param danmu 弹幕内容
* @since 0.0.6
*/
void sendDanmu(Object danmu, Runnable success);
/**
* 发送弹幕
*
* @param danmu 弹幕内容
* @since 0.0.6
*/
void sendDanmu(Object danmu, Consumer<Throwable> failed);
/**
* 为直播间点赞
*
* @since 0.2.0
*/
void clickLike(int count);
/**
* 为直播间点赞
*
* @since 0.2.0
*/
void clickLike(int count, Runnable success, Consumer<Throwable> failed);
/**
* 为直播间点赞
*
* @since 0.2.0
*/
void clickLike(int count, Runnable success);
/**
* 为直播间点赞
*
* @since 0.2.0
*/
void clickLike(int count, Consumer<Throwable> failed);
}

View File

@@ -0,0 +1,139 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.client.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
/**
* 直播间弹幕客户端配置
*
* @author mjz
* @date 2023/8/26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder(toBuilder = true)
public abstract class BaseLiveChatClientConfig {
protected PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public static final long DEFAULT_HEARTBEAT_INITIAL_DELAY = 15;
public static final long DEFAULT_HEARTBEAT_PERIOD = 25;
public static final long DEFAULT_MIN_SEND_DANMU_PERIOD = 3000L;
private String websocketUri;
/**
* 浏览器中的Cookie
*/
private String cookie;
/**
* 直播间id
*/
private Object roomId;
/**
* 是否启用自动重连
*/
@Builder.Default
private boolean autoReconnect = Boolean.TRUE;
/**
* 重试延迟时间默认5s后重试
*/
@Builder.Default
private int reconnectDelay = 5;
/**
* 首次发送心跳包的延迟时间(秒)
*/
@Builder.Default
private long heartbeatInitialDelay = DEFAULT_HEARTBEAT_INITIAL_DELAY;
/**
* 心跳包发送周期(秒)
*/
@Builder.Default
private long heartbeatPeriod = DEFAULT_HEARTBEAT_PERIOD;
/**
* 最小发送弹幕时间间隔(毫秒)
*/
@Builder.Default
private long minSendDanmuPeriod = DEFAULT_MIN_SEND_DANMU_PERIOD;
public void setCookie(String cookie) {
String oldValue = this.cookie;
this.cookie = cookie;
this.propertyChangeSupport.firePropertyChange("cookie", oldValue, cookie);
}
public void setRoomId(Object roomId) {
if (!(roomId instanceof Number || roomId instanceof String)) {
throw new BaseException("房间ID仅支持数字或字符串所传参数类型" + roomId.getClass() + "值:" + roomId);
}
Object oldValue = this.roomId;
this.roomId = roomId;
this.propertyChangeSupport.firePropertyChange("roomId", oldValue, roomId);
}
public void setWebsocketUri(String websocketUri) {
String oldValue = this.websocketUri;
this.websocketUri = websocketUri;
this.propertyChangeSupport.firePropertyChange("websocketUri", oldValue, websocketUri);
}
public void setMinSendDanmuPeriod(long minSendDanmuPeriod) {
long oldValue = this.minSendDanmuPeriod;
this.minSendDanmuPeriod = minSendDanmuPeriod;
this.propertyChangeSupport.firePropertyChange("minSendDanmuPeriod", oldValue, minSendDanmuPeriod);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
this.propertyChangeSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
this.propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this.propertyChangeSupport.removePropertyChangeListener(listener);
}
}

View File

@@ -0,0 +1,83 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.client.enums;
/**
* @author mjz
* @date 2023/8/26
*/
public enum ClientStatusEnums {
/**
* 新创建
*/
NEW(0),
/**
* 已初始化
*/
INITIALIZED(1),
/**
* 连接中
*/
CONNECTING(100),
/**
* 重新连接中
*/
RECONNECTING(101),
/**
* 已连接
*/
CONNECTED(200),
/**
* 连接失败
*/
CONNECT_FAILED(401),
/**
* 已断开连接
*/
DISCONNECTED(400),
/**
* 已销毁
*/
DESTROYED(-1),
;
public int getCode() {
return code;
}
ClientStatusEnums(int order) {
this.code = order;
}
private final int code;
}

View File

@@ -0,0 +1,24 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons</artifactId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<artifactId>live-chat-client-commons-util</artifactId>
<name>live-chat-client-commons-util</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,80 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import java.net.HttpCookie;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author mjz
* @date 2023/8/27
*/
public class OrLiveChatCookieUtil {
public static String toString(List<HttpCookie> cookies) {
if (CollUtil.isEmpty(cookies)) {
return StrUtil.EMPTY;
}
return cookies.stream().map(httpCookie -> {
httpCookie.setVersion(0);
return httpCookie.toString();
}).collect(Collectors.joining("; "));
}
public static Map<String, String> parseCookieString(String cookies) {
Map<String, String> map = new HashMap<>();
if (StrUtil.isNotBlank(cookies) && !StrUtil.isNullOrUndefined(cookies)) {
try {
String[] split = cookies.split("; ");
for (String s : split) {
String[] split1 = s.split("=");
map.put(split1[0], split1[1]);
}
} catch (Exception e) {
throw new RuntimeException("cookie解析失败 " + cookies, e);
}
}
return map;
}
public static String getCookieByName(Map<String, String> cookieMap, String name, Supplier<String> supplier) {
String str = MapUtil.getStr(cookieMap, name);
return str == null ? supplier.get() : str;
}
public static String getCookieByName(String cookie, String name, Supplier<String> supplier) {
String str = MapUtil.getStr(parseCookieString(cookie), name);
return str == null ? supplier.get() : str;
}
}

View File

@@ -0,0 +1,40 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
/**
* @author mjz
* @date 2023/12/2
*/
public class OrLiveChatNumberUtil extends NumberUtil {
public static long parseLong(Object object){
return NumberUtil.parseLong(StrUtil.toStringOrNull(object));
}
}

View File

@@ -0,0 +1,50 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.util;
import cn.hutool.core.util.ReflectUtil;
import java.lang.reflect.Method;
/**
* @author mjz
* @date 2023/8/28
*/
public class OrLiveChatReflectUtil extends ReflectUtil {
public static Method getGetterMethod(Class<?> objectClass, String key) {
Method method;
if (key.startsWith("is")) {
method = ReflectUtil.getMethodByNameIgnoreCase(objectClass, key);
if (method == null) {
ReflectUtil.getMethodByNameIgnoreCase(objectClass, "get" + key);
}
} else {
method = ReflectUtil.getMethodByNameIgnoreCase(objectClass, "get" + key);
}
return method;
}
}

View File

@@ -0,0 +1,54 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.commons.util;
import cn.hutool.core.date.LocalDateTimeUtil;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
* @author mjz
* @date 2023/9/7
*/
public class OrLocalDateTimeUtil extends LocalDateTimeUtil {
public static ZoneId ZONE_ID_CTT = ZoneId.of(ZoneId.SHORT_IDS.get("CTT"));
/**
* 获取中国标准时间的当前时间戳(毫秒)
*/
public static long zonedCurrentTimeMillis() {
ZonedDateTime now = ZonedDateTime.now(ZONE_ID_CTT);
return now.toEpochSecond() * 1000 + now.getNano() / 1_000_000;
}
/**
* 获取中国标准时间的当前时间戳(秒)
*/
public static long zonedCurrentTimeSecs() {
return ZonedDateTime.now(ZONE_ID_CTT).toEpochSecond();
}
}

View File

@@ -0,0 +1,43 @@
<!--
~ MIT License
~
~ Copyright (c) 2023 OrdinaryRoad
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>ruoyi-live</artifactId>
<version>1.0.0</version>
</parent>
<packaging>pom</packaging>
<artifactId>live-chat-client-commons</artifactId>
<modules>
<module>live-chat-client-commons-base</module>
<module>live-chat-client-commons-util</module>
<module>live-chat-client-commons-client</module>
</modules>
</project>

View File

@@ -0,0 +1,54 @@
<!--
~ MIT License
~
~ Copyright (c) 2023 OrdinaryRoad
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-servers</artifactId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<artifactId>live-chat-client-servers-netty-client</artifactId>
<name>live-chat-client-servers-netty</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons-client</artifactId>
</dependency>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-servers-netty</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,349 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.servers.netty.client.base;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseConnectionListener;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
import tech.ordinaryroad.live.chat.client.commons.client.BaseLiveChatClient;
import tech.ordinaryroad.live.chat.client.commons.client.enums.ClientStatusEnums;
import tech.ordinaryroad.live.chat.client.servers.netty.client.config.BaseNettyClientConfig;
import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseBinaryFrameHandler;
import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseConnectionHandler;
import javax.net.ssl.SSLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* @author mjz
* @date 2023/8/26
*/
@Slf4j
public abstract class BaseNettyClient
<Config extends BaseNettyClientConfig,
CmdEnum extends Enum<CmdEnum>,
Msg extends IMsg,
MsgListener extends IBaseMsgListener<BinaryFrameHandler, CmdEnum>,
ConnectionHandler extends BaseConnectionHandler<ConnectionHandler>,
BinaryFrameHandler extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener>
>
extends BaseLiveChatClient<Config, MsgListener> {
@Getter
private final EventLoopGroup workerGroup;
@Getter
private final Bootstrap bootstrap = new Bootstrap();
private BinaryFrameHandler binaryFrameHandler;
private ConnectionHandler connectionHandler;
private IBaseConnectionListener<ConnectionHandler> connectionListener;
private Channel channel;
@Getter
private URI websocketUri;
protected IBaseConnectionListener<ConnectionHandler> clientConnectionListener;
/**
* 控制弹幕发送频率
*/
private volatile long lastSendDanmuTimeInMillis;
public abstract ConnectionHandler initConnectionHandler(IBaseConnectionListener<ConnectionHandler> clientConnectionListener);
public abstract BinaryFrameHandler initBinaryFrameHandler();
protected BaseNettyClient(Config config, EventLoopGroup workerGroup, IBaseConnectionListener<ConnectionHandler> connectionListener) {
super(config);
this.workerGroup = workerGroup;
this.connectionListener = connectionListener;
}
public void onConnected(ConnectionHandler connectionHandler) {
this.setStatus(ClientStatusEnums.CONNECTED);
if (this.connectionListener != null) {
this.connectionListener.onConnected(connectionHandler);
}
}
public void onConnectFailed(ConnectionHandler connectionHandler) {
this.setStatus(ClientStatusEnums.CONNECT_FAILED);
tryReconnect();
if (this.connectionListener != null) {
this.connectionListener.onConnectFailed(connectionHandler);
}
}
public void onDisconnected(ConnectionHandler connectionHandler) {
this.setStatus(ClientStatusEnums.DISCONNECTED);
tryReconnect();
if (this.connectionListener != null) {
this.connectionListener.onDisconnected(connectionHandler);
}
}
@Override
public void init() {
if (checkStatus(ClientStatusEnums.INITIALIZED)) {
return;
}
try {
this.websocketUri = new URI(getWebSocketUriString());
SslContext sslCtx = SslContextBuilder.forClient().build();
this.clientConnectionListener = new IBaseConnectionListener<ConnectionHandler>() {
@Override
public void onConnected(ConnectionHandler connectionHandler) {
BaseNettyClient.this.onConnected(connectionHandler);
}
@Override
public void onConnectFailed(ConnectionHandler connectionHandler) {
BaseNettyClient.this.onConnectFailed(connectionHandler);
}
@Override
public void onDisconnected(ConnectionHandler connectionHandler) {
BaseNettyClient.this.onDisconnected(connectionHandler);
}
};
this.binaryFrameHandler = this.initBinaryFrameHandler();
this.connectionHandler = this.initConnectionHandler(this.clientConnectionListener);
this.bootstrap.group(this.workerGroup)
// 创建Channel
.channel(NioSocketChannel.class)
.remoteAddress(this.websocketUri.getHost(), getInetPort())
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
// Channel配置
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 责任链
ChannelPipeline pipeline = ch.pipeline();
// 放到第一位 addFirst 支持wss链接服务端
pipeline.addFirst(sslCtx.newHandler(ch.alloc(), BaseNettyClient.this.websocketUri.getHost(), getInetPort()));
// 添加一个http的编解码器
pipeline.addLast(new HttpClientCodec());
// 添加一个用于支持大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 添加一个聚合器这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
pipeline.addLast(new HttpObjectAggregator(BaseNettyClient.this.getConfig().getAggregatorMaxContentLength()));
// 连接处理器
pipeline.addLast(BaseNettyClient.this.connectionHandler);
// 弹幕处理器
pipeline.addLast(BaseNettyClient.this.binaryFrameHandler);
}
});
this.setStatus(ClientStatusEnums.INITIALIZED);
} catch (URISyntaxException e) {
throw new BaseException(e);
} catch (SSLException e) {
throw new BaseException(e);
}
}
private int getInetPort() {
int port = this.websocketUri.getPort();
return port == -1 ? "wss".equalsIgnoreCase(websocketUri.getScheme()) ? 443 : 80 : port;
}
@Override
public void connect(Runnable success, Consumer<Throwable> failed) {
if (this.cancelReconnect) {
this.cancelReconnect = false;
}
if (!checkStatus(ClientStatusEnums.INITIALIZED)) {
return;
}
if (getStatus() == ClientStatusEnums.CONNECTED) {
return;
}
if (getStatus() != ClientStatusEnums.RECONNECTING) {
this.setStatus(ClientStatusEnums.CONNECTING);
}
this.bootstrap.connect().addListener((ChannelFutureListener) connectFuture -> {
if (connectFuture.isSuccess()) {
if (log.isDebugEnabled()) {
log.debug("连接建立成功!");
}
this.channel = connectFuture.channel();
// 监听是否握手成功
this.connectionHandler.getHandshakeFuture().addListener((ChannelFutureListener) handshakeFuture -> {
try {
connectionHandler.sendAuthRequest(channel);
if (success != null) {
success.run();
}
} catch (Exception e) {
log.error("认证包发送失败,断开连接", e);
this.disconnect();
}
});
} else {
log.error("连接建立失败", connectFuture.cause());
this.onConnectFailed(this.connectionHandler);
if (failed != null) {
failed.accept(connectFuture.cause());
}
}
});
}
@Override
public void disconnect() {
if (this.channel == null) {
return;
}
this.channel.close();
}
@Override
protected void tryReconnect() {
if (this.cancelReconnect) {
this.cancelReconnect = false;
return;
}
if (!getConfig().isAutoReconnect()) {
return;
}
if (log.isWarnEnabled()) {
log.warn("{}s后将重新连接 {}", getConfig().getReconnectDelay(), getConfig().getRoomId());
}
workerGroup.schedule(() -> {
this.setStatus(ClientStatusEnums.RECONNECTING);
this.connect();
}, getConfig().getReconnectDelay(), TimeUnit.SECONDS);
}
@Override
public void send(Object msg, Runnable success, Consumer<Throwable> failed) {
ChannelFuture future = this.channel.writeAndFlush(msg);
if (success != null || failed != null) {
future.addListener((ChannelFutureListener) channelFuture -> {
if (channelFuture.isSuccess()) {
if (success != null) {
success.run();
}
} else {
if (failed != null) {
failed.accept(channelFuture.cause());
}
}
});
}
}
@Override
public void destroy() {
super.destroy();
// 销毁时不需要重连
this.cancelReconnect = true;
workerGroup.shutdownGracefully().addListener(future -> {
if (future.isSuccess()) {
this.setStatus(ClientStatusEnums.DESTROYED);
} else {
throw new BaseException("client销毁失败", future.cause());
}
});
}
@Override
protected String getWebSocketUriString() {
return getConfig().getWebsocketUri();
}
@Override
protected void setStatus(ClientStatusEnums status) {
if (log.isDebugEnabled()) {
if (getStatus() != status) {
log.debug("{} 状态变化 {} => {}\n", getClass().getSimpleName(), getStatus(), status);
}
}
super.setStatus(status);
}
@Override
public void sendDanmu(Object danmu, Runnable success, Consumer<Throwable> failed) {
throw new BaseException("暂未支持该功能");
}
@Override
public void clickLike(int count, Runnable success, Consumer<Throwable> failed) {
throw new BaseException("暂未支持该功能");
}
/**
* 发送弹幕前判断是否可以发送
*
* @param checkConnected 是否检查Client连接状态
*/
protected boolean checkCanSendDanmu(boolean checkConnected) {
if (checkConnected && getStatus() != ClientStatusEnums.CONNECTED) {
throw new BaseException("连接未建立,无法发送弹幕");
}
if (System.currentTimeMillis() - this.lastSendDanmuTimeInMillis <= getConfig().getMinSendDanmuPeriod()) {
if (log.isWarnEnabled()) {
log.warn("发送弹幕频率过快,忽略该次发送");
}
return false;
}
return true;
}
protected boolean checkCanSendDanmu() {
return checkCanSendDanmu(true);
}
/**
* 发送弹幕后调用该方法
*/
protected void finishSendDanmu() {
this.lastSendDanmuTimeInMillis = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("弹幕发送完成");
}
}
public void iteratorMsgListeners(Consumer<MsgListener> consumer) {
binaryFrameHandler.iteratorMsgListeners(consumer);
}
}

View File

@@ -0,0 +1,65 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.servers.netty.client.config;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import tech.ordinaryroad.live.chat.client.commons.client.config.BaseLiveChatClientConfig;
import java.net.URI;
/**
* @author mjz
* @date 2023/8/26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder(toBuilder = true)
public abstract class BaseNettyClientConfig extends BaseLiveChatClientConfig {
/**
* 聚合器允许的最大消息体长度,默认 64*1024 byte
*
* @see HttpObjectAggregator#HttpObjectAggregator(int)
*/
@Builder.Default
private int aggregatorMaxContentLength = 64 * 1024;
/**
* WebSocketClientHandshaker最大消息体长度默认 64*1024 byte
*
* @see WebSocketClientHandshakerFactory#newHandshaker(URI, WebSocketVersion, String, boolean, HttpHeaders, int)
*/
@Builder.Default
private int maxFramePayloadLength = 64 * 1024;
}

View File

@@ -0,0 +1,66 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.servers.netty.client.handler;
import lombok.Getter;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
import tech.ordinaryroad.live.chat.client.servers.netty.client.base.BaseNettyClient;
import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseBinaryFrameHandler;
import java.util.List;
/**
* BaseClientBinaryFrameHandler
*
* @author mjz
* @date 2023/8/30
*/
public abstract class BaseNettyClientBinaryFrameHandler<
Client extends BaseNettyClient<?, ?, ?, ?, ?, ?>,
BinaryFrameHandler extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener>,
CmdEnum extends Enum<CmdEnum>,
Msg extends IMsg,
MsgListener extends IBaseMsgListener<BinaryFrameHandler, CmdEnum>>
extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener> {
@Getter
protected final Client client;
public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, Client client, long roomId) {
super(msgListeners, roomId);
this.client = client;
}
public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, Client client) {
super(msgListeners, client.getConfig().getRoomId());
this.client = client;
}
public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, long roomId) {
super(msgListeners, roomId);
this.client = null;
}
}

View File

@@ -0,0 +1,65 @@
/*
* MIT License
*
* Copyright (c) 2023 OrdinaryRoad
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package tech.ordinaryroad.live.chat.client.servers.netty.client.handler;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import lombok.Getter;
import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseConnectionListener;
import tech.ordinaryroad.live.chat.client.servers.netty.client.base.BaseNettyClient;
import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseConnectionHandler;
/**
* BaseClientConnectionHandler
*
* @author mjz
* @date 2023/8/27
*/
public abstract class BaseNettyClientConnectionHandler<
Client extends BaseNettyClient<?, ?, ?, ?, ?, ?>,
ConnectionHandler extends BaseConnectionHandler<ConnectionHandler>>
extends BaseConnectionHandler<ConnectionHandler> {
@Getter
protected final Client client;
public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, Client client, IBaseConnectionListener<ConnectionHandler> listener) {
super(handshaker, listener);
this.client = client;
}
public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, Client client) {
this(handshaker, client, null);
}
public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, IBaseConnectionListener<ConnectionHandler> listener) {
super(handshaker, listener);
this.client = null;
}
public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, long roomId) {
super(handshaker, null);
this.client = null;
}
}

View File

@@ -0,0 +1,58 @@
<!--
~ MIT License
~
~ Copyright (c) 2023 OrdinaryRoad
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-servers</artifactId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<artifactId>live-chat-client-servers-netty</artifactId>
<name>live-chat-client-servers-netty</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.xmzs</groupId>
<artifactId>live-chat-client-commons-base</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
</project>

Some files were not shown because too many files have changed in this diff Show More