Commit 84825579b6f5a15231d2bbb884cda61c020d1c6c

Authored by cherishsince
1 parent bd8e6c2b

😄模拟发送、接收discard消息成功!

Showing 23 changed files with 1041 additions and 30 deletions
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/MjHttpExecute.java deleted
1 -package cn.iocoder.yudao.framework.ai.Midjourney;  
2 -  
3 -import org.springframework.http.HttpHeaders;  
4 -import org.springframework.http.MediaType;  
5 -import org.springframework.web.client.RestTemplate;  
6 -  
7 -public class MjHttpExecute implements MjExecute {  
8 -  
9 - private static final String URL = "https://discord.com/";  
10 -  
11 - private RestTemplate restTemplate = new RestTemplate();  
12 -  
13 -  
14 - @Override  
15 - public boolean execute(MjCommandEnum mjCommand, String prompt) {  
16 - // 发送的 uri  
17 - String uri = "api/v9/interactions";  
18 - // restTemplate 发送post请求  
19 - String result = restTemplate.postForObject(URL + uri, prompt, String.class);  
20 - // 加载当前目录下文件  
21 -  
22 - return false;  
23 - }  
24 -}  
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/DiscordJadMain.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney;
  2 +
  3 +/**
  4 + * author: fansili
  5 + * time: 2024/4/3 15:54
  6 + */
  7 +public class DiscordJadMain {
  8 +
  9 +
  10 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/DiscordWebSocketClient.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney;
  2 +import cn.hutool.http.useragent.UserAgent;
  3 +import cn.hutool.http.useragent.UserAgentUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import com.alibaba.fastjson.JSON;
  6 +import org.springframework.web.socket.*;
  7 +import org.springframework.web.socket.client.standard.StandardWebSocketClient;
  8 +import org.springframework.web.socket.handler.TextWebSocketHandler;
  9 +
  10 +import java.io.IOException;
  11 +import java.net.URI;
  12 +import java.net.URISyntaxException;
  13 +import java.nio.ByteBuffer;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.concurrent.ExecutionException;
  16 +import java.util.concurrent.Future;
  17 +
  18 +/**
  19 + * https://blog.csdn.net/qq_38490457/article/details/125250135
  20 + */
  21 +public class DiscordWebSocketClient {
  22 +
  23 + private static final String DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/?v=9&encoding=json";
  24 +
  25 + public static void main(String[] args) throws InterruptedException, ExecutionException, IOException, URISyntaxException {
  26 + StandardWebSocketClient client = new StandardWebSocketClient();
  27 + DiscordWebSocketHandler handler = new DiscordWebSocketHandler();
  28 +
  29 +
  30 + WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
  31 + headers.add("Accept-Encoding", "gzip, deflate, br");
  32 + headers.add("Accept-Language", "zh-CN,zh;q=0.9");
  33 + headers.add("Cache-Control", "no-cache");
  34 + headers.add("Pragma", "no-cache");
  35 + headers.add("Sec-Websocket-Extensions", "permessage-deflate; client_max_window_bits");
  36 + headers.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36");
  37 +
  38 + Future<WebSocketSession> futureSession = client.doHandshake(handler, headers, new URI(DISCORD_GATEWAY_URL));
  39 + WebSocketSession session = futureSession.get(); // 这会阻塞直到连接建立
  40 +
  41 + // 登录过程(你需要替换 TOKEN 为你的 Discord Bot Token)
  42 +// String token = "YOUR_DISCORD_BOT_TOKEN"; // 请替换为你的 Bot Token
  43 +// String identifyPayload = "{\"op\":2,\"d\":{\"token\":\"" + token + "\",\"properties\":{\"$os\":\"java\",\"$browser\":\"spring-websocket\",\"$device\":\"spring-websocket\"},\"compress\":false,\"large_threshold\":256,\"shard\":[0,1]}}";
  44 +// session.sendMessage(new TextMessage(identifyPayload));
  45 +
  46 + // 发送心跳以保持连接活跃
  47 + Thread heartbeatThread = new Thread(() -> {
  48 + int interval = 0; // 初始心跳间隔,后续从 Discord 服务器获取
  49 + while (!Thread.currentThread().isInterrupted()) {
  50 + try {
  51 + Thread.sleep(interval * 1000); // 等待指定的心跳间隔
  52 + session.sendMessage(new TextMessage("{\"op\":1,\"d\":null}")); // 发送心跳包
  53 + } catch (Exception e) {
  54 + e.printStackTrace();
  55 + break;
  56 + }
  57 + }
  58 + });
  59 + heartbeatThread.start();
  60 +
  61 + // 等待用户输入来保持程序运行(仅用于示例)
  62 + System.in.read();
  63 +
  64 + // 关闭连接和线程
  65 + session.close();
  66 + heartbeatThread.interrupt();
  67 + }
  68 +
  69 + private static class DiscordWebSocketHandler implements WebSocketHandler {
  70 +
  71 + @Override
  72 + public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  73 +
  74 + }
  75 +
  76 + @Override
  77 + public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
  78 + Object payload = message.getPayload();
  79 +
  80 + session.sendMessage(new TextMessage(JSON.toJSONString(createAuthData())));
  81 + String a= "";
  82 + }
  83 +
  84 + @Override
  85 + public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  86 +
  87 + }
  88 +
  89 + @Override
  90 + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
  91 +
  92 + }
  93 +
  94 + @Override
  95 + public boolean supportsPartialMessages() {
  96 + return false;
  97 + }
  98 +
  99 +
  100 + private JSONObject createAuthData() {
  101 + String userAgentStr = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
  102 + UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
  103 + String token = "NTY5MDY4NDAxNzEyOTU1Mzky.G4-Fu0.MzD-7ll-ElbXTTgDPHF-WS_UyhMAfbKN3WyyBc";
  104 +
  105 + JSONObject connectionProperties = new JSONObject()
  106 + .put("browser", userAgent.getBrowser().getName())
  107 + .put("browser_user_agent", userAgentStr)
  108 + .put("browser_version", userAgent.getVersion())
  109 + .put("client_build_number", 222963)
  110 + .put("client_event_source", null)
  111 + .put("device", "")
  112 + .put("os", userAgent.getOs().getName())
  113 + .put("referer", "https://www.midjourney.com")
  114 + .put("referrer_current", "")
  115 + .put("referring_domain", "www.midjourney.com")
  116 + .put("referring_domain_current", "")
  117 + .put("release_channel", "stable")
  118 + .put("system_locale", "zh-CN");
  119 + JSONObject presence = new JSONObject()
  120 + .put("activities", "")
  121 + .put("afk", false)
  122 + .put("since", 0)
  123 + .put("status", "online");
  124 + JSONObject clientState = new JSONObject()
  125 + .put("api_code_version", 0)
  126 + .put("guild_versions", "")
  127 + .put("highest_last_message_id", "0")
  128 + .put("private_channels_version", "0")
  129 + .put("read_state_version", 0)
  130 + .put("user_guild_settings_version", -1)
  131 + .put("user_settings_version", -1);
  132 + return new JSONObject()
  133 + .put("capabilities", 16381)
  134 + .put("client_state", clientState)
  135 + .put("compress", false)
  136 + .put("presence", presence)
  137 + .put("properties", connectionProperties)
  138 + .put("token", token);
  139 + }
  140 + }
  141 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/MidjourneyApi.java renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/MidjourneyApi.java
