Skip to content

Commit 1392c6e

Browse files
Merge pull request #9 from NeonAngelThreads/web-support
Web dashboard backend API server implement.
2 parents 80030d9 + 9842c5d commit 1392c6e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1003
-115
lines changed

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

PluginDocs.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,14 @@ Message APIs are aimed to help you to connect one or more bot between bots, that
436436
**Step 1:**
437437
Defining a message listener on a receiver bot:
438438
ExampleMessageListener.java
439+
439440
```java
440441
import org.angellock.impl.events.IListener;
441-
import org.angellock.impl.events.dolphin.MessageBroadcastEvent;
442+
import org.angellock.impl.api.events.MessageBroadcastEvent;
442443

443444
public class ExampleMessageListener implements IListener {
444445
@EventHandler
445-
public void onMessage(MessageBroadcastEvent event){
446+
public void onMessage(MessageBroadcastEvent event) {
446447
log.info("message payload: {}", event.getMessage());
447448
// Do something...
448449
}

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@
8787
<version>RELEASE</version>
8888
<scope>provided</scope>
8989
</dependency>
90+
<dependency>
91+
<groupId>com.sun.net.httpserver</groupId>
92+
<artifactId>http</artifactId>
93+
<version>20070405</version>
94+
<scope>system</scope>
95+
<systemPath>${java.home}/lib/jrt-fs.jar</systemPath>
96+
</dependency>
97+
<!-- Java WebSocket -->
98+
<dependency>
99+
<groupId>org.java-websocket</groupId>
100+
<artifactId>Java-WebSocket</artifactId>
101+
<version>1.5.4</version>
102+
</dependency>
90103
</dependencies>
91104

92105
<build>

src/main/java/org/angellock/impl/AbstractRobot.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.angellock.impl.plugin.PluginManager;
4040
import org.angellock.impl.plugin.SessionProvider;
4141
import org.angellock.impl.util.ConsoleTokens;
42+
import org.angellock.impl.util.ProxyObject;
4243
import org.angellock.impl.util.TranslatableUtil;
4344
import org.geysermc.mcprotocollib.network.BuiltinFlags;
4445
import org.geysermc.mcprotocollib.network.ProxyInfo;
@@ -47,8 +48,12 @@
4748
import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
4849
import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket;
4950
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
51+
import org.jetbrains.annotations.Nullable;
5052
import org.slf4j.Logger;
5153
import org.slf4j.LoggerFactory;
54+
import org.slf4j.Marker;
55+
import org.slf4j.MarkerFactory;
56+
import org.slf4j.spi.LoggingEventBuilder;
5257

5358
import java.util.ArrayList;
5459
import java.util.List;
@@ -150,7 +155,6 @@ public void connect(){
150155
}
151156

152157
this.messageManager = new ChatMessageManager(this);
153-
154158
this.serverSession.addListener((IConnectListener) event -> onJoin());
155159
this.serverSession.addListener(new DisconnectReasonHandler(this));
156160
this.serverSession.addListener(new ServerChatCommandHandler(this.commands));
@@ -246,6 +250,10 @@ public AbstractRobot withProfileName(String name) {
246250
return this;
247251
}
248252

253+
public Marker getBotLabel(){
254+
return MarkerFactory.getMarker(this.getInfoHelper().getName());
255+
}
256+
249257
public Map<UUID, Player> getOnlinePlayers() {
250258
return PlayerTracker.getOnlinePlayers();
251259
}

src/main/java/org/angellock/impl/ChatMessageManager.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,18 @@ private boolean isCommand(String msg){
6060
private void sendMessagePacket(String message){
6161
if (!this.isCommand(message)) {
6262
MinecraftPacket msgPacket = new ServerboundChatPacket(message, Instant.now().toEpochMilli(), System.currentTimeMillis(), null, 0, new BitSet());
63-
log.info(TranslatableUtil.getFormattedMessage(EnumSystemEvents.CHAT_MESSAGE_SEND, message));
63+
log.info(instance.getBotLabel(), TranslatableUtil.getFormattedMessage(EnumSystemEvents.CHAT_MESSAGE_SEND, message));
6464
this.instance.sendPacket(msgPacket);
6565
} else {
6666
try {
6767
boolean valid = this.instance.commandManager.callCommand(message, instance);
6868
if (!valid) {
6969
MinecraftPacket cmd = new ServerboundChatCommandPacket(message.replaceFirst("/", ""));
70-
log.info(TranslatableUtil.getFormattedMessage(EnumSystemEvents.CHAT_COMMAND_SEND, message));
70+
log.info(instance.getBotLabel(), TranslatableUtil.getFormattedMessage(EnumSystemEvents.CHAT_COMMAND_SEND, message));
7171
this.instance.sendPacket(cmd);
7272
}
7373
} catch (Exception e) {
74-
log.warn("An exception occurred: &7{}", e.getMessage());
74+
log.warn(instance.getBotLabel(), "An exception occurred: &7{}", e.getMessage());
7575
}
7676
}
7777
}

src/main/java/org/angellock/impl/DolphinConfig.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static class OtherSettings {
5656

5757
public DolphinConfig mergeCommandOptions(Map<String, Object> commandLines) {
5858
for (Map.Entry<String, Object> opt : commandLines.entrySet()) {
59-
String value = (String) opt.getValue();
59+
String value = String.valueOf(opt.getValue());
6060
switch (opt.getKey().toLowerCase()) {
6161
case "server" -> setServer(value);
6262
case "port" -> setPort(Integer.parseInt(value));
@@ -70,7 +70,11 @@ public DolphinConfig mergeCommandOptions(Map<String, Object> commandLines) {
7070
}
7171

7272
public Locale getLanguage() {
73-
return switch (this.language.toLowerCase()) {
73+
return getLanguage(this.language);
74+
}
75+
76+
public static Locale getLanguage(String locale){
77+
return switch (locale) {
7478
case "zh" -> Locale.CHINESE;
7579
default -> Locale.ENGLISH;
7680
};

src/main/java/org/angellock/impl/RobotPlayer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ public boolean canSendMessages() {
6060

6161
@Override
6262
public void onJoin() {
63-
log.info(TranslatableUtil.getFormattedMessage(EnumSystemEvents.SERVER_CONNECTION_ESTABLISHED, this.getProfileName()));
63+
log.info(this.getBotLabel(), TranslatableUtil.getFormattedMessage(EnumSystemEvents.SERVER_CONNECTION_ESTABLISHED, this.getProfileName()));
6464
}
6565

6666
@Override
6767
public void onQuit(String reason) {
6868
long millis = System.currentTimeMillis() - this.connectTime;
69-
log.info(ConsoleTokens.colorizeText("[{}] &7Session Duration: &f{}ms"), this.getProfileName(), millis);
70-
TranslatableUtil.infoTranslatableOf(EnumSystemEvents.DISCONNECT, reason);
69+
log.info(this.getBotLabel(), ConsoleTokens.colorizeText("[{}] &7Session Duration: &f{}ms"), this.getProfileName(), millis);
70+
log.info(TranslatableUtil.getFormattedMessage(EnumSystemEvents.DISCONNECT, reason));
7171
this.getPluginManager().disableAllPlugins(this);
7272
this.getSession().getChannel().close();
7373
this.getSession().getChannel().deregister();
@@ -87,7 +87,7 @@ public void onKicked() {
8787
@Override
8888
public void onPreLogin() {
8989
this.connectTime = System.currentTimeMillis();
90-
TranslatableUtil.infoTranslatableOf(EnumSystemEvents.CONNECT, this.config().getServer(), String.valueOf(this.config().getPort()));
90+
log.info(TranslatableUtil.getFormattedMessage(EnumSystemEvents.CONNECT, this.config().getServer(), this.config().getPort()));
9191
}
9292

9393
@Override

src/main/java/org/angellock/impl/Start.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import joptsimple.NonOptionArgumentSpec;
2121
import joptsimple.OptionParser;
2222
import joptsimple.OptionSet;
23+
import lombok.Getter;
24+
import org.angellock.impl.api.HttpAPIServer;
2325
import org.angellock.impl.dolphin.GUIWindowManager;
2426
import org.angellock.impl.managers.BotManager;
2527
import org.angellock.impl.managers.ConfigManager;
@@ -46,6 +48,8 @@ public class Start {
4648
private static volatile boolean exit = false;
4749
private static final boolean win32 = System.getProperty("os.name").toLowerCase().contains("windows");
4850
private static GUIWindowManager guiManager;
51+
@Getter
52+
private static OptionSet GLOBAL_CONFIG;
4953
public static void main(String[] args) {
5054
OptionParser optionParser = new OptionParser();
5155

@@ -59,19 +63,21 @@ public static void main(String[] args) {
5963
optionParser.accepts("port").withRequiredArg().ofType(String.class);
6064
optionParser.accepts("skin-recorder").withRequiredArg().ofType(String.class);
6165
optionParser.accepts("gui");
66+
optionParser.accepts("api").withOptionalArg().ofType(Integer.class).defaultsTo(25560);
6267
ArgumentAcceptingOptionSpec<String> profilesArg = optionParser.accepts("profiles").withOptionalArg().ofType(String.class);
6368
ArgumentAcceptingOptionSpec<String> pluginDir = optionParser.accepts("plugin-dir").withOptionalArg().ofType(String.class);
6469
ArgumentAcceptingOptionSpec<String> configFile = optionParser.accepts("config-file").withOptionalArg().ofType(String.class);
6570
NonOptionArgumentSpec<String> unrecognizedOptions = optionParser.nonOptions();
66-
OptionSet parsedOption = optionParser.parse(args);
71+
GLOBAL_CONFIG = optionParser.parse(args);
6772

68-
List<?> badOptions = parsedOption.valuesOf(unrecognizedOptions);
73+
74+
List<?> badOptions = GLOBAL_CONFIG.valuesOf(unrecognizedOptions);
6975
if (!badOptions.isEmpty()){
7076
log.warn(ConsoleTokens.colorizeText("&6Omitted option arguments " + badOptions));
7177
}
7278

7379
String defaultConfigPath = Optional
74-
.ofNullable(parsedOption.valueOf(configFile))
80+
.ofNullable(GLOBAL_CONFIG.valueOf(configFile))
7581
.orElse("not-set");
7682

7783
if (Files.exists(Paths.get(defaultConfigPath))) {
@@ -80,17 +86,25 @@ public static void main(String[] args) {
8086
log.error(ConsoleTokens.colorizeText("&4The specified config file path is invalid: " + defaultConfigPath));
8187
defaultConfigPath = null;
8288
}
83-
@Nullable String profiles = (parsedOption.valueOf(profilesArg));
89+
@Nullable String profiles = (GLOBAL_CONFIG.valueOf(profilesArg));
8490

85-
ConfigManager config = new ConfigManager(parsedOption, defaultConfigPath);
91+
ConfigManager config = new ConfigManager(GLOBAL_CONFIG, defaultConfigPath);
8692
BotManager botManager = new BotManager(defaultConfigPath, ".json", config)
87-
.globalPluginManager(parsedOption.valueOf(pluginDir))
93+
.globalPluginManager(GLOBAL_CONFIG.valueOf(pluginDir))
8894
.loadProfiles(profiles);
8995

9096
Map<String, RobotPlayer> bots = BotManager.bots();
9197
getTerminal(bots.values().iterator().next());
9298

93-
if (parsedOption.has("gui")){
99+
// Start HTTP API Server
100+
int apiPort = (int) GLOBAL_CONFIG.valueOf("api");
101+
try {
102+
new HttpAPIServer(apiPort);
103+
} catch (Exception e) {
104+
log.error("Failed to start HTTP API Server on port " + apiPort, e);
105+
}
106+
107+
if (GLOBAL_CONFIG.has("gui")){
94108
guiManager = new GUIWindowManager(botManager);
95109
guiManager.startGUI();
96110
} else {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* DolphinBot - https://github.com/NeonAngelThreads/DolphinBot
3+
* Copyright (C) 2025 NeonAngelThreads (https://github.com/NeonAngelThreads)
4+
*
5+
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
6+
* License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any
7+
* later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
10+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
11+
* License for more details. You should have received a copy of the GNU General Public License along with this
12+
* program. If not, see <https://www.gnu.org/licenses/>.
13+
*
14+
* https://space.bilibili.com/386644641
15+
*/
16+
17+
package org.angellock.impl.api;
18+
19+
import com.google.gson.Gson;
20+
import com.google.gson.GsonBuilder;
21+
import com.sun.net.httpserver.HttpExchange;
22+
import com.sun.net.httpserver.HttpHandler;
23+
import com.sun.net.httpserver.HttpServer;
24+
import lombok.Getter;
25+
import lombok.extern.slf4j.Slf4j;
26+
import org.angellock.impl.RobotPlayer;
27+
import org.angellock.impl.api.websocket.LogWebSocketHandler;
28+
import org.angellock.impl.api.websocket.handlers.*;
29+
import org.angellock.impl.managers.BotManager;
30+
import org.angellock.impl.util.ConsoleTokens;
31+
import org.angellock.impl.util.reason.KickReason;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
import java.io.IOException;
36+
import java.io.InputStreamReader;
37+
import java.io.OutputStream;
38+
import java.net.InetSocketAddress;
39+
import java.nio.charset.StandardCharsets;
40+
import java.util.ArrayList;
41+
import java.util.HashMap;
42+
import java.util.List;
43+
import java.util.Map;
44+
import java.util.concurrent.Executors;
45+
46+
@Slf4j
47+
public class HttpAPIServer {
48+
@Getter
49+
private static HttpAPIServer instance;
50+
private final HttpServer server;
51+
private final LogWebSocketHandler logWebSocketServer;
52+
private final int wsPort;
53+
54+
public HttpAPIServer(int port) throws IOException {
55+
instance = this;
56+
this.wsPort = port + 1;
57+
this.server = HttpServer.create(new InetSocketAddress(port), 0);
58+
this.server.setExecutor(Executors.newCachedThreadPool());
59+
60+
registerRoutes();
61+
62+
// 启动日志WebSocket服务
63+
this.logWebSocketServer = new LogWebSocketHandler(wsPort);
64+
this.logWebSocketServer.start();
65+
66+
this.server.start();
67+
log.info(ConsoleTokens.colorizeText("&aHTTP API Server started on port &b{}&a, ready to accept requests"), port);
68+
log.info(ConsoleTokens.colorizeText("&aLog WebSocket Server started on port &b{}&a"), wsPort);
69+
}
70+
71+
private void registerRoutes() {
72+
server.createContext("/api/health", new HeartBeatHandler());
73+
server.createContext("/api/bots", new BotsHandler("GET"));
74+
server.createContext("/api/bots/start", new BotStartHandler("POST"));
75+
server.createContext("/api/bots/stop", new BotStopHandler("POST"));
76+
server.createContext("/api/bots/send-command", new CommandHandler("POST"));
77+
server.createContext("/api/config", new ConfigHandler());
78+
server.createContext("/api/bot/create", new AddedBotHandler("POST"));
79+
}
80+
81+
public void stop() {
82+
server.stop(0);
83+
try {
84+
logWebSocketServer.stop();
85+
} catch (InterruptedException e) {
86+
log.error("Failed to stop WebSocket server", e);
87+
}
88+
log.info(ConsoleTokens.colorizeText("&eHTTP API Server stopped"));
89+
log.info(ConsoleTokens.colorizeText("&eLog WebSocket Server stopped"));
90+
}
91+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* DolphinBot - https://github.com/NeonAngelThreads/DolphinBot
3+
* Copyright (C) 2025 NeonAngelThreads (https://github.com/NeonAngelThreads)
4+
*
5+
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
6+
* License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any
7+
* later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
10+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
11+
* License for more details. You should have received a copy of the GNU General Public License along with this
12+
* program. If not, see <https://www.gnu.org/licenses/>.
13+
*
14+
* https://space.bilibili.com/386644641
15+
*/
16+
17+
package org.angellock.impl.api;
18+
19+
import ch.qos.logback.classic.spi.ILoggingEvent;
20+
import ch.qos.logback.core.AppenderBase;
21+
import ch.qos.logback.core.encoder.Encoder;
22+
import lombok.Getter;
23+
import lombok.Setter;
24+
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.function.BiConsumer;
29+
import java.util.function.Consumer;
30+
31+
@Setter
32+
@Getter
33+
public class WebLogAppender extends AppenderBase<ILoggingEvent> {
34+
private Encoder<ILoggingEvent> encoder;
35+
private static final List<BiConsumer<String, String>> logListeners = new ArrayList<>();
36+
37+
@Override
38+
protected void append(ILoggingEvent eventObject) {
39+
if (!isStarted() || logListeners.isEmpty()) {
40+
return;
41+
}
42+
43+
String botName = eventObject.getMarkerList().get(0).getName();
44+
byte[] byteArray = this.encoder.encode(eventObject);
45+
String message = new String(byteArray, StandardCharsets.UTF_8);
46+
47+
// 广播日志到所有监听器
48+
for (BiConsumer<String, String> listener : logListeners) {
49+
try {
50+
listener.accept(message, botName);
51+
} catch (Exception ignore) {
52+
}
53+
}
54+
}
55+
56+
public static void addLogListener(BiConsumer<String, String> listener) {
57+
logListeners.add(listener);
58+
}
59+
60+
public static void removeLogListener(BiConsumer<String, String> listener) {
61+
logListeners.remove(listener);
62+
}
63+
64+
}

0 commit comments

Comments
 (0)