Compare commits
3 commits
a51aea8013
...
590798c0c5
Author | SHA1 | Date | |
---|---|---|---|
590798c0c5 | |||
225f708c9f | |||
9d9a6fc42f |
14 changed files with 268 additions and 48 deletions
|
@ -1,14 +1,19 @@
|
||||||
package de.hhhammer.dchat.discord.rest;
|
package de.hhhammer.dchat.discord.rest;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class DiscordRest {
|
public class DiscordRest {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DiscordRest.class);
|
||||||
private static final String USER_AGENT = "DiscordBot (https://git.hhhammer.de/hamburghammer/dchat, main)";
|
private static final String USER_AGENT = "DiscordBot (https://git.hhhammer.de/hamburghammer/dchat, main)";
|
||||||
private static final String DISCORD_URL = "https://discord.com/api/v10";
|
private static final String DISCORD_URL = "https://discord.com/api/v10";
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
@ -19,35 +24,40 @@ public class DiscordRest {
|
||||||
this.webConfig = webConfig;
|
this.webConfig = webConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postMessage(int channelId, Message message) throws IOException, InterruptedException {
|
public void postMessage(final int channelId, final Message message) throws IOException, InterruptedException {
|
||||||
var uri = URI.create(DISCORD_URL + "/channels/%s/messages".formatted(channelId));
|
final var uri = URI.create(DISCORD_URL + "/channels/%s/messages".formatted(channelId));
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
final HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("Authorization", "Bot " + webConfig.token())
|
.header("Authorization", "Bot " + webConfig.token())
|
||||||
.header("User-Agent", USER_AGENT)
|
.header("User-Agent", USER_AGENT)
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(message.toJson()))
|
.POST(HttpRequest.BodyPublishers.ofString(message.toJson()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
final HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
||||||
if (response.statusCode() != 201) {
|
if (response.statusCode() != 201) {
|
||||||
// TODO: What about errors?
|
logger.error("Unexpected status code: {}", response.statusCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMessage(int channelId, int messageId) throws IOException, InterruptedException {
|
public Optional<String> getMessage(int channelId, int messageId) throws IOException, InterruptedException {
|
||||||
var uri = URI.create(DISCORD_URL + "/channels/%s/messages/%s".formatted(channelId, messageId));
|
final var uri = URI.create(DISCORD_URL + "/channels/%s/messages/%s".formatted(channelId, messageId));
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
final HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("Authorization", "Bot " + webConfig.token())
|
.header("Authorization", "Bot " + webConfig.token())
|
||||||
.header("User-Agent", USER_AGENT)
|
.header("User-Agent", USER_AGENT)
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
return ""; // TODO: Think about transparently inform caller about error.
|
logger.error("Could not get message");
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
final var responseJson = new JSONObject(response.body());
|
final var responseJson = new JSONObject(response.body());
|
||||||
return responseJson.getString("content"); // FIXME: Can throw exception if content is not set -> https://discord.com/developers/docs/resources/message#message-object-message-structure
|
return Optional.of(responseJson.getString("content")); // content not be set -> https://discord.com/developers/docs/resources/message#message-object-message-structure
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public final class DiscordWebSocket {
|
public final class DiscordWebSocket {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DiscordWebSocket.class);
|
private static final Logger logger = LoggerFactory.getLogger(DiscordWebSocket.class);
|
||||||
private static final String DISCORD_API_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
||||||
private final String discordApiKey;
|
private final String discordApiKey;
|
||||||
private final EventHandler eventHandler;
|
private final EventHandler eventHandler;
|
||||||
private final Retryer retryer;
|
private final Retryer retryer;
|
||||||
|
@ -30,8 +29,10 @@ public final class DiscordWebSocket {
|
||||||
instance.start();
|
instance.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() throws InterruptedException {
|
||||||
final var connector = new Connector(eventHandler);
|
final var connector = new Connector(eventHandler);
|
||||||
final var connectionManager = new ConnectionManager(DISCORD_API_URL, discordApiKey, retryer);
|
final String initGatewayUrl = "wss://gateway.discord.gg/?v=10&encoding=json";
|
||||||
|
final var connectionManager = new ConnectionManager(initGatewayUrl, discordApiKey, retryer);
|
||||||
|
connectionManager.start(connector);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ public final class IdendificationConnectionInitiator implements ConnectionInitia
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initiate(WebSocket webSocket) {
|
public void initiate(final WebSocket webSocket) {
|
||||||
final String identifyPayload = "{\"op\": 2, \"d\": {\"token\": \"" + token + "\", \"intents\": 513, \"properties\": {\"os\": \"linux\", \"browser\": \"dchat_lib\", \"device\": \"dchat_lib\"}}}";
|
final String identifyPayload = "{\"op\": 2, \"d\": {\"token\": \"" + token + "\", \"intents\": 513, \"properties\": {\"os\": \"linux\", \"browser\": \"dchat_lib\", \"device\": \"dchat_lib\"}}}";
|
||||||
webSocket.sendText(identifyPayload, true);
|
webSocket.sendText(identifyPayload, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ public final class ResumeConnectionInitiator implements ConnectionInitiator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initiate(WebSocket webSocket) {
|
public void initiate(final WebSocket webSocket) {
|
||||||
final String identifyPayload = "{\"op\": 6, \"d\": {\"token\": \"" + token + "\", \"session_id\": \"" + sessionId + "\", \"seq\": " + lastSequence + "}}";
|
final String identifyPayload = "{\"op\": 6, \"d\": {\"token\": \"" + token + "\", \"session_id\": \"" + sessionId + "\", \"seq\": " + lastSequence + "}}";
|
||||||
webSocket.sendText(identifyPayload, true);
|
webSocket.sendText(identifyPayload, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,36 @@ import org.json.JSONObject;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public final class EventDeserializer {
|
public final class EventDeserializer {
|
||||||
|
private static EventType resolveEventType(final String type) {
|
||||||
|
if (type.isBlank()) return new EventType.Empty();
|
||||||
|
if ("READY".equals(type)) return new EventType.Ready();
|
||||||
|
return new EventType.Unknown(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int extractPossibleNullInt(final JSONObject jsonObject, final String key) {
|
||||||
|
try {
|
||||||
|
return jsonObject.getInt(key);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractPossibleNullString(final JSONObject jsonObject, final String key) {
|
||||||
|
try {
|
||||||
|
return jsonObject.getString(key);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject extractPossibleNullObject(final JSONObject jsonObject, final String key) {
|
||||||
|
try {
|
||||||
|
return jsonObject.getJSONObject(key);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return new JSONObject(Map.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Event deserialize(final String text) {
|
public Event deserialize(final String text) {
|
||||||
final JSONObject object = new JSONObject(text);
|
final JSONObject object = new JSONObject(text);
|
||||||
final String type = extractPossibleNullString(object, "t");
|
final String type = extractPossibleNullString(object, "t");
|
||||||
|
@ -15,34 +45,4 @@ public final class EventDeserializer {
|
||||||
final JSONObject data = extractPossibleNullObject(object, "d");
|
final JSONObject data = extractPossibleNullObject(object, "d");
|
||||||
return new Event(eventType, sequence, operation, data);
|
return new Event(eventType, sequence, operation, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EventType resolveEventType(final String type) {
|
|
||||||
if (type.isBlank()) return new EventType.Empty();
|
|
||||||
if ("READY".equals(type)) return new EventType.Ready();
|
|
||||||
return new EventType.Unknown(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int extractPossibleNullInt(final JSONObject jsonObject, final String key) {
|
|
||||||
try {
|
|
||||||
return jsonObject.getInt(key);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractPossibleNullString(final JSONObject jsonObject, final String key) {
|
|
||||||
try {
|
|
||||||
return jsonObject.getString(key);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject extractPossibleNullObject(final JSONObject jsonObject, final String key) {
|
|
||||||
try {
|
|
||||||
return jsonObject.getJSONObject(key);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return new JSONObject(Map.of());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
23
openai-rest/pom.xml
Normal file
23
openai-rest/pom.xml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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>de.hhhammer.dchat</groupId>
|
||||||
|
<artifactId>dchat</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>de.hhhammer.dchat.openai.rest</groupId>
|
||||||
|
<artifactId>openai-rest</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -0,0 +1,30 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ChatGPTRequest(String model, List<Message> messages, float temperature) implements JsonSerializable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JSONObject toJson() {
|
||||||
|
final var json = new JSONObject();
|
||||||
|
json.put("model", model);
|
||||||
|
final var messageJson = new JSONArray(messages.stream().map(Message::toJson).toList());
|
||||||
|
json.put("messages", messageJson);
|
||||||
|
json.put("temperature", temperature);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Message(String role, String content) implements JsonSerializable {
|
||||||
|
@Override
|
||||||
|
public @NotNull JSONObject toJson() {
|
||||||
|
final var json = new JSONObject();
|
||||||
|
json.put("role", role);
|
||||||
|
json.put("content", content);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
import de.hhhammer.dchat.openai.rest.MessageContext.PreviousInteraction;
|
||||||
|
import de.hhhammer.dchat.openai.rest.MessageContext.ReplyInteraction;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class ChatGPTRequestBuilder {
|
||||||
|
private static final String contextModel = "gpt-4o-mini";
|
||||||
|
|
||||||
|
public ChatGPTRequestBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static List<ChatGPTRequest.Message> getMessages(final List<? extends MessageContext> contextMessages, final String message, final String systemMessage) {
|
||||||
|
final ChatGPTRequest.Message systemMsg = new ChatGPTRequest.Message("system", systemMessage);
|
||||||
|
final List<ChatGPTRequest.Message> contextMsgs = getContextMessages(contextMessages);
|
||||||
|
final ChatGPTRequest.Message userMessage = new ChatGPTRequest.Message("user", message);
|
||||||
|
final List<ChatGPTRequest.Message> messages = new ArrayList<>();
|
||||||
|
messages.add(systemMsg);
|
||||||
|
messages.addAll(contextMsgs);
|
||||||
|
messages.add(userMessage);
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static List<ChatGPTRequest.Message> getContextMessages(final List<? extends MessageContext> contextMessages) {
|
||||||
|
return contextMessages.stream()
|
||||||
|
.map(ChatGPTRequestBuilder::mapContextMessages)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static List<ChatGPTRequest.Message> mapContextMessages(final MessageContext contextMessage) {
|
||||||
|
return switch (contextMessage) {
|
||||||
|
case PreviousInteraction previousInteractions -> List.of(
|
||||||
|
new ChatGPTRequest.Message("user", previousInteractions.question()),
|
||||||
|
new ChatGPTRequest.Message("assistant", previousInteractions.answer())
|
||||||
|
);
|
||||||
|
case ReplyInteraction replyInteractions ->
|
||||||
|
List.of(new ChatGPTRequest.Message("assistant", replyInteractions.answer()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatGPTRequest contextRequest(final List<? extends MessageContext> contextMessages, final String message, final String systemMessage) {
|
||||||
|
final List<ChatGPTRequest.Message> messages = getMessages(contextMessages, message, systemMessage);
|
||||||
|
return new ChatGPTRequest(
|
||||||
|
contextModel,
|
||||||
|
messages,
|
||||||
|
0.7f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import java.util.Spliterators;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
public final class ChatGPTService {
|
||||||
|
private final String apiKey;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public ChatGPTService(final String apiKey, final HttpClient httpClient) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> @Nullable T getFromJson(final Supplier<T> supplier) {
|
||||||
|
try {
|
||||||
|
return supplier.get();
|
||||||
|
} catch (final JSONException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> submit(final ChatGPTRequest chatGPTRequest) throws IOException, InterruptedException, ResponseException {
|
||||||
|
final String data = chatGPTRequest.toJson().toString();
|
||||||
|
final URI uri = URI.create("https://api.openai.com/v1/chat/completions");
|
||||||
|
final HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(data))
|
||||||
|
.setHeader("Content-Type", "application/json")
|
||||||
|
.setHeader("Authorization", "Bearer " + this.apiKey)
|
||||||
|
.timeout(Duration.ofMinutes(5))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new ResponseException("Response status code was not 200: " + response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
final var responseJson = new JSONObject(response);
|
||||||
|
final JSONArray choices = responseJson.getJSONArray("choices");
|
||||||
|
final Spliterator<Object> spliterator = Spliterators.spliterator(choices.iterator(), choices.length(), Spliterator.SIZED);
|
||||||
|
return StreamSupport.stream(spliterator, false)
|
||||||
|
.filter(JSONObject.class::isInstance)
|
||||||
|
.map(JSONObject.class::cast)
|
||||||
|
.map(choice -> getFromJson(() -> choice.getJSONObject("message")))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(message -> getFromJson(() -> message.getString("content")))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public interface JsonSerializable {
|
||||||
|
@NotNull
|
||||||
|
JSONObject toJson();
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
public sealed interface MessageContext {
|
||||||
|
record ReplyInteraction(String answer) implements MessageContext {
|
||||||
|
}
|
||||||
|
|
||||||
|
record PreviousInteraction(String question, String answer) implements MessageContext {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package de.hhhammer.dchat.openai.rest;
|
||||||
|
|
||||||
|
public final class ResponseException extends Exception {
|
||||||
|
public ResponseException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
9
openai-rest/src/main/java/module-info.java
Normal file
9
openai-rest/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module de.hhhammer.dchat.openai.rest {
|
||||||
|
exports de.hhhammer.dchat.openai.rest;
|
||||||
|
exports de.hhhammer.dchat.openai.rest.models;
|
||||||
|
|
||||||
|
requires java.net.http;
|
||||||
|
|
||||||
|
requires org.json;
|
||||||
|
requires static org.jetbrains.annotations;
|
||||||
|
}
|
1
pom.xml
1
pom.xml
|
@ -98,5 +98,6 @@
|
||||||
<module>bot</module>
|
<module>bot</module>
|
||||||
<module>discord-ws</module>
|
<module>discord-ws</module>
|
||||||
<module>discord-rest</module>
|
<module>discord-rest</module>
|
||||||
|
<module>openai-rest</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
Loading…
Reference in a new issue