1 -package cn.iocoder.yudao.framework.ai.Midjourney; 1 +package cn.iocoder.yudao.framework.ai.midjourney;
2 2
3 /** 3 /**
4 * 文档: https://www.xiubbs.com/t-401-1-1.html 4 * 文档: https://www.xiubbs.com/t-401-1-1.html
5 * 5 *
6 * https://github.com/novicezk/midjourney-proxy/blob/main/README_CN.md 6 * https://github.com/novicezk/midjourney-proxy/blob/main/README_CN.md
7 * 7 *
  8 + * discord4j、https://github.com/discord-jda/JDA
  9 + *
8 */ 10 */
9 public class MidjourneyApi { 11 public class MidjourneyApi {
10 12
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/MjCommandEnum.java renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/MjCommandEnum.java
1 -package cn.iocoder.yudao.framework.ai.Midjourney; 1 +package cn.iocoder.yudao.framework.ai.midjourney;
2 2
3 import lombok.Getter; 3 import lombok.Getter;
4 4
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/MjExecute.java renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/MjExecute.java
1 -package cn.iocoder.yudao.framework.ai.Midjourney; 1 +package cn.iocoder.yudao.framework.ai.midjourney;
2 2
3 /** 3 /**
4 * mj 命令 4 * mj 命令
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/MjHttpExecute.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney;
  2 +
  3 +import cn.hutool.core.io.FileUtil;
  4 +import org.springframework.http.HttpEntity;
  5 +import org.springframework.http.HttpHeaders;
  6 +import org.springframework.http.MediaType;
  7 +import org.springframework.web.client.RestTemplate;
  8 +
  9 +import java.nio.charset.Charset;
  10 +
  11 +public class MjHttpExecute implements MjExecute {
  12 +
  13 + private static final String URL = "https://discord.com/";
  14 +
  15 + @Override
  16 + public boolean execute(MjCommandEnum mjCommand, String prompt) {
  17 + // 发送的 uri
  18 + String uri = "api/v9/interactions";
  19 + // restTemplate 发送post请求
  20 +// String result = restTemplate.postForObject(URL + uri, prompt, String.class);
  21 + // 加载当前目录下文件
  22 + return false;
  23 + }
  24 +
  25 + public static void main(String[] args) {
  26 + RestTemplate restTemplate = new RestTemplate();
  27 +
  28 + String token = "NTY5MDY4NDAxNzEyOTU1Mzky.G4-Fu0.MzD-7ll-ElbXTTgDPHF-WS_UyhMAfbKN3WyyBc";
  29 + String body = FileUtil.readString("/Users/fansili/projects/github/ruoyi-vue-pro/yudao-module-ai" +
  30 + "/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney" +
  31 + "/interactions_type2.json", Charset.forName("utf-8"));
  32 + // 创建HTTP头部
  33 + HttpHeaders headers = new HttpHeaders();
  34 + headers.setContentType(MediaType.APPLICATION_JSON); // 设置内容类型为JSON
  35 + headers.set("Authorization", token); // 如果需要,设置认证信息(例如JWT令牌)
  36 + headers.set("Referer", "https://discord.com/channels/1221445697157468200/1221445862962630706"); // 如果需要,设置认证信息(例如JWT令牌)
  37 + headers.set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"); // 如果需要,设置认证信息(例如JWT令牌)
  38 + headers.set("Cookie", "__dcfduid=6ca536c0e3fa11eeb7cbe34c31b49caf; __sdcfduid=6ca536c1e3fa11eeb7cbe34c31b49caf52cce5ffd8983d2a052cf6aba75fe5fe566f2c265902e283ce30dbf98b8c9c93; _gcl_au=1.1.245923998.1710853617; _ga=GA1.1.111061823.1710853617; __cfruid=6385bb3f48345a006b25992db7dcf984e395736d-1712124666; _cfuvid=O09la5ms0ypNptiG0iD8A6BKWlTxz1LG0WR7qRStD7o-1712124666575-0.0.1.1-604800000; locale=zh-CN; cf_clearance=l_YGod1_SUtYxpDVeZXiX7DLLPl1DYrquZe8WVltvYs-1712124668-1.0.1.1-Hl2.fToel23EpF2HCu9J20rB4D7OhhCzoajPSdo.9Up.wPxhvq22DP9RHzEBKuIUlKyH.kJLxXJfAt2N.LD5WQ; OptanonConsent=isIABGlobal=false&datestamp=Wed+Apr+03+2024+14%3A11%3A15+GMT%2B0800+(%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4)&version=6.33.0&hosts=&landingPath=https%3A%2F%2Fdiscord.com%2F&groups=C0001%3A1%2CC0002%3A1%2CC0003%3A1; _ga_Q149DFWHT7=GS1.1.1712124668.4.1.1712124679.0.0.0"); // 如果需要,设置认证信息(例如JWT令牌)
  39 + // 封装请求体和头部信息
  40 + HttpEntity<String> requestEntity = new HttpEntity<>(body, headers);
  41 +
  42 + // 定义请求URL和返回类型
  43 + String uri = "api/v9/interactions";
  44 + String res = restTemplate.postForObject(URL + uri, requestEntity, String.class);
  45 + System.err.println("11");
  46 +
  47 + //
  48 +// MjHttpExecute mjHttpExecute = new MjHttpExecute();
  49 +// mjHttpExecute.execute(null, "童话世界应该是什么样?");
  50 +
  51 + }
  52 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/interactions_type2.json renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/interactions_type2.json
1 { 1 {
2 "type": 2, 2 "type": 2,
3 "application_id": "936929561302675456", 3 "application_id": "936929561302675456",
4 - "guild_id": "1224337694918971392",  
5 - "channel_id": "1224337694918971396", 4 + "guild_id": "1221445697157468200",
  5 + "channel_id": "1221445862962630706",
6 "session_id": "696318caed5180a2210e358e44801449", 6 "session_id": "696318caed5180a2210e358e44801449",
7 "data": { 7 "data": {
8 "version": "1166847114203123795", 8 "version": "1166847114203123795",
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 { 13 {
14 "type": 3, 14 "type": 3,
15 "name": "prompt", 15 "name": "prompt",
16 - "value": "中国的是什么样子" 16 + "value": "童话世界应该是什么样?"
17 } 17 }
18 ], 18 ],
19 "application_command": { 19 "application_command": {
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/Midjourney/interactions_type3.json renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/interactions_type3.json
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/jad/Constants.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.jad;
  2 +
  3 +import lombok.experimental.UtilityClass;
  4 +
  5 +@UtilityClass
  6 +public final class Constants {
  7 + // 任务扩展属性 start
  8 + public static final String TASK_PROPERTY_NOTIFY_HOOK = "notifyHook";
  9 + public static final String TASK_PROPERTY_FINAL_PROMPT = "finalPrompt";
  10 + public static final String TASK_PROPERTY_MESSAGE_ID = "messageId";
  11 + public static final String TASK_PROPERTY_MESSAGE_HASH = "messageHash";
  12 + public static final String TASK_PROPERTY_PROGRESS_MESSAGE_ID = "progressMessageId";
  13 + public static final String TASK_PROPERTY_FLAGS = "flags";
  14 + public static final String TASK_PROPERTY_NONCE = "nonce";
  15 + public static final String TASK_PROPERTY_DISCORD_INSTANCE_ID = "discordInstanceId";
  16 + // 任务扩展属性 end
  17 +
  18 + public static final String API_SECRET_HEADER_NAME = "mj-api-secret";
  19 + public static final String DEFAULT_DISCORD_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
  20 +
  21 + public static final String MJ_MESSAGE_HANDLED = "mj_proxy_handled";
  22 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/jad/DiscordAccount.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.jad;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +
  7 +@Data
  8 +@EqualsAndHashCode(callSuper = true)
  9 +
  10 +public class DiscordAccount extends DomainObject {
  11 +
  12 +
  13 + private String guildId;
  14 +
  15 + private String channelId;
  16 +
  17 + private String userToken;
  18 +
  19 + private String userAgent = Constants.DEFAULT_DISCORD_USER_AGENT;
  20 +
  21 +
  22 + private boolean enable = true;
  23 +
  24 +
  25 + private int coreSize = 3;
  26 +
  27 + private int queueSize = 10;
  28 +
  29 + private int timeoutMinutes = 5;
  30 +
  31 + @JsonIgnore
  32 + public String getDisplay() {
  33 + return this.channelId;
  34 + }
  35 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/jad/DomainObject.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.jad;
  2 +
  3 +
  4 +import com.fasterxml.jackson.annotation.JsonIgnore;
  5 +import lombok.Getter;
  6 +import lombok.Setter;
  7 +
  8 +import java.io.Serializable;
  9 +import java.util.HashMap;
  10 +import java.util.Map;
  11 +
  12 +
  13 +public class DomainObject implements Serializable {
  14 + @Getter
  15 + @Setter
  16 + protected String id;
  17 +
  18 + @Setter
  19 + protected Map<String, Object> properties; // 扩展属性,仅支持基本类型
  20 +
  21 + @JsonIgnore
  22 + private final transient Object lock = new Object();
  23 +
  24 + public void sleep() throws InterruptedException {
  25 + synchronized (this.lock) {
  26 + this.lock.wait();
  27 + }
  28 + }
  29 +
  30 + public void awake() {
  31 + synchronized (this.lock) {
  32 + this.lock.notifyAll();
  33 + }
  34 + }
  35 +
  36 + public DomainObject setProperty(String name, Object value) {
  37 + getProperties().put(name, value);
  38 + return this;
  39 + }
  40 +
  41 + public DomainObject removeProperty(String name) {
  42 + getProperties().remove(name);
  43 + return this;
  44 + }
  45 +
  46 + public Object getProperty(String name) {
  47 + return getProperties().get(name);
  48 + }
  49 +
  50 + @SuppressWarnings("unchecked")
  51 + public <T> T getPropertyGeneric(String name) {
  52 + return (T) getProperty(name);
  53 + }
  54 +
  55 + public <T> T getProperty(String name, Class<T> clz) {
  56 + return getProperty(name, clz, null);
  57 + }
  58 +
  59 + public <T> T getProperty(String name, Class<T> clz, T defaultValue) {
  60 + Object value = getProperty(name);
  61 + return value == null ? defaultValue : clz.cast(value);
  62 + }
  63 +
  64 + public Map<String, Object> getProperties() {
  65 + if (this.properties == null) {
  66 + this.properties = new HashMap<>();
  67 + }
  68 + return this.properties;
  69 + }
  70 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/AsyncLockUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +import cn.hutool.cache.CacheUtil;
  4 +import cn.hutool.cache.impl.TimedCache;
  5 +import cn.hutool.core.thread.ThreadUtil;
  6 +import cn.iocoder.yudao.framework.ai.midjourney.jad.DomainObject;
  7 +import lombok.experimental.UtilityClass;
  8 +
  9 +import java.time.Duration;
  10 +import java.util.concurrent.ExecutionException;
  11 +import java.util.concurrent.Future;
  12 +import java.util.concurrent.TimeUnit;
  13 +import java.util.concurrent.TimeoutException;
  14 +
  15 +@UtilityClass
  16 +public class AsyncLockUtils {
  17 + private static final TimedCache<String, LockObject> LOCK_MAP = CacheUtil.newTimedCache(Duration.ofDays(1).toMillis());
  18 +
  19 + public static synchronized LockObject getLock(String key) {
  20 + return LOCK_MAP.get(key);
  21 + }
  22 +
  23 + public static LockObject waitForLock(String key, Duration duration) throws TimeoutException {
  24 + LockObject lockObject;
  25 + synchronized (LOCK_MAP) {
  26 + if (LOCK_MAP.containsKey(key)) {
  27 + lockObject = LOCK_MAP.get(key);
  28 + } else {
  29 + lockObject = new LockObject(key);
  30 + LOCK_MAP.put(key, lockObject);
  31 + }
  32 + }
  33 + Future<?> future = ThreadUtil.execAsync(() -> {
  34 + try {
  35 + lockObject.sleep();
  36 + } catch (InterruptedException e) {
  37 + Thread.currentThread().interrupt();
  38 + }
  39 + });
  40 + try {
  41 + future.get(duration.toMillis(), TimeUnit.MILLISECONDS);
  42 + } catch (InterruptedException e) {
  43 + Thread.currentThread().interrupt();
  44 + } catch (ExecutionException e) {
  45 + // do nothing
  46 + } catch (TimeoutException e) {
  47 + future.cancel(true);
  48 + throw new TimeoutException("Wait Timeout");
  49 + } finally {
  50 + LOCK_MAP.remove(lockObject.getId());
  51 + }
  52 + return lockObject;
  53 + }
  54 +
  55 + public static class LockObject extends DomainObject {
  56 +
  57 + public LockObject(String id) {
  58 + this.id = id;
  59 + }
  60 + }
  61 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/DiscordHelper.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +import cn.hutool.core.text.CharSequenceUtil;
  4 +import lombok.RequiredArgsConstructor;
  5 +import org.springframework.stereotype.Component;
  6 +
  7 +@Component
  8 +@RequiredArgsConstructor
  9 +public class DiscordHelper {
  10 + /**
  11 + * DISCORD_SERVER_URL.
  12 + */
  13 + public static final String DISCORD_SERVER_URL = "https://discord.com";
  14 + /**
  15 + * DISCORD_CDN_URL.
  16 + */
  17 + public static final String DISCORD_CDN_URL = "https://cdn.discordapp.com";
  18 + /**
  19 + * DISCORD_WSS_URL.
  20 + */
  21 + public static final String DISCORD_WSS_URL = "wss://gateway.discord.gg";
  22 + /**
  23 + * DISCORD_UPLOAD_URL.
  24 + */
  25 + public static final String DISCORD_UPLOAD_URL = "https://discord-attachments-uploads-prd.storage.googleapis.com";
  26 +
  27 + public String getServer() {
  28 + return DISCORD_SERVER_URL;
  29 + }
  30 +
  31 + public String getCdn() {
  32 + return DISCORD_CDN_URL;
  33 + }
  34 +
  35 + public String getWss() {
  36 + return DISCORD_WSS_URL;
  37 + }
  38 +
  39 + public String getMessageHash(String imageUrl) {
  40 + if (CharSequenceUtil.isBlank(imageUrl)) {
  41 + return null;
  42 + }
  43 + if (CharSequenceUtil.endWith(imageUrl, "_grid_0.webp")) {
  44 + int hashStartIndex = imageUrl.lastIndexOf("/");
  45 + if (hashStartIndex < 0) {
  46 + return null;
  47 + }
  48 + return CharSequenceUtil.sub(imageUrl, hashStartIndex + 1, imageUrl.length() - "_grid_0.webp".length());
  49 + }
  50 + int hashStartIndex = imageUrl.lastIndexOf("_");
  51 + if (hashStartIndex < 0) {
  52 + return null;
  53 + }
  54 + return CharSequenceUtil.subBefore(imageUrl.substring(hashStartIndex + 1), ".", true);
  55 + }
  56 +
  57 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/Main.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +import cn.iocoder.yudao.framework.ai.midjourney.jad.DiscordAccount;
  4 +import cn.iocoder.yudao.framework.ai.midjourney.wss.user.SpringUserWebSocketStarter;
  5 +import cn.iocoder.yudao.framework.ai.midjourney.wss.user.UserMessageListener;
  6 +
  7 +import java.util.Scanner;
  8 +
  9 +/**
  10 + * author: fansili
  11 + * time: 2024/4/3 16:40
  12 + */
  13 +public class Main {
  14 +
  15 + public static void main(String[] args) {
  16 + String token = "NTY5MDY4NDAxNzEyOTU1Mzky.G4-Fu0.MzD-7ll-ElbXTTgDPHF-WS_UyhMAfbKN3WyyBc";
  17 +
  18 +
  19 + DiscordHelper discordHelper = new DiscordHelper();
  20 + DiscordAccount discordAccount = new DiscordAccount();
  21 + discordAccount.setUserToken(token);
  22 + discordAccount.setGuildId("1221445697157468200");
  23 + discordAccount.setChannelId("1221445862962630706");
  24 +
  25 +
  26 + var messageListener = new UserMessageListener();
  27 + var webSocketStarter = new SpringUserWebSocketStarter(discordHelper.getWss(), null, discordAccount, messageListener);
  28 +
  29 + try {
  30 + webSocketStarter.start();
  31 + } catch (Exception e) {
  32 + throw new RuntimeException(e);
  33 + }
  34 +
  35 +
  36 + Scanner scanner = new Scanner(System.in);
  37 + scanner.nextLine();
  38 + }
  39 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/MessageType.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +
  4 +public enum MessageType {
  5 + /**
  6 + * 创建.
  7 + */
  8 + CREATE,
  9 + /**
  10 + * 修改.
  11 + */
  12 + UPDATE,
  13 + /**
  14 + * 删除.
  15 + */
  16 + DELETE;
  17 +
  18 + public static MessageType of(String type) {
  19 + return switch (type) {
  20 + case "MESSAGE_CREATE" -> CREATE;
  21 + case "MESSAGE_UPDATE" -> UPDATE;
  22 + case "MESSAGE_DELETE" -> DELETE;
  23 + default -> null;
  24 + };
  25 + }
  26 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/ReturnCode.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +import lombok.experimental.UtilityClass;
  4 +
  5 +@UtilityClass
  6 +public final class ReturnCode {
  7 + /**
  8 + * 成功.
  9 + */
  10 + public static final int SUCCESS = 1;
  11 + /**
  12 + * 数据未找到.
  13 + */
  14 + public static final int NOT_FOUND = 3;
  15 + /**
  16 + * 校验错误.
  17 + */
  18 + public static final int VALIDATION_ERROR = 4;
  19 + /**
  20 + * 系统异常.
  21 + */
  22 + public static final int FAILURE = 9;
  23 +
  24 + /**
  25 + * 已存在.
  26 + */
  27 + public static final int EXISTED = 21;
  28 + /**
  29 + * 排队中.
  30 + */
  31 + public static final int IN_QUEUE = 22;
  32 + /**
  33 + * 队列已满.
  34 + */
  35 + public static final int QUEUE_REJECTED = 23;
  36 + /**
  37 + * prompt包含敏感词.
  38 + */
  39 + public static final int BANNED_PROMPT = 24;
  40 +
  41 +
  42 +}
0 \ No newline at end of file 43 \ No newline at end of file
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/WebSocketStarter.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss;
  2 +
  3 +
  4 +public interface WebSocketStarter {
  5 +
  6 + void start() throws Exception;
  7 +
  8 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/user/FailureCallback.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss.user;
  2 +
  3 +
  4 +public interface FailureCallback {
  5 + void onFailure(int code, String reason);
  6 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/user/SpringUserWebSocketStarter.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss.user;
  2 +
  3 +
  4 +import cn.hutool.core.exceptions.ValidateException;
  5 +import cn.hutool.core.text.CharSequenceUtil;
  6 +import cn.hutool.core.thread.ThreadUtil;
  7 +import cn.iocoder.yudao.framework.ai.midjourney.jad.DiscordAccount;
  8 +import cn.iocoder.yudao.framework.ai.midjourney.wss.AsyncLockUtils;
  9 +import cn.iocoder.yudao.framework.ai.midjourney.wss.ReturnCode;
  10 +import cn.iocoder.yudao.framework.ai.midjourney.wss.WebSocketStarter;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.apache.tomcat.websocket.Constants;
  13 +import org.jetbrains.annotations.NotNull;
  14 +import org.springframework.util.concurrent.ListenableFutureCallback;
  15 +import org.springframework.web.socket.CloseStatus;
  16 +import org.springframework.web.socket.WebSocketHttpHeaders;
  17 +import org.springframework.web.socket.WebSocketSession;
  18 +import org.springframework.web.socket.client.standard.StandardWebSocketClient;
  19 +
  20 +import java.io.IOException;
  21 +import java.net.URI;
  22 +import java.time.Duration;
  23 +import java.util.concurrent.TimeoutException;
  24 +
  25 +@Slf4j
  26 +public class SpringUserWebSocketStarter implements WebSocketStarter {
  27 + private static final int CONNECT_RETRY_LIMIT = 5;
  28 +
  29 + private final DiscordAccount account;
  30 + private final UserMessageListener userMessageListener;
  31 + private final String wssServer;
  32 + private final String resumeWss;
  33 +
  34 + private boolean running = false;
  35 +
  36 + private WebSocketSession webSocketSession = null;
  37 + private ResumeData resumeData = null;
  38 +
  39 + public SpringUserWebSocketStarter(String wssServer, String resumeWss, DiscordAccount account, UserMessageListener userMessageListener) {
  40 + this.wssServer = wssServer;
  41 + this.resumeWss = resumeWss;
  42 + this.account = account;
  43 + this.userMessageListener = userMessageListener;
  44 + }
  45 +
  46 + @Override
  47 + public void start() throws Exception {
  48 + start(false);
  49 + }
  50 +
  51 + private void start(boolean reconnect) {
  52 + WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
  53 + headers.add("Accept-Encoding", "gzip, deflate, br");
  54 + headers.add("Accept-Language", "zh-CN,zh;q=0.9");
  55 + headers.add("Cache-Control", "no-cache");
  56 + headers.add("Pragma", "no-cache");
  57 + headers.add("Sec-Websocket-Extensions", "permessage-deflate; client_max_window_bits");
  58 + headers.add("User-Agent", this.account.getUserAgent());
  59 + var handler = new SpringWebSocketHandler(this.account, this.userMessageListener, this::onSocketSuccess, this::onSocketFailure);
  60 + String gatewayUrl;
  61 + if (reconnect) {
  62 + gatewayUrl = getGatewayServer(this.resumeData.resumeGatewayUrl()) + "/?encoding=json&v=9&compress=zlib-stream";
  63 + handler.setSessionId(this.resumeData.sessionId());
  64 + handler.setSequence(this.resumeData.sequence());
  65 + handler.setResumeGatewayUrl(this.resumeData.resumeGatewayUrl());
  66 + } else {
  67 + gatewayUrl = getGatewayServer(null) + "/?encoding=json&v=9&compress=zlib-stream";
  68 + }
  69 + var webSocketClient = new StandardWebSocketClient();
  70 + webSocketClient.getUserProperties().put(Constants.IO_TIMEOUT_MS_PROPERTY, "10000");
  71 + var socketSessionFuture = webSocketClient.doHandshake(handler, headers, URI.create(gatewayUrl));
  72 + socketSessionFuture.addCallback(new ListenableFutureCallback<>() {
  73 + @Override
  74 + public void onFailure(@NotNull Throwable e) {
  75 + onSocketFailure(SpringWebSocketHandler.CLOSE_CODE_EXCEPTION, e.getMessage());
  76 + }
  77 +
  78 + @Override
  79 + public void onSuccess(WebSocketSession session) {
  80 + SpringUserWebSocketStarter.this.webSocketSession = session;
  81 + }
  82 + });
  83 + }
  84 +
  85 + private void onSocketSuccess(String sessionId, Object sequence, String resumeGatewayUrl) {
  86 + this.resumeData = new ResumeData(sessionId, sequence, resumeGatewayUrl);
  87 + this.running = true;
  88 + notifyWssLock(ReturnCode.SUCCESS, "");
  89 + }
  90 +
  91 + private void onSocketFailure(int code, String reason) {
  92 + if (code == 1001) {
  93 + return;
  94 + }
  95 + closeSocketSessionWhenIsOpen();
  96 + if (!this.running) {
  97 + notifyWssLock(code, reason);
  98 + return;
  99 + }
  100 + this.running = false;
  101 + if (code >= 4000) {
  102 + log.warn("[wss-{}] Can't reconnect! Account disabled. Closed by {}({}).", this.account.getDisplay(), code, reason);
  103 + disableAccount();
  104 + } else if (code == 2001) {
  105 + log.warn("[wss-{}] Closed by {}({}). Try reconnect...", this.account.getDisplay(), code, reason);
  106 + tryReconnect();
  107 + } else {
  108 + log.warn("[wss-{}] Closed by {}({}). Try new connection...", this.account.getDisplay(), code, reason);
  109 + tryNewConnect();
  110 + }
  111 + }
  112 +
  113 + private void tryReconnect() {
  114 + try {
  115 + tryStart(true);
  116 + } catch (Exception e) {
  117 + if (e instanceof TimeoutException) {
  118 + closeSocketSessionWhenIsOpen();
  119 + }
  120 + log.warn("[wss-{}] Reconnect fail: {}, Try new connection...", this.account.getDisplay(), e.getMessage());
  121 + ThreadUtil.sleep(1000);
  122 + tryNewConnect();
  123 + }
  124 + }
  125 +
  126 + private void tryNewConnect() {
  127 + for (int i = 1; i <= CONNECT_RETRY_LIMIT; i++) {
  128 + try {
  129 + tryStart(false);
  130 + return;
  131 + } catch (Exception e) {
  132 + if (e instanceof TimeoutException) {
  133 + closeSocketSessionWhenIsOpen();
  134 + }
  135 + log.warn("[wss-{}] New connect fail ({}): {}", this.account.getDisplay(), i, e.getMessage());
  136 + ThreadUtil.sleep(5000);
  137 + }
  138 + }
  139 + log.error("[wss-{}] Account disabled", this.account.getDisplay());
  140 + disableAccount();
  141 + }
  142 +
  143 + public void tryStart(boolean reconnect) throws Exception {
  144 + start(reconnect);
  145 + AsyncLockUtils.LockObject lock = AsyncLockUtils.waitForLock("wss:" + this.account.getId(), Duration.ofSeconds(20));
  146 + int code = lock.getProperty("code", Integer.class, 0);
  147 + if (code == ReturnCode.SUCCESS) {
  148 + log.debug("[wss-{}] {} success.", this.account.getDisplay(), reconnect ? "Reconnect" : "New connect");
  149 + return;
  150 + }
  151 + throw new ValidateException(lock.getProperty("description", String.class));
  152 + }
  153 +
  154 + private void notifyWssLock(int code, String reason) {
  155 + AsyncLockUtils.LockObject lock = AsyncLockUtils.getLock("wss:" + this.account.getId());
  156 + if (lock != null) {
  157 + lock.setProperty("code", code);
  158 + lock.setProperty("description", reason);
  159 + lock.awake();
  160 + }
  161 + }
  162 +
  163 + private void disableAccount() {
  164 + if (Boolean.FALSE.equals(this.account.isEnable())) {
  165 + return;
  166 + }
  167 + this.account.setEnable(false);
  168 + }
  169 +
  170 + private void closeSocketSessionWhenIsOpen() {
  171 + try {
  172 + if (this.webSocketSession != null && this.webSocketSession.isOpen()) {
  173 + this.webSocketSession.close(CloseStatus.GOING_AWAY);
  174 + }
  175 + } catch (IOException e) {
  176 + // do nothing
  177 + }
  178 + }
  179 +
  180 + private String getGatewayServer(String resumeGatewayUrl) {
  181 + if (CharSequenceUtil.isNotBlank(resumeGatewayUrl)) {
  182 + return CharSequenceUtil.isBlank(this.resumeWss) ? resumeGatewayUrl : this.resumeWss;
  183 + }
  184 + return this.wssServer;
  185 + }
  186 +
  187 + public record ResumeData(String sessionId, Object sequence, String resumeGatewayUrl) {
  188 + }
  189 +}
0 \ No newline at end of file 190 \ No newline at end of file
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/user/SpringWebSocketHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss.user;
  2 +
  3 +import cn.hutool.core.text.CharSequenceUtil;
  4 +import cn.hutool.core.thread.ThreadUtil;
  5 +import cn.hutool.core.util.RandomUtil;
  6 +import cn.hutool.http.useragent.UserAgent;
  7 +import cn.hutool.http.useragent.UserAgentUtil;
  8 +import cn.iocoder.yudao.framework.ai.midjourney.jad.DiscordAccount;
  9 +import lombok.Setter;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import net.dv8tion.jda.api.utils.data.DataArray;
  12 +import net.dv8tion.jda.api.utils.data.DataObject;
  13 +import net.dv8tion.jda.api.utils.data.DataType;
  14 +import net.dv8tion.jda.internal.requests.WebSocketCode;
  15 +import net.dv8tion.jda.internal.utils.compress.Decompressor;
  16 +import net.dv8tion.jda.internal.utils.compress.ZlibDecompressor;
  17 +import org.jetbrains.annotations.NotNull;
  18 +import org.springframework.web.socket.*;
  19 +
  20 +import java.io.IOException;
  21 +import java.nio.ByteBuffer;
  22 +import java.nio.charset.StandardCharsets;
  23 +import java.util.concurrent.Executors;
  24 +import java.util.concurrent.Future;
  25 +import java.util.concurrent.ScheduledExecutorService;
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +@Slf4j
  29 +public class SpringWebSocketHandler implements WebSocketHandler {
  30 + public static final int CLOSE_CODE_RECONNECT = 2001;
  31 + public static final int CLOSE_CODE_INVALIDATE = 1009;
  32 + public static final int CLOSE_CODE_EXCEPTION = 1011;
  33 +
  34 + private final DiscordAccount account;
  35 + private final UserMessageListener userMessageListener;
  36 + private final SuccessCallback successCallback;
  37 + private final FailureCallback failureCallback;
  38 +
  39 + private final ScheduledExecutorService heartExecutor;
  40 + private final DataObject authData;
  41 +
  42 + @Setter
  43 + private String sessionId = null;
  44 + @Setter
  45 + private Object sequence = null;
  46 + @Setter
  47 + private String resumeGatewayUrl = null;
  48 +
  49 + private long interval = 41250;
  50 + private boolean heartbeatAck = false;
  51 +
  52 + private Future<?> heartbeatInterval;
  53 + private Future<?> heartbeatTimeout;
  54 +
  55 + private final Decompressor decompressor = new ZlibDecompressor(2048);
  56 +
  57 + public SpringWebSocketHandler(DiscordAccount account, UserMessageListener userMessageListener, SuccessCallback successCallback, FailureCallback failureCallback) {
  58 + this.account = account;
  59 + this.userMessageListener = userMessageListener;
  60 + this.successCallback = successCallback;
  61 + this.failureCallback = failureCallback;
  62 + this.heartExecutor = Executors.newSingleThreadScheduledExecutor();
  63 + this.authData = createAuthData();
  64 + }
  65 +
  66 + @Override
  67 + public void afterConnectionEstablished(@NotNull WebSocketSession session) throws Exception {
  68 + // do nothing
  69 + }
  70 +
  71 + @Override
  72 + public void handleTransportError(@NotNull WebSocketSession session, @NotNull Throwable e) throws Exception {
  73 + log.error("[wss-{}] Transport error", this.account.getDisplay(), e);
  74 + onFailure(CLOSE_CODE_EXCEPTION, "transport error");
  75 + }
  76 +
  77 + @Override
  78 + public void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus closeStatus) throws Exception {
  79 + onFailure(closeStatus.getCode(), closeStatus.getReason());
  80 + }
  81 +
  82 + @Override
  83 + public boolean supportsPartialMessages() {
  84 + return true;
  85 + }
  86 +
  87 + @Override
  88 + public void handleMessage(@NotNull WebSocketSession session, WebSocketMessage<?> message) throws Exception {
  89 + ByteBuffer buffer = (ByteBuffer) message.getPayload();
  90 + byte[] decompressed = decompressor.decompress(buffer.array());
  91 + if (decompressed == null) {
  92 + return;
  93 + }
  94 + String json = new String(decompressed, StandardCharsets.UTF_8);
  95 + DataObject data = DataObject.fromJson(json);
  96 + int opCode = data.getInt("op");
  97 + switch (opCode) {
  98 + case WebSocketCode.HEARTBEAT -> handleHeartbeat(session);
  99 + case WebSocketCode.HEARTBEAT_ACK -> {
  100 + this.heartbeatAck = true;
  101 + clearHeartbeatTimeout();
  102 + }
  103 + case WebSocketCode.HELLO -> {
  104 + handleHello(session, data);
  105 + doResumeOrIdentify(session);
  106 + }
  107 + case WebSocketCode.RESUME -> onSuccess();
  108 + case WebSocketCode.RECONNECT -> onFailure(CLOSE_CODE_RECONNECT, "receive server reconnect");
  109 + case WebSocketCode.INVALIDATE_SESSION -> onFailure(CLOSE_CODE_INVALIDATE, "receive session invalid");
  110 + case WebSocketCode.DISPATCH -> handleDispatch(data);
  111 + default -> log.debug("[wss-{}] Receive unknown code: {}.", account.getDisplay(), data);
  112 + }
  113 + }
  114 +
  115 + private void handleDispatch(DataObject raw) {
  116 + this.sequence = raw.opt("s").orElse(null);
  117 + if (!raw.isType("d", DataType.OBJECT)) {
  118 + return;
  119 + }
  120 + DataObject content = raw.getObject("d");
  121 + String t = raw.getString("t", null);
  122 + if ("READY".equals(t)) {
  123 + this.sessionId = content.getString("session_id");
  124 + this.resumeGatewayUrl = content.getString("resume_gateway_url");
  125 + onSuccess();
  126 + } else if ("RESUMED".equals(t)) {
  127 + onSuccess();
  128 + } else {
  129 + try {
  130 + this.userMessageListener.onMessage(raw);
  131 + } catch (Exception e) {
  132 + log.error("[wss-{}] Handle message error", this.account.getDisplay(), e);
  133 + }
  134 + }
  135 + }
  136 +
  137 + private void handleHeartbeat(WebSocketSession session) {
  138 + sendMessage(session, WebSocketCode.HEARTBEAT, this.sequence);
  139 + this.heartbeatTimeout = ThreadUtil.execAsync(() -> {
  140 + ThreadUtil.sleep(this.interval);
  141 + onFailure(CLOSE_CODE_RECONNECT, "heartbeat has not ack");
  142 + });
  143 + }
  144 +
  145 + private void handleHello(WebSocketSession session, DataObject data) {
  146 + clearHeartbeatInterval();
  147 + this.interval = data.getObject("d").getLong("heartbeat_interval");
  148 + this.heartbeatAck = true;
  149 + this.heartbeatInterval = this.heartExecutor.scheduleAtFixedRate(() -> {
  150 + if (this.heartbeatAck) {
  151 + this.heartbeatAck = false;
  152 + sendMessage(session, WebSocketCode.HEARTBEAT, this.sequence);
  153 + } else {
  154 + onFailure(CLOSE_CODE_RECONNECT, "heartbeat has not ack interval");
  155 + }
  156 + }, (long) Math.floor(RandomUtil.randomDouble(0, 1) * this.interval), this.interval, TimeUnit.MILLISECONDS);
  157 + }
  158 +
  159 + private void doResumeOrIdentify(WebSocketSession session) {
  160 + if (CharSequenceUtil.isBlank(this.sessionId)) {
  161 + sendMessage(session, WebSocketCode.IDENTIFY, this.authData);
  162 + } else {
  163 + var data = DataObject.empty().put("token", this.account.getUserToken())
  164 + .put("session_id", this.sessionId).put("seq", this.sequence);
  165 + sendMessage(session, WebSocketCode.RESUME, data);
  166 + }
  167 + }
  168 +
  169 + private void sendMessage(WebSocketSession session, int op, Object d) {
  170 + var data = DataObject.empty().put("op", op).put("d", d);
  171 + try {
  172 + session.sendMessage(new TextMessage(data.toString()));
  173 + } catch (IOException e) {
  174 + log.error("[wss-{}] Send message error", this.account.getDisplay(), e);
  175 + onFailure(CLOSE_CODE_EXCEPTION, "send message error");
  176 + }
  177 + }
  178 +
  179 + private void onSuccess() {
  180 + ThreadUtil.execute(() -> this.successCallback.onSuccess(this.sessionId, this.sequence, this.resumeGatewayUrl));
  181 + }
  182 +
  183 + private void onFailure(int code, String reason) {
  184 + clearHeartbeatTimeout();
  185 + clearHeartbeatInterval();
  186 + ThreadUtil.execute(() -> this.failureCallback.onFailure(code, reason));
  187 + }
  188 +
  189 + private void clearHeartbeatTimeout() {
  190 + if (this.heartbeatTimeout != null) {
  191 + this.heartbeatTimeout.cancel(true);
  192 + this.heartbeatTimeout = null;
  193 + }
  194 + }
  195 +
  196 + private void clearHeartbeatInterval() {
  197 + if (this.heartbeatInterval != null) {
  198 + this.heartbeatInterval.cancel(true);
  199 + this.heartbeatInterval = null;
  200 + }
  201 + }
  202 +
  203 + private DataObject createAuthData() {
  204 + UserAgent userAgent = UserAgentUtil.parse(this.account.getUserAgent());
  205 + DataObject connectionProperties = DataObject.empty()
  206 + .put("browser", userAgent.getBrowser().getName())
  207 + .put("browser_user_agent", this.account.getUserAgent())
  208 + .put("browser_version", userAgent.getVersion())
  209 + .put("client_build_number", 222963)
  210 + .put("client_event_source", null)
  211 + .put("device", "")
  212 + .put("os", userAgent.getOs().getName())
  213 + .put("referer", "https://www.midjourney.com")
  214 + .put("referrer_current", "")
  215 + .put("referring_domain", "www.midjourney.com")
  216 + .put("referring_domain_current", "")
  217 + .put("release_channel", "stable")
  218 + .put("system_locale", "zh-CN");
  219 + DataObject presence = DataObject.empty()
  220 + .put("activities", DataArray.empty())
  221 + .put("afk", false)
  222 + .put("since", 0)
  223 + .put("status", "online");
  224 + DataObject clientState = DataObject.empty()
  225 + .put("api_code_version", 0)
  226 + .put("guild_versions", DataObject.empty())
  227 + .put("highest_last_message_id", "0")
  228 + .put("private_channels_version", "0")
  229 + .put("read_state_version", 0)
  230 + .put("user_guild_settings_version", -1)
  231 + .put("user_settings_version", -1);
  232 + return DataObject.empty()
  233 + .put("capabilities", 16381)
  234 + .put("client_state", clientState)
  235 + .put("compress", false)
  236 + .put("presence", presence)
  237 + .put("properties", connectionProperties)
  238 + .put("token", this.account.getUserToken());
  239 + }
  240 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/user/SuccessCallback.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss.user;
  2 +
  3 +
  4 +public interface SuccessCallback {
  5 +
  6 + void onSuccess(String sessionId, Object sequence, String resumeGatewayUrl);
  7 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/wss/user/UserMessageListener.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.midjourney.wss.user;
  2 +
  3 +
  4 +import cn.hutool.core.thread.ThreadUtil;
  5 +import cn.iocoder.yudao.framework.ai.midjourney.wss.MessageType;
  6 +import com.alibaba.fastjson.JSON;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import net.dv8tion.jda.api.utils.data.DataObject;
  9 +
  10 +@Slf4j
  11 +public class UserMessageListener {
  12 +
  13 + public void onMessage(DataObject raw) {
  14 + MessageType messageType = MessageType.of(raw.getString("t"));
  15 + if (messageType == null || MessageType.DELETE == messageType) {
  16 + return;
  17 + }
  18 + DataObject data = raw.getObject("d");
  19 + System.err.println(data);
  20 + ThreadUtil.sleep(50);
  21 +// for (MessageHandler messageHandler : this.messageHandlers) {
  22 +// if (data.getBoolean(Constants.MJ_MESSAGE_HANDLED, false)) {
  23 +// return;
  24 +// }
  25 +// messageHandler.handle(this.instance, messageType, data);
  26 +// }
  27 + }
  28 +}