Compare commits
2 commits
b522e9dd66
...
77b7608608
Author | SHA1 | Date | |
---|---|---|---|
77b7608608 | |||
18ee28daee |
96 changed files with 257 additions and 8128 deletions
|
@ -1,10 +1,7 @@
|
|||
matrix:
|
||||
MODULE:
|
||||
- db
|
||||
- bot
|
||||
- web
|
||||
- migration
|
||||
- monolith
|
||||
- discord
|
||||
|
||||
when:
|
||||
event: [tag, push, manual]
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
when:
|
||||
event: [tag, push, manual]
|
||||
|
||||
steps:
|
||||
build:
|
||||
image: docker.io/node:18-alpine
|
||||
directory: ui
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run build
|
|
@ -1,10 +1,6 @@
|
|||
matrix:
|
||||
TARGET:
|
||||
- bot
|
||||
- web
|
||||
- migration
|
||||
- ui
|
||||
- monolith
|
||||
PLATFORM:
|
||||
- linux/amd64
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ matrix:
|
|||
|
||||
depends_on:
|
||||
- java
|
||||
- nodejs
|
||||
|
||||
when:
|
||||
event: [tag, push, manual]
|
||||
|
|
48
Dockerfile
48
Dockerfile
|
@ -13,57 +13,9 @@ RUN --mount=type=cache,target=/root/.m2/ \
|
|||
|
||||
FROM ${SETUP_IMAGE} as setup
|
||||
|
||||
# Create final monolith
|
||||
FROM docker.io/eclipse-temurin:${TEMURIN_IMAGE_TAG} AS monolith
|
||||
WORKDIR /app
|
||||
COPY --from=setup /app/monolith/target/monolith-*-fat.jar /app/monolith.jar
|
||||
CMD ["java", "-jar", "/app/monolith.jar"]
|
||||
|
||||
# Create final web
|
||||
FROM docker.io/eclipse-temurin:${TEMURIN_IMAGE_TAG} AS web
|
||||
WORKDIR /app
|
||||
COPY --from=setup /app/web/target/web-*-fat.jar /app/web.jar
|
||||
EXPOSE 8080
|
||||
CMD ["java", "-jar", "/app/web.jar"]
|
||||
|
||||
# Create final migration
|
||||
FROM docker.io/eclipse-temurin:${TEMURIN_IMAGE_TAG} AS migration
|
||||
WORKDIR /app
|
||||
COPY --from=setup /app/migration/target/migration-*-fat.jar /app/migration.jar
|
||||
CMD ["java", "-jar", "/app/migration.jar"]
|
||||
|
||||
# Create final bot
|
||||
FROM docker.io/eclipse-temurin:${TEMURIN_IMAGE_TAG} AS bot
|
||||
WORKDIR /app
|
||||
COPY --from=setup /app/bot/target/bot-*-fat.jar /app/bot.jar
|
||||
CMD ["java", "-jar", "/app/bot.jar"]
|
||||
|
||||
# Build the ui
|
||||
FROM docker.io/node:18-alpine AS ui-build
|
||||
WORKDIR /ui
|
||||
COPY ui/package.json ui/package-lock.json .
|
||||
RUN npm ci
|
||||
COPY ui .
|
||||
RUN npm run build
|
||||
|
||||
# Create final ui
|
||||
FROM docker.io/nginx AS ui
|
||||
COPY --from=ui-build ui/dist /usr/share/nginx/html
|
||||
|
||||
# Create caddy reverse proxy
|
||||
FROM docker.io/caddy AS caddy
|
||||
RUN <<EOF cat > /etc/caddy/Caddyfile
|
||||
{
|
||||
debug
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:8080 {
|
||||
route {
|
||||
reverse_proxy /api/* monolith:8080
|
||||
reverse_proxy ui:80
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
EXPOSE 8080
|
||||
|
|
59
bot/pom.xml
59
bot/pom.xml
|
@ -15,65 +15,14 @@
|
|||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<artifactId>db</artifactId>
|
||||
<artifactId>discord</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.javacord</groupId>
|
||||
<artifactId>javacord</artifactId>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-to-slf4j</artifactId>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>fat</shadedClassifierName>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>de.hhhammer.dchat.bot.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
@ -1,51 +1,12 @@
|
|||
package de.hhhammer.dchat.bot;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTService;
|
||||
import de.hhhammer.dchat.db.PostgresServerDBService;
|
||||
import de.hhhammer.dchat.db.PostgresUserDBService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
|
||||
public final class App {
|
||||
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
||||
|
||||
public static void main(final String[] args) {
|
||||
final String discordApiKey = System.getenv("DISCORD_API_KEY");
|
||||
if (discordApiKey == null) {
|
||||
logger.error("Missing environment variables: DISCORD_API_KEY");
|
||||
System.exit(1);
|
||||
}
|
||||
final String openaiApiKey = System.getenv("OPENAI_API_KEY");
|
||||
if (openaiApiKey == null) {
|
||||
logger.error("Missing environment variables: OPENAI_API_KEY");
|
||||
System.exit(1);
|
||||
}
|
||||
final String postgresUser = System.getenv("POSTGRES_USER");
|
||||
final String postgresPassword = System.getenv("POSTGRES_PASSWORD");
|
||||
final String postgresUrl = System.getenv("POSTGRES_URL");
|
||||
if (postgresUser == null || postgresPassword == null || postgresUrl == null) {
|
||||
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
final var chatGPTService = new ChatGPTService(openaiApiKey, HttpClient.newHttpClient(), new ObjectMapper());
|
||||
|
||||
final var config = new HikariConfig();
|
||||
config.setJdbcUrl(postgresUrl);
|
||||
config.setUsername(postgresUser);
|
||||
config.setPassword(postgresPassword);
|
||||
|
||||
try (var ds = new HikariDataSource(config)) {
|
||||
final var serverDBService = new PostgresServerDBService(ds);
|
||||
final var userDBService = new PostgresUserDBService(ds);
|
||||
|
||||
final var discordBot = new DiscordBot(serverDBService, userDBService, chatGPTService, discordApiKey);
|
||||
discordBot.run();
|
||||
}
|
||||
logger.error("Currently not implemented!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
package de.hhhammer.dchat.bot;
|
||||
|
||||
import de.hhhammer.dchat.bot.discord.MessageCreateHandler;
|
||||
import de.hhhammer.dchat.bot.discord.ServerMessageHandler;
|
||||
import de.hhhammer.dchat.bot.discord.UserMessageHandler;
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTService;
|
||||
import de.hhhammer.dchat.db.ServerDBService;
|
||||
import de.hhhammer.dchat.db.UserDBService;
|
||||
import org.javacord.api.DiscordApi;
|
||||
import org.javacord.api.DiscordApiBuilder;
|
||||
import org.javacord.api.interaction.SlashCommand;
|
||||
import org.javacord.api.interaction.SlashCommandInteraction;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class DiscordBot implements Runnable {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DiscordBot.class);
|
||||
|
||||
private final ServerDBService serverDBService;
|
||||
private final UserDBService userDBService;
|
||||
private final ChatGPTService chatGPTService;
|
||||
private final String discordApiKey;
|
||||
|
||||
public DiscordBot(final ServerDBService serverDBService, final UserDBService userDBService, final ChatGPTService chatGPTService, final String discordApiKey) {
|
||||
this.serverDBService = serverDBService;
|
||||
this.userDBService = userDBService;
|
||||
this.chatGPTService = chatGPTService;
|
||||
this.discordApiKey = discordApiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("Starting Discord application");
|
||||
final DiscordApi discordApi = new DiscordApiBuilder()
|
||||
.setToken(discordApiKey)
|
||||
.login()
|
||||
.join();
|
||||
discordApi.setMessageCacheSize(10, 60 * 60);
|
||||
final var future = new CompletableFuture<Void>();
|
||||
Runtime.getRuntime().addShutdownHook(Thread.ofVirtual().unstarted(() -> {
|
||||
logger.info("Shutting down Discord application");
|
||||
discordApi.disconnect().thenAccept(future::complete);
|
||||
}));
|
||||
final SlashCommand token = SlashCommand.with("tokens", "Check how many tokens where spend on this server")
|
||||
.createGlobal(discordApi)
|
||||
.join();
|
||||
|
||||
discordApi.addSlashCommandCreateListener(event -> {
|
||||
logger.debug("Event? " + event.getSlashCommandInteraction().getFullCommandName());
|
||||
final SlashCommandInteraction command = event.getSlashCommandInteraction();
|
||||
if (token.getFullCommandNames().contains(command.getFullCommandName())) {
|
||||
event.getInteraction()
|
||||
.respondLater()
|
||||
.orTimeout(30, TimeUnit.SECONDS)
|
||||
.thenAccept((interactionOriginalResponseUpdater) -> {
|
||||
final long tokens = event.getInteraction().getServer().isPresent() ?
|
||||
this.serverDBService.tokensOfLast30Days(String.valueOf(event.getInteraction().getServer().get().getId())) :
|
||||
this.userDBService.tokensOfLast30Days(String.valueOf(event.getInteraction().getUser().getId()));
|
||||
interactionOriginalResponseUpdater.setContent("" + tokens).update();
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
discordApi.addMessageCreateListener(new MessageCreateHandler(new ServerMessageHandler(serverDBService, chatGPTService)));
|
||||
discordApi.addMessageCreateListener(new MessageCreateHandler(new UserMessageHandler(userDBService, chatGPTService)));
|
||||
|
||||
// Print the invite url of your bot
|
||||
logger.info("You can invite the bot by using the following url: " + discordApi.createBotInvite());
|
||||
|
||||
try {
|
||||
future.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.discord;
|
||||
|
||||
import de.hhhammer.dchat.bot.openai.ResponseException;
|
||||
import org.javacord.api.entity.message.MessageType;
|
||||
import org.javacord.api.event.message.MessageCreateEvent;
|
||||
import org.javacord.api.listener.message.MessageCreateListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class MessageCreateHandler implements MessageCreateListener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageCreateHandler.class);
|
||||
|
||||
private final MessageHandler messageHandler;
|
||||
|
||||
public MessageCreateHandler(final MessageHandler messageHandler) {
|
||||
this.messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageCreate(final MessageCreateEvent event) {
|
||||
Thread.ofVirtual().start(() -> {
|
||||
if (!event.canYouReadContent() || event.getMessageAuthor().isBotUser() || !isNormalOrReplyMessageType(event)) {
|
||||
return;
|
||||
}
|
||||
if (!this.messageHandler.canHandle(event)) {
|
||||
return;
|
||||
}
|
||||
if (!this.messageHandler.isAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
if (this.messageHandler.exceedsRate(event)) {
|
||||
event.getChannel().sendMessage("Rate limit hit - cooling down...");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.messageHandler.handle(event);
|
||||
} catch (final ResponseException | IOException | InterruptedException e) {
|
||||
logger.error("Reading a message from the listener", e);
|
||||
event.getMessage().reply("Sorry but something went wrong :(");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isNormalOrReplyMessageType(final MessageCreateEvent event) {
|
||||
final MessageType type = event.getMessage().getType();
|
||||
return type == MessageType.NORMAL || type == MessageType.REPLY;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.discord;
|
||||
|
||||
import de.hhhammer.dchat.bot.openai.ResponseException;
|
||||
import org.javacord.api.event.message.MessageCreateEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface MessageHandler {
|
||||
void handle(MessageCreateEvent event) throws ResponseException, IOException, InterruptedException;
|
||||
|
||||
boolean isAllowed(MessageCreateEvent event);
|
||||
|
||||
boolean exceedsRate(MessageCreateEvent event);
|
||||
|
||||
boolean canHandle(MessageCreateEvent event);
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.discord;
|
||||
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTRequestBuilder;
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTService;
|
||||
import de.hhhammer.dchat.bot.openai.MessageContext.ReplyInteraction;
|
||||
import de.hhhammer.dchat.bot.openai.ResponseException;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTRequest;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTResponse;
|
||||
import de.hhhammer.dchat.db.ServerDBService;
|
||||
import de.hhhammer.dchat.db.models.server.ServerConfig;
|
||||
import de.hhhammer.dchat.db.models.server.ServerMessage;
|
||||
import org.javacord.api.entity.DiscordEntity;
|
||||
import org.javacord.api.entity.message.Message;
|
||||
import org.javacord.api.entity.message.MessageReference;
|
||||
import org.javacord.api.entity.message.MessageType;
|
||||
import org.javacord.api.event.message.MessageCreateEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ServerMessageHandler implements MessageHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ServerMessageHandler.class);
|
||||
private final ServerDBService serverDBService;
|
||||
private final ChatGPTService chatGPTService;
|
||||
|
||||
public ServerMessageHandler(final ServerDBService serverDBService, final ChatGPTService chatGPTService) {
|
||||
this.serverDBService = serverDBService;
|
||||
this.chatGPTService = chatGPTService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final MessageCreateEvent event) throws ResponseException, IOException, InterruptedException {
|
||||
logger.debug("Handling server event");
|
||||
final String content = extractContent(event);
|
||||
final long serverId = event.getServer().get().getId();
|
||||
final String systemMessage = this.serverDBService.getConfig(String.valueOf(serverId)).get().systemMessage();
|
||||
final List<ReplyInteraction> messageContext = event.getMessage().getType() == MessageType.REPLY ? getContextMessages(event) : List.of();
|
||||
final ChatGPTRequest request = new ChatGPTRequestBuilder().contextRequest(messageContext, content, systemMessage);
|
||||
final ChatGPTResponse response = this.chatGPTService.submit(request);
|
||||
if (response.choices().isEmpty()) {
|
||||
event.getMessage().reply("No response available");
|
||||
return;
|
||||
}
|
||||
final String answer = response.choices().get(0).message().content();
|
||||
logServerMessage(event, response.usage().totalTokens());
|
||||
event.getMessage().reply(answer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed(final MessageCreateEvent event) {
|
||||
if (event.getServer().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final long serverId = event.getServer().get().getId();
|
||||
final Optional<ServerConfig> config = this.serverDBService.getConfig(String.valueOf(serverId));
|
||||
if (config.isEmpty()) {
|
||||
logger.debug("Not allowed with id: " + serverId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exceedsRate(final MessageCreateEvent event) {
|
||||
final String serverId = String.valueOf(event.getServer().get().getId());
|
||||
final Optional<ServerConfig> config = this.serverDBService.getConfig(serverId);
|
||||
if (config.isEmpty()) {
|
||||
logger.error("Missing configuration for server with id: " + serverId);
|
||||
return true;
|
||||
}
|
||||
final int rateLimit = config.get().rateLimit();
|
||||
final int countMessagesInLastMinute = this.serverDBService.countMessagesInLastMinute(serverId);
|
||||
|
||||
return countMessagesInLastMinute >= rateLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canHandle(final MessageCreateEvent event) {
|
||||
return event.isServerMessage();
|
||||
}
|
||||
|
||||
private void logServerMessage(final MessageCreateEvent event, final int tokens) {
|
||||
final long serverId = event.getServer().map(DiscordEntity::getId).get();
|
||||
final long userId = event.getMessageAuthor().getId();
|
||||
|
||||
final var serverMessage = new ServerMessage.NewServerMessage(String.valueOf(serverId), userId, tokens);
|
||||
this.serverDBService.addMessage(serverMessage);
|
||||
}
|
||||
|
||||
private String extractContent(final MessageCreateEvent event) {
|
||||
final long ownId = event.getApi().getYourself().getId();
|
||||
return event.getMessageContent().replaceFirst("<" + ownId + "> ", "");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<ReplyInteraction> getContextMessages(final MessageCreateEvent event) {
|
||||
return event.getMessage()
|
||||
.getMessageReference()
|
||||
.map(MessageReference::getMessage)
|
||||
.flatMap(m -> m)
|
||||
.map(Message::getReadableContent)
|
||||
.map(ReplyInteraction::new)
|
||||
.stream()
|
||||
.toList();
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.discord;
|
||||
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTRequestBuilder;
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTService;
|
||||
import de.hhhammer.dchat.bot.openai.MessageContext.PreviousInteraction;
|
||||
import de.hhhammer.dchat.bot.openai.ResponseException;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTRequest;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTResponse;
|
||||
import de.hhhammer.dchat.db.UserDBService;
|
||||
import de.hhhammer.dchat.db.models.user.UserConfig;
|
||||
import de.hhhammer.dchat.db.models.user.UserMessage;
|
||||
import org.javacord.api.event.message.MessageCreateEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class UserMessageHandler implements MessageHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserMessageHandler.class);
|
||||
private final UserDBService userDBService;
|
||||
private final ChatGPTService chatGPTService;
|
||||
|
||||
public UserMessageHandler(final UserDBService userDBService, final ChatGPTService chatGPTService) {
|
||||
this.userDBService = userDBService;
|
||||
this.chatGPTService = chatGPTService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final MessageCreateEvent event) throws ResponseException, IOException, InterruptedException {
|
||||
logger.debug("Handling user event");
|
||||
final String content = event.getReadableMessageContent();
|
||||
final String userId = String.valueOf(event.getMessageAuthor().getId());
|
||||
final UserConfig config = this.userDBService.getConfig(userId).get();
|
||||
final String systemMessage = config.systemMessage();
|
||||
final List<PreviousInteraction> context = this.userDBService.getLastMessages(userId, config.contextLength())
|
||||
.stream()
|
||||
.map(userMessage -> new PreviousInteraction(userMessage.question(), userMessage.answer()))
|
||||
.toList();
|
||||
final ChatGPTRequest request = new ChatGPTRequestBuilder().contextRequest(context, content, systemMessage);
|
||||
final ChatGPTResponse response = this.chatGPTService.submit(request);
|
||||
if (response.choices().isEmpty()) {
|
||||
event.getMessage().reply("No response available");
|
||||
return;
|
||||
}
|
||||
final String answer = response.choices().get(0).message().content();
|
||||
logUserMessage(event, content, answer, response.usage().totalTokens());
|
||||
event.getChannel().sendMessage(answer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed(final MessageCreateEvent event) {
|
||||
if (event.getServer().isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final long userId = event.getMessageAuthor().getId();
|
||||
final Optional<UserConfig> config = this.userDBService.getConfig(String.valueOf(userId));
|
||||
if (config.isEmpty()) {
|
||||
logger.debug("Not allowed with id: " + userId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exceedsRate(final MessageCreateEvent event) {
|
||||
final String userId = String.valueOf(event.getMessageAuthor().getId());
|
||||
final Optional<UserConfig> config = this.userDBService.getConfig(userId);
|
||||
if (config.isEmpty()) {
|
||||
logger.error("Missing configuration for userId with id: " + userId);
|
||||
return true;
|
||||
}
|
||||
final int rateLimit = config.get().rateLimit();
|
||||
final int countMessagesInLastMinute = this.userDBService.countMessagesInLastMinute(userId);
|
||||
|
||||
return countMessagesInLastMinute >= rateLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canHandle(final MessageCreateEvent event) {
|
||||
return event.isPrivateMessage();
|
||||
}
|
||||
|
||||
private void logUserMessage(final MessageCreateEvent event, final String question, final String answer, final int tokens) {
|
||||
final long userId = event.getMessageAuthor().getId();
|
||||
|
||||
final UserMessage.NewUserMessage userMessage = new UserMessage.NewUserMessage(String.valueOf(userId), question, answer, tokens);
|
||||
this.userDBService.addMessage(userMessage);
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai;
|
||||
|
||||
import de.hhhammer.dchat.bot.openai.MessageContext.PreviousInteraction;
|
||||
import de.hhhammer.dchat.bot.openai.MessageContext.ReplyInteraction;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTRequest;
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTRequest;
|
||||
import de.hhhammer.dchat.bot.openai.models.ChatGPTResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public final class ChatGPTService {
|
||||
private static final String url = "https://api.openai.com/v1/chat/completions";
|
||||
private final String apiKey;
|
||||
private final HttpClient httpClient;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public ChatGPTService(final String apiKey, final HttpClient httpClient, final ObjectMapper mapper) {
|
||||
this.apiKey = apiKey;
|
||||
this.httpClient = httpClient;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public ChatGPTResponse submit(final ChatGPTRequest chatGPTRequest) throws IOException, InterruptedException, ResponseException {
|
||||
final byte[] data = mapper.writeValueAsBytes(chatGPTRequest);
|
||||
final HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(data))
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setHeader("Authorization", "Bearer " + this.apiKey)
|
||||
.timeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
|
||||
final HttpResponse<InputStream> responseStream = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (responseStream.statusCode() != 200) {
|
||||
throw new ResponseException("Response status code was not 200: " + responseStream.statusCode());
|
||||
}
|
||||
|
||||
return mapper.readValue(responseStream.body(), ChatGPTResponse.class);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai;
|
||||
|
||||
public sealed interface MessageContext {
|
||||
record ReplyInteraction(String answer) implements MessageContext {
|
||||
}
|
||||
|
||||
record PreviousInteraction(String question, String answer) implements MessageContext {
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai;
|
||||
|
||||
public final class ResponseException extends Exception {
|
||||
public ResponseException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ChatGPTRequest(String model, List<Message> messages, float temperature) {
|
||||
|
||||
public record Message(String role, String content) {
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package de.hhhammer.dchat.bot.openai.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ChatGPTResponse(Usage usage,
|
||||
List<Choice> choices) {
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Usage(@JsonProperty("prompt_tokens") int promptTokens,
|
||||
@JsonProperty("completion_tokens") int completionTokens,
|
||||
@JsonProperty("total_tokens") int totalTokens) {
|
||||
}
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Choice(Message message) {
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Message(String role, String content) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="de.hhhammer.dchat" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
|
@ -1,7 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
public final class DBException extends Exception {
|
||||
public DBException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import de.hhhammer.dchat.db.models.server.ServerConfig;
|
||||
import de.hhhammer.dchat.db.models.server.ServerMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public final class PostgresServerDBService implements ServerDBService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostgresServerDBService.class);
|
||||
private final DataSource dataSource;
|
||||
|
||||
public PostgresServerDBService(final DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ServerConfig> getConfig(final String serverId) {
|
||||
final var getServerConfig = """
|
||||
SELECT * FROM server_configs WHERE server_id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, serverId);
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<ServerConfig> iterable = () -> new ResultSetIterator<>(resultSet, new ServerConfig.ServerConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Getting configuration for server with id: " + serverId, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
logger.error("Iterating over ServerConfig ResultSet for server with id: " + serverId, e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServerConfig> getAllConfigs() throws DBException {
|
||||
final var getAllowedServerSql = """
|
||||
SELECT * FROM server_configs
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
|
||||
) {
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<ServerConfig> iterable = () -> new ResultSetIterator<>(resultSet, new ServerConfig.ServerConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).toList();
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Loading all configs", e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
throw new DBException("Iterating over configs", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ServerConfig> getConfigBy(final long id) throws DBException {
|
||||
final var getServerConfig = """
|
||||
SELECT * FROM server_configs WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setLong(1, id);
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<ServerConfig> iterable = () -> new ResultSetIterator<>(resultSet, new ServerConfig.ServerConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
|
||||
} catch (SQLException e) {
|
||||
throw new DBException("Getting configuration with id: " + id, e);
|
||||
} catch (ResultSetIteratorException e) {
|
||||
throw new DBException("Iterating over ServerConfig ResultSet for id: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConfig(final ServerConfig.NewServerConfig newServerConfig) throws DBException {
|
||||
final var getServerConfig = """
|
||||
INSERT INTO server_configs (server_id, system_message, rate_limit) VALUES (?,?,?)
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, newServerConfig.serverId());
|
||||
pstmt.setString(2, newServerConfig.systemMessage());
|
||||
pstmt.setInt(3, newServerConfig.rateLimit());
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config added for server with id: " + newServerConfig.serverId());
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Adding configuration to server with id: " + newServerConfig.serverId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateConfig(final long id, final ServerConfig.NewServerConfig newServerConfig) throws DBException {
|
||||
final var getServerConfig = """
|
||||
UPDATE server_configs SET system_message = ?, rate_limit = ?, server_id = ? WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, newServerConfig.systemMessage());
|
||||
pstmt.setInt(2, newServerConfig.rateLimit());
|
||||
pstmt.setString(3, newServerConfig.serverId());
|
||||
pstmt.setLong(4, id);
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config update for server with id: " + newServerConfig.serverId());
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Adding configuration to server with id: " + newServerConfig.serverId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteConfig(final long id) throws DBException {
|
||||
final var getServerConfig = """
|
||||
DELETE FROM server_configs WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setLong(1, id);
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config deleted for server with id: " + id);
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Deleting configuration for server with id: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countMessagesInLastMinute(final String serverId) {
|
||||
final var getServerConfig = """
|
||||
SELECT count(*) FROM server_messages WHERE server_id = ? AND time <= ? and time >= ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, serverId);
|
||||
final var now = Instant.now();
|
||||
pstmt.setTimestamp(2, Timestamp.from(now));
|
||||
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
if (resultSet.next()) return resultSet.getInt(1);
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Getting messages for server with id: " + serverId, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
logger.error("Iterating over ServerMessages ResultSet for server with id: " + serverId, e);
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMessage(final ServerMessage.NewServerMessage serverMessage) {
|
||||
final var getServerConfig = """
|
||||
INSERT INTO server_messages (server_id, user_id, tokens) VALUES (?,?,?)
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, serverMessage.serverId());
|
||||
pstmt.setLong(2, serverMessage.userId());
|
||||
pstmt.setInt(3, serverMessage.tokens());
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No message added for server with id: " + serverMessage.serverId());
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Adding message to server with id: " + serverMessage.serverId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long tokensOfLast30Days(final String serverId) {
|
||||
final var countTokensOfLast30Days = """
|
||||
SELECT sum(tokens) FROM server_messages WHERE server_id = ? AND time < ? AND time >= ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
|
||||
) {
|
||||
pstmt.setString(1, serverId);
|
||||
final var now = Instant.now();
|
||||
pstmt.setTimestamp(2, Timestamp.from(now));
|
||||
pstmt.setTimestamp(3, Timestamp.from(now.minus(30, ChronoUnit.DAYS)));
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
if (resultSet.next()) return resultSet.getLong(1);
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Counting tokens of the last 30 days from server with id: " + serverId, e);
|
||||
}
|
||||
logger.error("No tokens found for server with id: " + serverId);
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import de.hhhammer.dchat.db.models.user.UserConfig;
|
||||
import de.hhhammer.dchat.db.models.user.UserMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public final class PostgresUserDBService implements UserDBService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostgresUserDBService.class);
|
||||
private final DataSource dataSource;
|
||||
|
||||
public PostgresUserDBService(final DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserConfig> getConfig(final String userId) {
|
||||
final var getServerConfig = """
|
||||
SELECT * FROM user_configs WHERE user_id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, userId);
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<UserConfig> iterable = () -> new ResultSetIterator<>(resultSet, new UserConfig.UserConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Getting configuration for user with id: " + userId, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
logger.error("Iterating over ServerConfig ResultSet for user with id: " + userId, e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserConfig> getConfigBy(final long id) throws DBException {
|
||||
final var getServerConfig = """
|
||||
SELECT * FROM user_configs WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setLong(1, id);
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<UserConfig> iterable = () -> new ResultSetIterator<>(resultSet, new UserConfig.UserConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Getting configuration id: " + id, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
throw new DBException("Iterating over UserConfig ResultSet with id: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserConfig> getAllConfigs() throws DBException {
|
||||
final var getServerConfig = """
|
||||
SELECT * FROM user_configs
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<UserConfig> iterable = () -> new ResultSetIterator<>(resultSet, new UserConfig.UserConfigResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).toList();
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Getting all configurations", e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
throw new DBException("Iterating over all UserConfig ResultSet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConfig(UserConfig.NewUserConfig newUserConfig) throws DBException {
|
||||
final var getServerConfig = """
|
||||
INSERT INTO user_configs (user_id, system_message, context_length, rate_limit) VALUES (?,?,?,?)
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, newUserConfig.userId());
|
||||
pstmt.setString(2, newUserConfig.systemMessage());
|
||||
pstmt.setInt(3, newUserConfig.contextLength());
|
||||
pstmt.setInt(4, newUserConfig.rateLimit());
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config added for user with id: " + newUserConfig.userId());
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Adding configuration for user with id: " + newUserConfig.userId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateConfig(final long id, final UserConfig.NewUserConfig newUserConfig) throws DBException {
|
||||
final var getServerConfig = """
|
||||
UPDATE user_configs SET system_message = ?, context_length = ?, rate_limit = ?, user_id = ? WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, newUserConfig.systemMessage());
|
||||
pstmt.setInt(2, newUserConfig.rateLimit());
|
||||
pstmt.setLong(3, newUserConfig.contextLength());
|
||||
pstmt.setString(4, newUserConfig.userId());
|
||||
pstmt.setLong(5, id);
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config update with id: " + id);
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Updating configuration with id: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteConfig(final long id) throws DBException {
|
||||
final var getServerConfig = """
|
||||
DELETE FROM user_configs WHERE id = ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setLong(1, id);
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No config deleted for user with id: " + id);
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
throw new DBException("Deleting configuration with id: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countMessagesInLastMinute(final String userId) {
|
||||
final var getServerConfig = """
|
||||
SELECT count(*) FROM user_messages WHERE user_id = ? AND time <= ? and time >= ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, userId);
|
||||
final var now = Instant.now();
|
||||
pstmt.setTimestamp(2, Timestamp.from(now));
|
||||
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
if (resultSet.next()) return resultSet.getInt(1);
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Getting messages for user with id: " + userId, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
logger.error("Iterating over ServerMessages ResultSet for user with id: " + userId, e);
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMessage(final UserMessage.NewUserMessage newUserMessage) {
|
||||
final var getServerConfig = """
|
||||
INSERT INTO user_messages (user_id, question, answer, tokens) VALUES (?,?,?,?)
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getServerConfig)
|
||||
) {
|
||||
pstmt.setString(1, newUserMessage.userId());
|
||||
pstmt.setString(2, newUserMessage.question());
|
||||
pstmt.setString(3, newUserMessage.answer());
|
||||
pstmt.setInt(4, newUserMessage.tokens());
|
||||
final int affectedRows = pstmt.executeUpdate();
|
||||
if (affectedRows == 0) {
|
||||
logger.error("No message added for user with id: " + newUserMessage.userId());
|
||||
}
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Adding message to user with id: " + newUserMessage.userId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserMessage> getLastMessages(final String userId, final int limit) {
|
||||
final var getLastMessages = """
|
||||
SELECT * FROM user_messages WHERE user_id = ? ORDER BY time DESC LIMIT ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(getLastMessages)
|
||||
) {
|
||||
pstmt.setString(1, userId);
|
||||
pstmt.setInt(2, limit);
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
final Iterable<UserMessage> iterable = () -> new ResultSetIterator<>(resultSet, new UserMessage.UserMessageResultSetTransformer());
|
||||
return StreamSupport.stream(iterable.spliterator(), false).toList();
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Fetching last messages for user whit id: " + userId, e);
|
||||
} catch (final ResultSetIteratorException e) {
|
||||
logger.error("Iterating over messages ResultSet from user with id: " + userId, e);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long tokensOfLast30Days(final String userId) {
|
||||
final var countTokensOfLast30Days = """
|
||||
SELECT sum(tokens) FROM user_messages WHERE user_id = ? AND time < ? AND time >= ?
|
||||
""";
|
||||
try (final Connection con = dataSource.getConnection();
|
||||
final PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
|
||||
) {
|
||||
pstmt.setString(1, userId);
|
||||
final var now = Instant.now();
|
||||
pstmt.setTimestamp(2, Timestamp.from(now));
|
||||
pstmt.setTimestamp(3, Timestamp.from(now.minus(30, ChronoUnit.DAYS)));
|
||||
final ResultSet resultSet = pstmt.executeQuery();
|
||||
if (resultSet.next()) return resultSet.getLong(1);
|
||||
} catch (final SQLException e) {
|
||||
logger.error("Counting tokens of the last 30 days from user with id: " + userId, e);
|
||||
}
|
||||
logger.error("No tokens found for user with id: " + userId);
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
|
||||
public final class ResultSetIterator<T> implements Iterator<T> {
|
||||
private final ResultSet resultSet;
|
||||
private final ResultSetTransformer<T> transformer;
|
||||
|
||||
public ResultSetIterator(final ResultSet resultSet, final ResultSetTransformer<T> transformer) {
|
||||
this.resultSet = resultSet;
|
||||
this.transformer = transformer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
try {
|
||||
return resultSet.next();
|
||||
} catch (SQLException e) {
|
||||
throw new ResultSetIteratorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
try {
|
||||
return transformer.transform(resultSet);
|
||||
} catch (SQLException e) {
|
||||
throw new ResultSetIteratorException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
public final class ResultSetIteratorException extends RuntimeException {
|
||||
public ResultSetIteratorException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ResultSetTransformer<T> {
|
||||
T transform(ResultSet resultSet) throws SQLException;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import de.hhhammer.dchat.db.models.server.ServerConfig;
|
||||
import de.hhhammer.dchat.db.models.server.ServerMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ServerDBService {
|
||||
Optional<ServerConfig> getConfig(String serverId);
|
||||
|
||||
List<ServerConfig> getAllConfigs() throws DBException;
|
||||
|
||||
Optional<ServerConfig> getConfigBy(long id) throws DBException;
|
||||
|
||||
void addConfig(ServerConfig.NewServerConfig newServerConfig) throws DBException;
|
||||
|
||||
void updateConfig(long id, ServerConfig.NewServerConfig newServerConfig) throws DBException;
|
||||
|
||||
void deleteConfig(long id) throws DBException;
|
||||
|
||||
int countMessagesInLastMinute(String serverId);
|
||||
|
||||
void addMessage(ServerMessage.NewServerMessage serverMessage);
|
||||
|
||||
long tokensOfLast30Days(String serverId);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package de.hhhammer.dchat.db;
|
||||
|
||||
import de.hhhammer.dchat.db.models.user.UserConfig;
|
||||
import de.hhhammer.dchat.db.models.user.UserMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserDBService {
|
||||
Optional<UserConfig> getConfig(String userId);
|
||||
|
||||
Optional<UserConfig> getConfigBy(long id) throws DBException;
|
||||
|
||||
List<UserConfig> getAllConfigs() throws DBException;
|
||||
|
||||
void addConfig(UserConfig.NewUserConfig newUserConfig) throws DBException;
|
||||
|
||||
void updateConfig(long id, UserConfig.NewUserConfig newUserConfig) throws DBException;
|
||||
|
||||
void deleteConfig(long id) throws DBException;
|
||||
|
||||
int countMessagesInLastMinute(String userId);
|
||||
|
||||
void addMessage(UserMessage.NewUserMessage newUserMessage);
|
||||
|
||||
List<UserMessage> getLastMessages(String userId, int limit);
|
||||
|
||||
long tokensOfLast30Days(String userId);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package de.hhhammer.dchat.db.models.server;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import de.hhhammer.dchat.db.ResultSetTransformer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
|
||||
public record ServerConfig(long id, String serverId, String systemMessage, int rateLimit, Instant time) {
|
||||
|
||||
public static class ServerConfigResultSetTransformer implements ResultSetTransformer<ServerConfig> {
|
||||
|
||||
@Override
|
||||
public ServerConfig transform(ResultSet resultSet) throws SQLException {
|
||||
var id = resultSet.getLong("id");
|
||||
var serverId = resultSet.getString("server_id");
|
||||
var systemMessage = resultSet.getString("system_message");
|
||||
var rateLimit = resultSet.getInt("rate_limit");
|
||||
var time = resultSet.getTimestamp("time").toInstant();
|
||||
return new ServerConfig(id, serverId, systemMessage, rateLimit, time);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record NewServerConfig(String serverId, String systemMessage, @Nullable int rateLimit) {
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package de.hhhammer.dchat.db.models.server;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import de.hhhammer.dchat.db.ResultSetTransformer;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
|
||||
public record ServerMessage(long id, String serverId, long userId, int tokens, Instant time) {
|
||||
|
||||
public static class ServerMessageResultSetTransformer implements ResultSetTransformer<ServerMessage> {
|
||||
|
||||
@Override
|
||||
public ServerMessage transform(ResultSet resultSet) throws SQLException {
|
||||
var id = resultSet.getLong("id");
|
||||
var serverId = resultSet.getString("server_id");
|
||||
var userId = resultSet.getLong("user_id");
|
||||
var tokens = resultSet.getInt("tokens");
|
||||
var time = resultSet.getTimestamp("time").toInstant();
|
||||
return new ServerMessage(id, serverId, userId, tokens, time);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record NewServerMessage(String serverId, long userId, int tokens) {
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package de.hhhammer.dchat.db.models.user;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import de.hhhammer.dchat.db.ResultSetTransformer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
|
||||
public record UserConfig(long id, String userId, String systemMessage, int contextLength, int rateLimit, Instant time) {
|
||||
|
||||
public static class UserConfigResultSetTransformer implements ResultSetTransformer<UserConfig> {
|
||||
|
||||
@Override
|
||||
public UserConfig transform(ResultSet resultSet) throws SQLException {
|
||||
var id = resultSet.getLong("id");
|
||||
var userId = resultSet.getString("user_id");
|
||||
var systemMessage = resultSet.getString("system_message");
|
||||
var contextLength = resultSet.getInt("context_length");
|
||||
var rateLimit = resultSet.getInt("rate_limit");
|
||||
var time = resultSet.getTimestamp("time").toInstant();
|
||||
return new UserConfig(id, userId, systemMessage, contextLength, rateLimit, time);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record NewUserConfig(String userId, String systemMessage, @Nullable int contextLength,
|
||||
@Nullable int rateLimit) {
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package de.hhhammer.dchat.db.models.user;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import de.hhhammer.dchat.db.ResultSetTransformer;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
|
||||
public record UserMessage(long id, String userId, String question, String answer, int tokens, Instant time) {
|
||||
|
||||
public static class UserMessageResultSetTransformer implements ResultSetTransformer<UserMessage> {
|
||||
|
||||
@Override
|
||||
public UserMessage transform(ResultSet resultSet) throws SQLException {
|
||||
var id = resultSet.getLong("id");
|
||||
var userId = resultSet.getString("user_id");
|
||||
var question = resultSet.getString("question");
|
||||
var answer = resultSet.getString("answer");
|
||||
var tokens = resultSet.getInt("tokens");
|
||||
var time = resultSet.getTimestamp("time").toInstant();
|
||||
return new UserMessage(id, userId, question, answer, tokens, time);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record NewUserMessage(String userId, String question, String answer, int tokens) {
|
||||
}
|
||||
}
|
|
@ -1,31 +1,26 @@
|
|||
<?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>
|
||||
<artifactId>dchat</artifactId>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<artifactId>dchat</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>db</artifactId>
|
||||
<artifactId>discord</artifactId>
|
||||
<name>discord</name>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>db</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
</build>
|
||||
</project>
|
7
discord/src/main/java/de/hhhammer/dchat/App.java
Normal file
7
discord/src/main/java/de/hhhammer/dchat/App.java
Normal file
|
@ -0,0 +1,7 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
public class App {
|
||||
public static void main( String[] args ) {
|
||||
System.out.println( "Hello World!" );
|
||||
}
|
||||
}
|
103
discord/src/main/java/de/hhhammer/dchat/DiscordBotWebSocket.java
Normal file
103
discord/src/main/java/de/hhhammer/dchat/DiscordBotWebSocket.java
Normal file
|
@ -0,0 +1,103 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.WebSocket;
|
||||
import java.net.http.WebSocket.Listener;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DiscordBotWebSocket {
|
||||
|
||||
private static final String DISCORD_API_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
||||
private static final String BOT_TOKEN = "MTA5MzE0MjYwNjg1NjMyNzE5OA.GD4FUi.e_deBXlqjeZxbdb7ooX2jWb4irM8XonGu6GpW0"; // Replace with your bot token
|
||||
private static final int HEARTBEAT_INTERVAL = 41250; // Heartbeat interval in milliseconds
|
||||
|
||||
private static boolean isConnected = false;
|
||||
|
||||
public static void main(String[] args) {
|
||||
connect();
|
||||
}
|
||||
|
||||
private static void connect() {
|
||||
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||
WebSocket webSocket = client.newWebSocketBuilder()
|
||||
.buildAsync(URI.create(DISCORD_API_URL), new DiscordListener())
|
||||
.join();
|
||||
|
||||
// Keep the client running
|
||||
while (webSocket.isInputClosed() || webSocket.isOutputClosed() || client.isTerminated()) {
|
||||
try {
|
||||
Thread.sleep(1000); // Sleep to prevent busy waiting
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendIdentify(WebSocket webSocket) {
|
||||
String identifyPayload = "{\"op\": 2, \"d\": {\"token\": \"" + BOT_TOKEN + "\", \"intents\": 513, \"properties\": {\"$os\": \"linux\", \"$browser\": \"my_library\", \"$device\": \"my_library\"}}}";
|
||||
webSocket.sendText(identifyPayload, true);
|
||||
}
|
||||
|
||||
private static void startHeartbeat(WebSocket webSocket) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
while (isConnected) {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(HEARTBEAT_INTERVAL);
|
||||
// Send heartbeat
|
||||
webSocket.sendText("{\"op\": 1, \"d\": null}", true);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class DiscordListener implements Listener {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket) {
|
||||
System.out.println("WebSocket opened");
|
||||
isConnected = true;
|
||||
sendIdentify(webSocket);
|
||||
startHeartbeat(webSocket);
|
||||
webSocket.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||
System.out.println("Received: " + data);
|
||||
// Handle incoming messages and events here
|
||||
webSocket.request(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket webSocket, Throwable error) {
|
||||
System.err.println("Error: " + error.getMessage());
|
||||
webSocket.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
||||
System.out.println("WebSocket closed: " + reason);
|
||||
isConnected = false;
|
||||
// Attempt to reconnect
|
||||
// reconnect();
|
||||
webSocket.request(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void reconnect() {
|
||||
System.out.println("Attempting to reconnect...");
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(5); // Wait before reconnecting
|
||||
connect(); // Try to reconnect
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
discord/src/main/java/de/hhhammer/dchat/Event.java
Normal file
10
discord/src/main/java/de/hhhammer/dchat/Event.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record Event(@Nullable @JsonProperty("t") EventType eventType,
|
||||
@JsonProperty("s") int sequence,
|
||||
@JsonProperty("op") int operation,
|
||||
@Nullable @JsonProperty("d") String data) {
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
public interface EventHandler {
|
||||
void handle(String data);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public class EventHandlerSelector {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EventHandlerSelector.class);
|
||||
private final ConcurrentMap<EventType, EventHandler> eventTypeEventHandlerMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Integer, EventHandler> operationEventHandlerMap = new ConcurrentHashMap<>();
|
||||
|
||||
public void handle(EventWithPreviousSequence eventWithPreviousSequence) {
|
||||
Event event = eventWithPreviousSequence.event();
|
||||
EventType eventType = event.eventType();
|
||||
int operation = event.operation();
|
||||
EventHandler handler = eventType != null ?
|
||||
eventTypeEventHandlerMap.get(eventType) : operationEventHandlerMap.get(operation);
|
||||
if (handler == null) {
|
||||
logger.warn("could not find handler for the event type '{}' and operation '{}'", eventType, operation);
|
||||
return;
|
||||
}
|
||||
handler.handle();
|
||||
}
|
||||
}
|
4
discord/src/main/java/de/hhhammer/dchat/EventType.java
Normal file
4
discord/src/main/java/de/hhhammer/dchat/EventType.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
public enum EventType {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record EventWithPreviousSequence(@NotNull Event event, int lastSequence) {
|
||||
}
|
21
discord/src/main/java/de/hhhammer/dchat/TextCollector.java
Normal file
21
discord/src/main/java/de/hhhammer/dchat/TextCollector.java
Normal file
|
@ -0,0 +1,21 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import java.net.http.WebSocket;
|
||||
|
||||
public class TextCollector {
|
||||
private final StringBuilder stringBuilder = new StringBuilder();
|
||||
private final TextHandler textHandler;
|
||||
|
||||
public TextCollector(TextHandler textHandler) {
|
||||
this.textHandler = textHandler;
|
||||
}
|
||||
|
||||
public void onText(CharSequence text, boolean last) {
|
||||
stringBuilder.append(text);
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
textHandler.handle(stringBuilder.toString());
|
||||
stringBuilder.setLength(0);
|
||||
}
|
||||
}
|
32
discord/src/main/java/de/hhhammer/dchat/TextHandler.java
Normal file
32
discord/src/main/java/de/hhhammer/dchat/TextHandler.java
Normal file
|
@ -0,0 +1,32 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class TextHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TextHandler.class);
|
||||
private final ObjectMapper objectMapper;
|
||||
private final EventHandlerSelector eventHandlerSelector;
|
||||
private final AtomicInteger lastSeq = new AtomicInteger();
|
||||
|
||||
public TextHandler(ObjectMapper objectMapper, EventHandlerSelector eventHandlerSelector) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.eventHandlerSelector = eventHandlerSelector;
|
||||
}
|
||||
|
||||
public void handle(String text) {
|
||||
try {
|
||||
Event event = objectMapper.readValue(text, Event.class);
|
||||
if (event.sequence() != 0) {
|
||||
lastSeq.set(event.sequence());
|
||||
}
|
||||
eventHandlerSelector.handle(event);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("failed to deserialize text to event", e);
|
||||
}
|
||||
}
|
||||
}
|
5
discord/src/test/java/de/hhhammer/dchat/AppTest.java
Normal file
5
discord/src/test/java/de/hhhammer/dchat/AppTest.java
Normal file
|
@ -0,0 +1,5 @@
|
|||
package de.hhhammer.dchat;
|
||||
|
||||
public class AppTest {
|
||||
|
||||
}
|
|
@ -1,90 +1,10 @@
|
|||
services:
|
||||
monolith:
|
||||
image: git.hhhammer.de/hamburghammer/dchat/monolith:latest
|
||||
bot:
|
||||
image: git.hhhammer.de/hamburghammer/dchat/bot:latest
|
||||
env_file:
|
||||
- .env
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: monolith
|
||||
target: bot
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- db
|
||||
- web
|
||||
ports:
|
||||
- 8080:8080
|
||||
user: 1000:1000
|
||||
depends_on:
|
||||
- db
|
||||
- migration
|
||||
|
||||
migration:
|
||||
image: git.hhhammer.de/hamburghammer/dchat/migration:latest
|
||||
env_file:
|
||||
- .env
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: migration
|
||||
user: 1000:1000
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- db
|
||||
|
||||
ui:
|
||||
image: git.hhhammer.de/hamburghammer/dchat/ui:latest
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8081:80
|
||||
networks:
|
||||
- ui
|
||||
|
||||
caddy:
|
||||
image: git.hhhammer.de/hamburghammer/dchat/caddy:latest
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: caddy
|
||||
restart: unless-stopped
|
||||
user: 1000:1000
|
||||
depends_on:
|
||||
- ui
|
||||
- monolith
|
||||
ports:
|
||||
- 8082:8080
|
||||
networks:
|
||||
- ui
|
||||
- web
|
||||
volumes:
|
||||
- caddy_config:/config
|
||||
|
||||
db:
|
||||
image: docker.io/postgres:15-alpine
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
networks:
|
||||
- db
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data:rw
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
volumes:
|
||||
caddy_config:
|
||||
|
||||
networks:
|
||||
db:
|
||||
ui:
|
||||
web:
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<?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>
|
||||
<artifactId>dchat</artifactId>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>migration</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<name>migration</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>fat</shadedClassifierName>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>de.hhhammer.dchat.migration.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -1,25 +0,0 @@
|
|||
package de.hhhammer.dchat.migration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*/
|
||||
public final class App {
|
||||
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
||||
private static final String DB_MIGRATION_PATH = "db/schema.sql";
|
||||
|
||||
public static void main(final String[] args) {
|
||||
final String postgresUser = System.getenv("POSTGRES_USER");
|
||||
final String postgresPassword = System.getenv("POSTGRES_PASSWORD");
|
||||
final String postgresUrl = System.getenv("POSTGRES_URL");
|
||||
if (postgresUser == null || postgresPassword == null || postgresUrl == null) {
|
||||
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
|
||||
System.exit(1);
|
||||
}
|
||||
final var migrationExecutor = new MigrationExecutor(postgresUrl, postgresUser, postgresPassword);
|
||||
final var dbMigrator = new DBMigrator(migrationExecutor, DB_MIGRATION_PATH);
|
||||
dbMigrator.run();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package de.hhhammer.dchat.migration;
|
||||
|
||||
public final class DBMigrationException extends Exception {
|
||||
public DBMigrationException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package de.hhhammer.dchat.migration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public final class DBMigrator implements Runnable {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DBMigrator.class);
|
||||
private final MigrationExecutor migrationExecutor;
|
||||
private final String resourcePath;
|
||||
|
||||
public DBMigrator(final MigrationExecutor migrationExecutor, final String resourcePath) {
|
||||
this.migrationExecutor = migrationExecutor;
|
||||
this.resourcePath = resourcePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("Starting db migration");
|
||||
final ClassLoader classLoader = getClass().getClassLoader();
|
||||
try (final InputStream inputStream = classLoader.getResourceAsStream(this.resourcePath)) {
|
||||
if (inputStream == null) {
|
||||
logger.error("Migration file not found: " + resourcePath);
|
||||
throw new RuntimeException("Migration file not found");
|
||||
}
|
||||
migrationExecutor.migrate(inputStream);
|
||||
} catch (IOException | DBMigrationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
logger.info("Finished migration");
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package de.hhhammer.dchat.migration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
public final class MigrationExecutor {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MigrationExecutor.class);
|
||||
private final String jdbcConnectionString;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public MigrationExecutor(final String jdbcConnectionString, final String username, final String password) {
|
||||
this.jdbcConnectionString = jdbcConnectionString;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void migrate(final InputStream input) throws DBMigrationException {
|
||||
try (final Connection con = DriverManager
|
||||
.getConnection(this.jdbcConnectionString, this.username, this.password);
|
||||
final Statement stmp = con.createStatement();
|
||||
) {
|
||||
final String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
stmp.execute(content);
|
||||
} catch (SQLException | IOException e) {
|
||||
throw new DBMigrationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS allowed_servers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
server_id BIGINT NOT NULL,
|
||||
comment TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
server_id BIGINT NOT NULL,
|
||||
system_message TEXT NOT NULL,
|
||||
rate_limit INT NOT NULL DEFAULT 10,
|
||||
context_length INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
server_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
question TEXT NOT NULL,
|
||||
answer TEXT NOT NULL,
|
||||
tokens int NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
-- Remove unneeded question and answer from server_messages.
|
||||
-- We only support using reply function on servers.
|
||||
ALTER TABLE server_messages
|
||||
DROP COLUMN IF EXISTS question;
|
||||
|
||||
ALTER TABLE server_messages
|
||||
DROP COLUMN IF EXISTS answer;
|
||||
|
||||
-- Remove unused context_length from server_config
|
||||
-- We only support using reply function on servers.
|
||||
ALTER TABLE server_configs
|
||||
DROP COLUMN IF EXISTS context_length;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Add user support
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS allowed_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
comment TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
system_message TEXT NOT NULL,
|
||||
rate_limit INT NOT NULL DEFAULT 10
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
tokens int NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Add question and response to the saved user message to be able to generate a conversation context
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE user_messages
|
||||
ADD COLUMN IF NOT EXISTS question TEXT NOT NULL;
|
||||
|
||||
ALTER TABLE user_messages
|
||||
ADD COLUMN IF NOT EXISTS answer TEXT NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Add a limit to the context length
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE user_configs
|
||||
ADD COLUMN IF NOT EXISTS context_length INT NOT NULL DEFAULT 5;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Add time to config to know when it was added
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE server_configs
|
||||
ADD COLUMN IF NOT EXISTS time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE user_configs
|
||||
ADD COLUMN IF NOT EXISTS time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Remove obsolete allowed tables that are being replaced the configs tables
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS allowed_servers;
|
||||
DROP TABLE IF EXISTS allowed_users;
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE server_configs
|
||||
ALTER COLUMN server_id TYPE text USING server_id::text;
|
||||
|
||||
ALTER TABLE server_messages
|
||||
ALTER COLUMN server_id TYPE text USING server_id::text;
|
||||
|
||||
ALTER TABLE user_configs
|
||||
ALTER COLUMN user_id TYPE text USING user_id::text;
|
||||
|
||||
ALTER TABLE user_messages
|
||||
ALTER COLUMN user_id TYPE text USING user_id::text;
|
||||
|
||||
COMMIT;
|
|
@ -1,13 +0,0 @@
|
|||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="de.hhhammer.dchat" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
|
@ -1,65 +0,0 @@
|
|||
<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>
|
||||
|
||||
<artifactId>monolith</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>monolith</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<artifactId>bot</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<artifactId>web</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>fat</shadedClassifierName>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>de.hhhammer.dchat.monolith.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -1,80 +0,0 @@
|
|||
package de.hhhammer.dchat.monolith;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import de.hhhammer.dchat.bot.DiscordBot;
|
||||
import de.hhhammer.dchat.bot.openai.ChatGPTService;
|
||||
import de.hhhammer.dchat.db.PostgresServerDBService;
|
||||
import de.hhhammer.dchat.db.PostgresUserDBService;
|
||||
import de.hhhammer.dchat.web.AppConfig;
|
||||
import de.hhhammer.dchat.web.WebAPI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class App {
|
||||
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
||||
|
||||
public static void main(final String[] args) {
|
||||
final String discordApiKey = System.getenv("DISCORD_API_KEY");
|
||||
if (discordApiKey == null) {
|
||||
logger.error("Missing environment variables: DISCORD_API_KEY");
|
||||
System.exit(1);
|
||||
}
|
||||
final String openaiApiKey = System.getenv("OPENAI_API_KEY");
|
||||
if (openaiApiKey == null) {
|
||||
logger.error("Missing environment variables: OPENAI_API_KEY");
|
||||
System.exit(1);
|
||||
}
|
||||
final String postgresUser = System.getenv("POSTGRES_USER");
|
||||
final String postgresPassword = System.getenv("POSTGRES_PASSWORD");
|
||||
final String postgresUrl = System.getenv("POSTGRES_URL");
|
||||
if (postgresUser == null || postgresPassword == null || postgresUrl == null) {
|
||||
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
|
||||
System.exit(1);
|
||||
}
|
||||
final String apiPortStr = System.getenv("API_PORT") != null ? System.getenv("API_PORT") : "8080";
|
||||
final int apiPort = Integer.parseInt(apiPortStr);
|
||||
final boolean debug = "true".equals(System.getenv("API_DEBUG"));
|
||||
|
||||
final var chatGPTService = new ChatGPTService(openaiApiKey, HttpClient.newHttpClient(), new ObjectMapper());
|
||||
|
||||
final var config = new HikariConfig();
|
||||
config.setJdbcUrl(postgresUrl);
|
||||
config.setUsername(postgresUser);
|
||||
config.setPassword(postgresPassword);
|
||||
|
||||
try (final var ds = new HikariDataSource(config)) {
|
||||
final var serverDBService = new PostgresServerDBService(ds);
|
||||
final var userDBService = new PostgresUserDBService(ds);
|
||||
|
||||
final var discordBot = new DiscordBot(serverDBService, userDBService, chatGPTService, discordApiKey);
|
||||
final var appConfig = new AppConfig(apiPort, debug);
|
||||
final var webApi = new WebAPI(serverDBService, userDBService, appConfig);
|
||||
run(discordBot, webApi);
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Application failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void run(final Runnable... apps) throws InterruptedException {
|
||||
final List<Callable<Void>> callableApps = Arrays.stream(apps).sequential().map(runnable -> (Callable<Void>) () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
logger.error("Running app failed", e);
|
||||
throw e;
|
||||
}
|
||||
return null;
|
||||
}).toList();
|
||||
try (final var executorService = Executors.newFixedThreadPool(apps.length)) {
|
||||
executorService.invokeAll(callableApps);
|
||||
}
|
||||
}
|
||||
}
|
119
pom.xml
119
pom.xml
|
@ -30,20 +30,14 @@
|
|||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jackson.version>2.17.1</jackson.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<!-- logging -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
|
@ -53,6 +47,13 @@
|
|||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>5.11.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
@ -71,111 +72,27 @@
|
|||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.javalin</groupId>
|
||||
<artifactId>javalin</artifactId>
|
||||
<version>6.1.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.javacord</groupId>
|
||||
<artifactId>javacord</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<groupId>com.fasterxml.jackson</groupId>
|
||||
<artifactId>jackson-bom</artifactId>
|
||||
<version>2.18.1</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-to-slf4j</artifactId>
|
||||
<version>2.23.1</version>
|
||||
<scope>runtime</scope>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</plugin>
|
||||
|
||||
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
|
||||
<plugin>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</plugin>
|
||||
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>2.5.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.8.2</version>
|
||||
</plugin>
|
||||
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
|
||||
<plugin>
|
||||
<artifactId>maven-site-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-project-info-reports-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
<modules>
|
||||
<module>bot</module>
|
||||
<module>db</module>
|
||||
<module>web</module>
|
||||
<module>migration</module>
|
||||
<module>monolith</module>
|
||||
<module>discord</module>
|
||||
</modules>
|
||||
</project>
|
30
ui/.gitignore
vendored
30
ui/.gitignore
vendored
|
@ -1,30 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/extensions.json
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
node
|
40
ui/README.md
40
ui/README.md
|
@ -1,40 +0,0 @@
|
|||
# dchat-ui
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
1
ui/env.d.ts
vendored
1
ui/env.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dchat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
5052
ui/package-lock.json
generated
5052
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "dchat-ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MPL-2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"daisyui": "^2.51.5",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.14.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.22",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "~4.8.4",
|
||||
"vite": "^4.1.4",
|
||||
"vue-tsc": "^1.2.0"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,37 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="flex-1">
|
||||
<a class="btn btn-ghost normal-case text-xl">
|
||||
<RouterLink to="/">dchat</RouterLink>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li class="m-1">
|
||||
<RouterLink to="/servers">Servers</RouterLink>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<RouterLink to="/users">Users</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<Suspense>
|
||||
<RouterView />
|
||||
</Suspense>
|
||||
<footer class="footer footer-center p-4 bg-base-300 text-base-content">
|
||||
<div>
|
||||
<p>Copyright © 2023 Augusto Dwenger J.</p>
|
||||
<p>This website is licensed under the <a href="https://git.hhhammer.de/hamburghammer/dchat/raw/branch/main/LICENSE"
|
||||
class="link">MPL-2.0 License</a>
|
||||
and uses open source software. View the <a href="https://git.hhhammer.de/hamburghammer/dchat" class="link">source
|
||||
code</a> on Forgejo.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -1 +0,0 @@
|
|||
@import './base.css';
|
|
@ -1,36 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
modalId: string | number,
|
||||
openModal: { class: string, label: string },
|
||||
isDestructive?: boolean,
|
||||
submitAction: () => Promise<void>,
|
||||
cancelAction?: () => Promise<void>,
|
||||
onLoadAction?: () => Promise<void>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isDestructive: false,
|
||||
cancelAction: () => Promise.resolve(),
|
||||
onLoadAction: () => Promise.resolve()
|
||||
})
|
||||
const modalId = "modal-" + props.modalId
|
||||
</script>
|
||||
<template>
|
||||
<!-- The button to open modal -->
|
||||
<label v-bind:for="modalId" v-bind:class="props.openModal.class" @click="props.onLoadAction">
|
||||
{{ props.openModal.label }}
|
||||
</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" v-bind:id="modalId" class="modal-toggle" />
|
||||
<div class="modal">
|
||||
<div class="modal-box">
|
||||
<slot />
|
||||
<div class="modal-action">
|
||||
<label v-if="props.isDestructive" v-bind:for="modalId" class="btn btn-error" @click="props.submitAction">Delete</label>
|
||||
<label v-else v-bind:for="modalId" class="btn btn-success" @click="props.submitAction">Save</label>
|
||||
<label v-bind:for="modalId" class="btn" @click="props.cancelAction">Cancel</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,35 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
// index arg being the index number of the row
|
||||
type ActionFunction = (index: number) => void
|
||||
const { tableHeader, tableRows, showActions } = defineProps<{
|
||||
tableHeader: string[],
|
||||
tableRows: any[][],
|
||||
showActions: boolean
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<template v-for="header in tableHeader">
|
||||
<th>{{ header }}</th>
|
||||
</template>
|
||||
<th v-if="showActions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row -->
|
||||
<tr v-for="row in tableRows">
|
||||
<template v-for="data in row">
|
||||
<th>{{ data }}</th>
|
||||
</template>
|
||||
<th>
|
||||
<slot name="action" :row="row" />
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
|
@ -1,4 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const {id} = defineProps<{id: any}>()
|
||||
debugger
|
||||
</script>
|
|
@ -1,34 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import type { ServerConfig } from "@/models/server";
|
||||
import { addConfig } from '@/services/ServerConfigs';
|
||||
import { ref } from 'vue'
|
||||
|
||||
const modalId = "addServerConfig"
|
||||
|
||||
const newConfig = ref<ServerConfig | Record<string, never>>({})
|
||||
const submitAction = async () => {
|
||||
if (!newConfig) {
|
||||
return
|
||||
}
|
||||
return addConfig(newConfig.value as ServerConfig)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-circle btn-success', label: 'Add' }"
|
||||
:submitAction="submitAction">
|
||||
<h1 class="text-xl normal-case">Add Server Config</h1>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Server ID</label>
|
||||
<input v-model="newConfig.serverId" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">System Message</label>
|
||||
<textarea v-model="newConfig.systemMessage" class="textarea textarea-bordered w-2/3 h-2/3" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Rate Limit</label>
|
||||
<input v-model.number="newConfig.rateLimit" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,17 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import { deleteConfig } from '@/services/ServerConfigs';
|
||||
|
||||
const { id } = defineProps<{ id: number }>()
|
||||
|
||||
const submitAction = async () => {
|
||||
return deleteConfig(id)
|
||||
}
|
||||
const modalId = "delete-" + id
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-sm btn-error m-1', label: 'Delete' }"
|
||||
:submitAction="submitAction" :isDestructive="true">
|
||||
<h1 class="text-xl normal-case">Are you sure?</h1>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,41 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import { getConfig } from '@/services/ServerConfigs'
|
||||
import { ref } from 'vue';
|
||||
import type { ServerConfig } from '@/models/server';
|
||||
import { updateConfig } from '@/services/ServerConfigs';
|
||||
|
||||
const { id } = defineProps<{ id: number }>()
|
||||
|
||||
const configToEdit = ref<ServerConfig | Record<string, never>>({})
|
||||
const submitAction = async () => {
|
||||
return updateConfig(id, configToEdit.value as ServerConfig)
|
||||
|
||||
}
|
||||
const onLoadAction = async () => {
|
||||
configToEdit.value = await getConfig(id)
|
||||
}
|
||||
const modalId = "edit-" + id
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-sm btn-info m-1', label: 'Edit' }"
|
||||
:submitAction="submitAction" :onLoadAction="onLoadAction">
|
||||
<h1 class="text-xl normal-case">Edit Server Config</h1>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">ID</label>
|
||||
<input v-model.number="configToEdit.id" class="input input-bordered w-full max-w-xs" disabled />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Server ID</label>
|
||||
<input v-model="configToEdit.serverId" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">System Message</label>
|
||||
<textarea v-model="configToEdit.systemMessage" class="textarea textarea-bordered w-2/3 h-2/3" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Rate Limit</label>
|
||||
<input v-model.number="configToEdit.rateLimit" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,35 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import type { UserConfig } from "@/models/user";
|
||||
import { ref } from 'vue'
|
||||
import { addConfig } from '@/services/UserConfigs';
|
||||
|
||||
const modalId = "addUserConfig"
|
||||
|
||||
const newConfig = ref<UserConfig | Record<string, never>>({})
|
||||
const submitAction = async () => {
|
||||
return addConfig(newConfig.value as UserConfig)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-circle btn-success', label: 'Add' }"
|
||||
:submitAction="submitAction">
|
||||
<h1 class="text-xl normal-case">Add User Config</h1>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">User ID</label>
|
||||
<input v-model="newConfig.userId" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">System Message</label>
|
||||
<textarea v-model="newConfig.systemMessage" class="textarea textarea-bordered w-2/3 h-2/3" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Context Length</label>
|
||||
<input v-model.number="newConfig.contextLength" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Rate Limit</label>
|
||||
<input v-model.number="newConfig.rateLimit" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,17 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import { deleteConfig } from '@/services/UserConfigs';
|
||||
|
||||
const { id } = defineProps<{ id: number }>()
|
||||
|
||||
const submitAction = async () => {
|
||||
return deleteConfig(id)
|
||||
}
|
||||
const modalId = "delete-" + id
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-sm btn-error m-1', label: 'Delete' }"
|
||||
:submitAction="submitAction" :isDestructive="true">
|
||||
<h1 class="text-xl normal-case">Are you sure?</h1>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,44 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ModalComponent from '@/components/ModalComponent.vue';
|
||||
import { getConfig } from '@/services/UserConfigs'
|
||||
import { ref } from 'vue';
|
||||
import type { UserConfig } from '@/models/user';
|
||||
import { updateConfig } from '@/services/UserConfigs';
|
||||
|
||||
const { id } = defineProps<{ id: number }>()
|
||||
|
||||
const configToEdit = ref<UserConfig | Record<string, never>>({})
|
||||
const submitAction = async () => {
|
||||
return updateConfig(id, configToEdit.value as UserConfig)
|
||||
}
|
||||
const onLoadAction = async () => {
|
||||
configToEdit.value = await getConfig(id)
|
||||
}
|
||||
const modalId = "edit-" + id
|
||||
</script>
|
||||
<template>
|
||||
<ModalComponent :modalId="modalId" :openModal="{ class: 'btn btn-sm btn-info m-1', label: 'Edit' }"
|
||||
:submitAction="submitAction" :onLoadAction="onLoadAction">
|
||||
<h1 class="text-xl normal-case">Edit User Config</h1>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">ID</label>
|
||||
<input v-model.number="configToEdit.id" class="input input-bordered w-full max-w-xs" disabled />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">User ID</label>
|
||||
<input v-model="configToEdit.userId" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">System Message</label>
|
||||
<textarea v-model="configToEdit.systemMessage" class="textarea textarea-bordered w-2/3 h-2/3" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Context Length</label>
|
||||
<input v-model.number="configToEdit.contextLength" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
<div class="m-2">
|
||||
<label class="mr-4">Rate Limit</label>
|
||||
<input v-model.number="configToEdit.rateLimit" class="input input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
|
@ -1,11 +0,0 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from '@/App.vue'
|
||||
import router from './router'
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
|
@ -1,8 +0,0 @@
|
|||
export type ServerConfig = {
|
||||
id: number,
|
||||
serverId: string,
|
||||
systemMessage: string,
|
||||
rateLimit: number,
|
||||
time: string
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export type UserConfig = {
|
||||
id: number,
|
||||
userId: string,
|
||||
systemMessage: string,
|
||||
contextLength: number,
|
||||
rateLimit: number,
|
||||
time: string
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import IndexServersView from '@/views/servers/IndexServersView.vue'
|
||||
import ServerConfigsView from '@/views/servers/ServerConfigsView.vue'
|
||||
import IndexUsersView from '@/views/users/IndexUsersView.vue'
|
||||
import UserConfigsView from '@/views/users/UserConfigsView.vue'
|
||||
|
||||
const routerConfig = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/servers',
|
||||
name: 'servers',
|
||||
component: IndexServersView
|
||||
},
|
||||
{
|
||||
path: '/servers/configs',
|
||||
name: 'serverConfigs',
|
||||
component: ServerConfigsView
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'users',
|
||||
component: IndexUsersView
|
||||
},
|
||||
{
|
||||
path: '/users/configs',
|
||||
name: 'userConfigs',
|
||||
component: UserConfigsView
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default routerConfig
|
|
@ -1,39 +0,0 @@
|
|||
import type { ServerConfig } from "@/models/server";
|
||||
|
||||
const configUrl = "/api/servers/configs/"
|
||||
|
||||
export async function getConfigs(): Promise<ServerConfig[]> {
|
||||
return fetch(configUrl)
|
||||
.then(shouldBeSuccess)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
export function getConfig(id: number): Promise<ServerConfig> {
|
||||
return fetch(configUrl + id)
|
||||
.then(shouldBeSuccess)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
export function updateConfig(id: number, config: ServerConfig): Promise<void> {
|
||||
return fetch(configUrl + id, { method: "PATCH", body: JSON.stringify(config) })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
export function addConfig(config: ServerConfig): Promise<void> {
|
||||
debugger
|
||||
return fetch(configUrl, { method: "POST", body: JSON.stringify(config) })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
export function deleteConfig(id: number): Promise<void> {
|
||||
return fetch(configUrl + id, { method: "DELETE" })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
function shouldBeSuccess(res: Response): Response {
|
||||
if (res.status >= 200 && res.status < 300) return res
|
||||
throw new Error(`Expected success status code but got: ${res.status} ${res.statusText}`)
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import type { UserConfig } from '@/models/user'
|
||||
|
||||
const configUrl = "/api/users/configs/"
|
||||
|
||||
export async function getConfigs(): Promise<UserConfig[]> {
|
||||
return fetch(configUrl)
|
||||
.then(shouldBeSuccess)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
export function getConfig(id: number): Promise<UserConfig> {
|
||||
return fetch(configUrl + id)
|
||||
.then(shouldBeSuccess)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
export function updateConfig(id: number, config: UserConfig): Promise<void> {
|
||||
return fetch(configUrl + id, { method: "PATCH", body: JSON.stringify(config) })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
export function addConfig(config: UserConfig): Promise<void> {
|
||||
debugger
|
||||
return fetch(configUrl, { method: "POST", body: JSON.stringify(config) })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
export function deleteConfig(id: number): Promise<void> {
|
||||
return fetch(configUrl + id, { method: "DELETE" })
|
||||
.then(shouldBeSuccess)
|
||||
.then()
|
||||
}
|
||||
|
||||
|
||||
function shouldBeSuccess(res: Response): Response {
|
||||
if (res.status >= 200 && res.status < 300) return res
|
||||
throw new Error(`Expected success status code but got: ${res.status} ${res.statusText}`)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<h1 class="text-xl normal-case m-5">Overview</h1>
|
||||
<div class="flex justify-center m-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<RouterLink to="/servers">
|
||||
<button class="btn btn-wide btn-outline">Servers</button>
|
||||
</RouterLink>
|
||||
<RouterLink to="/users">
|
||||
<button class="btn btn-wide btn-outline">Users</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
|
||||
</template>
|
|
@ -1,12 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
<h1 class="text-xl normal-case m-5">Server overview</h1>
|
||||
<div class="flex justify-center m-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<RouterLink to="/servers/configs">
|
||||
<button class="btn btn-wide btn-outline">Configs</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,27 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import TableComponent from '@/components/TableComponent.vue';
|
||||
import AddServerConfig from '@/components/server/AddServerConfig.vue';
|
||||
import { getConfigs } from '@/services/ServerConfigs'
|
||||
import EditServerConfig from '@/components/server/EditServerConfig.vue';
|
||||
import DeleteServerConfig from '@/components/server/DeleteServerConfig.vue';
|
||||
|
||||
const trimSystemMessage = (str: string) => str.length > 50 ? str.slice(0, 47) + "..." : str
|
||||
|
||||
const serverConfigs = await getConfigs()
|
||||
|
||||
const tableHeader = ["ID", "Server ID", "System Message", "Rate limit", "Created"]
|
||||
const tableRows = serverConfigs.map(config => [config.id, config.serverId, trimSystemMessage(config.systemMessage), config.rateLimit, config.time])
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex justify-between m-5">
|
||||
<h1 class="text-xl normal-case">Server Configs</h1>
|
||||
<AddServerConfig />
|
||||
</div>
|
||||
<TableComponent v-if="serverConfigs.length" :tableHeader="tableHeader" :tableRows="tableRows" :showActions="true">
|
||||
<template #action="action">
|
||||
<EditServerConfig :id="action.row[0]" />
|
||||
<DeleteServerConfig :id="action.row[0]" />
|
||||
</template>
|
||||
</TableComponent>
|
||||
<p v-else class="m-5">No server configs</p>
|
||||
</template>
|
|
@ -1,12 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
<h1 class="text-xl normal-case m-5">Server overview</h1>
|
||||
<div class="flex justify-center m-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<RouterLink to="/users/configs">
|
||||
<button class="btn btn-wide btn-outline">Configs</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,27 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import TableComponent from '@/components/TableComponent.vue';
|
||||
import AddUserConfig from '@/components/user/AddUserConfig.vue';
|
||||
import { getConfigs } from '@/services/UserConfigs'
|
||||
import EditUserConfig from '@/components/user/EditUserConfig.vue';
|
||||
import DeleteServerConfig from '@/components/server/DeleteServerConfig.vue';
|
||||
|
||||
const trimSystemMessage = (str: string) => str.length > 50 ? str.slice(0, 47) + "..." : str
|
||||
|
||||
const userConfigs = await getConfigs()
|
||||
|
||||
const tableHeader = ["ID", "User ID", "System Message", "Context Length", "Rate limit", "Created"]
|
||||
const tableRows = userConfigs.map(config => [config.id, config.userId, trimSystemMessage(config.systemMessage), config.contextLength, config.rateLimit, config.time])
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex justify-between m-5">
|
||||
<h1 class="text-xl normal-case">User Configs</h1>
|
||||
<AddUserConfig />
|
||||
</div>
|
||||
<TableComponent v-if="userConfigs.length" :tableHeader="tableHeader" :tableRows="tableRows" :showActions="true">
|
||||
<template #action="action">
|
||||
<EditUserConfig :id="action.row[0]" />
|
||||
<DeleteServerConfig :id="action.row[0]" />
|
||||
</template>
|
||||
</TableComponent>
|
||||
<p v-else class="m-5">No user configs</p>
|
||||
</template>
|
|
@ -1,9 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
77
web/pom.xml
77
web/pom.xml
|
@ -1,77 +0,0 @@
|
|||
<?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>
|
||||
<artifactId>dchat</artifactId>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>web</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>web</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.hhhammer.dchat</groupId>
|
||||
<artifactId>db</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.javalin</groupId>
|
||||
<artifactId>javalin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>fat</shadedClassifierName>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>de.hhhammer.dchat.web.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -1,43 +0,0 @@
|
|||
package de.hhhammer.dchat.web;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import de.hhhammer.dchat.db.PostgresServerDBService;
|
||||
import de.hhhammer.dchat.db.PostgresUserDBService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*/
|
||||
public final class App {
|
||||
private static final Logger logger = LoggerFactory.getLogger(App.class);
|
||||
|
||||
public static void main(final String[] args) {
|
||||
final String postgresUser = System.getenv("POSTGRES_USER");
|
||||
final String postgresPassword = System.getenv("POSTGRES_PASSWORD");
|
||||
final String postgresUrl = System.getenv("POSTGRES_URL");
|
||||
if (postgresUser == null || postgresPassword == null || postgresUrl == null) {
|
||||
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
final String apiPortStr = System.getenv("API_PORT") != null ? System.getenv("API_PORT") : "8080";
|
||||
final int apiPort = Integer.parseInt(apiPortStr);
|
||||
final boolean debug = "true".equals(System.getenv("API_DEBUG"));
|
||||
|
||||
final var config = new HikariConfig();
|
||||
config.setJdbcUrl(postgresUrl);
|
||||
config.setUsername(postgresUser);
|
||||
config.setPassword(postgresPassword);
|
||||
|
||||
try (final var ds = new HikariDataSource(config)) {
|
||||
final var serverDBService = new PostgresServerDBService(ds);
|
||||
final var userDBService = new PostgresUserDBService(ds);
|
||||
final var appConfig = new AppConfig(apiPort, debug);
|
||||
|
||||
final var webApi = new WebAPI(serverDBService, userDBService, appConfig);
|
||||
webApi.run();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
package de.hhhammer.dchat.web;
|
||||
|
||||
public record AppConfig(int port, boolean debug) {
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package de.hhhammer.dchat.web;
|
||||
|
||||
import de.hhhammer.dchat.db.ServerDBService;
|
||||
import de.hhhammer.dchat.db.UserDBService;
|
||||
import de.hhhammer.dchat.web.server.ConfigCrudHandler;
|
||||
import de.hhhammer.dchat.web.user.ConfigUserCrudHandler;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import io.javalin.plugin.bundled.DevLoggingPlugin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static io.javalin.apibuilder.ApiBuilder.crud;
|
||||
import static io.javalin.apibuilder.ApiBuilder.path;
|
||||
|
||||
public final class WebAPI implements Runnable {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebAPI.class);
|
||||
private final ServerDBService serverDBService;
|
||||
private final UserDBService userDBService;
|
||||
private final AppConfig appConfig;
|
||||
|
||||
public WebAPI(final ServerDBService serverDBService, final UserDBService userDBService, final AppConfig appConfig) {
|
||||
this.serverDBService = serverDBService;
|
||||
this.userDBService = userDBService;
|
||||
this.appConfig = appConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("Starting web application");
|
||||
final Javalin app = Javalin.create(config -> {
|
||||
if (appConfig.debug()) config.registerPlugin(new DevLoggingPlugin());
|
||||
config.http.prefer405over404 = true; // return 405 instead of 404 if path is mapped to different HTTP method
|
||||
config.http.defaultContentType = "application/json";
|
||||
config.useVirtualThreads = true;
|
||||
config.showJavalinBanner = false;
|
||||
config.router.apiBuilder(() ->
|
||||
path("api", () -> {
|
||||
path("servers", () -> crud("configs/{id}", new ConfigCrudHandler(this.serverDBService)));
|
||||
path("users", () -> crud("configs/{id}", new ConfigUserCrudHandler(this.userDBService)));
|
||||
})
|
||||
);
|
||||
});
|
||||
final var waitForShutdown = new CompletableFuture<Void>();
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("Shutting down web application");
|
||||
app.stop();
|
||||
waitForShutdown.complete(null);
|
||||
|
||||
}));
|
||||
app.events(event -> {
|
||||
event.serverStopping(() -> logger.info("Stopping web service"));
|
||||
event.serverStopped(() -> logger.info("Stopped web service"));
|
||||
});
|
||||
app.before(ctx -> {
|
||||
ctx.header("Access-Control-Allow-Origin", "*");
|
||||
ctx.header("Access-Control-Allow-Methods", "*");
|
||||
});
|
||||
app.options("*", ctx -> ctx.status(HttpStatus.OK));
|
||||
|
||||
|
||||
app.start(appConfig.port());
|
||||
try {
|
||||
waitForShutdown.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package de.hhhammer.dchat.web.server;
|
||||
|
||||
import de.hhhammer.dchat.db.DBException;
|
||||
import de.hhhammer.dchat.db.ServerDBService;
|
||||
import de.hhhammer.dchat.db.models.server.ServerConfig;
|
||||
import io.javalin.apibuilder.CrudHandler;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ConfigCrudHandler implements CrudHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConfigCrudHandler.class);
|
||||
|
||||
private final ServerDBService serverDBService;
|
||||
|
||||
public ConfigCrudHandler(final ServerDBService serverDBService) {
|
||||
this.serverDBService = serverDBService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(@NotNull final Context context) {
|
||||
final ServerConfig.NewServerConfig body = context.bodyAsClass(ServerConfig.NewServerConfig.class);
|
||||
try {
|
||||
this.serverDBService.addConfig(body);
|
||||
} catch (DBException e) {
|
||||
logger.error("Adding new server configuration", e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
context.status(HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(@NotNull final Context context, @NotNull final String s) {
|
||||
final var id = Long.parseLong(s);
|
||||
try {
|
||||
this.serverDBService.deleteConfig(id);
|
||||
context.status(HttpStatus.NO_CONTENT);
|
||||
} catch (DBException e) {
|
||||
logger.error("Deleting configuration with id: " + s, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getAll(@NotNull final Context context) {
|
||||
try {
|
||||
final List<ServerConfig> allowedServers = this.serverDBService.getAllConfigs();
|
||||
context.json(allowedServers);
|
||||
} catch (DBException e) {
|
||||
logger.error("Getting all server configs", e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOne(@NotNull final Context context, @NotNull final String s) {
|
||||
final var id = Long.parseLong(s);
|
||||
try {
|
||||
final Optional<ServerConfig> server = this.serverDBService.getConfigBy(id);
|
||||
if (server.isEmpty()) {
|
||||
context.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
context.json(server.get());
|
||||
} catch (DBException e) {
|
||||
logger.error("Searching for config with id: " + s, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(@NotNull final Context context, @NotNull final String idString) {
|
||||
final ServerConfig.NewServerConfig body = context.bodyAsClass(ServerConfig.NewServerConfig.class);
|
||||
final var id = Long.parseLong(idString);
|
||||
|
||||
try {
|
||||
this.serverDBService.updateConfig(id, body);
|
||||
} catch (DBException e) {
|
||||
logger.error("Updating allowed server with id: " + idString, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package de.hhhammer.dchat.web.user;
|
||||
|
||||
import de.hhhammer.dchat.db.DBException;
|
||||
import de.hhhammer.dchat.db.UserDBService;
|
||||
import de.hhhammer.dchat.db.models.user.UserConfig;
|
||||
import io.javalin.apibuilder.CrudHandler;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ConfigUserCrudHandler implements CrudHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConfigUserCrudHandler.class);
|
||||
|
||||
private final UserDBService userDBService;
|
||||
|
||||
public ConfigUserCrudHandler(final UserDBService userDBService) {
|
||||
this.userDBService = userDBService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(@NotNull final Context context) {
|
||||
final UserConfig.NewUserConfig body = context.bodyAsClass(UserConfig.NewUserConfig.class);
|
||||
try {
|
||||
this.userDBService.addConfig(body);
|
||||
} catch (DBException e) {
|
||||
logger.error("Adding new server configuration", e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
context.status(HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(@NotNull final Context context, @NotNull final String s) {
|
||||
final var id = Long.parseLong(s);
|
||||
try {
|
||||
this.userDBService.deleteConfig(id);
|
||||
context.status(HttpStatus.NO_CONTENT);
|
||||
} catch (DBException e) {
|
||||
logger.error("Deleting configuration with id: " + s, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getAll(@NotNull final Context context) {
|
||||
try {
|
||||
final List<UserConfig> allowedServers = this.userDBService.getAllConfigs();
|
||||
context.json(allowedServers);
|
||||
} catch (DBException e) {
|
||||
logger.error("Getting all user configs", e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOne(@NotNull final Context context, @NotNull final String s) {
|
||||
final var id = Long.parseLong(s);
|
||||
try {
|
||||
final Optional<UserConfig> server = this.userDBService.getConfigBy(id);
|
||||
if (server.isEmpty()) {
|
||||
context.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
context.json(server.get());
|
||||
} catch (DBException e) {
|
||||
logger.error("Searching for config with id: " + s, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(@NotNull Context context, @NotNull String idString) {
|
||||
final UserConfig.NewUserConfig body = context.bodyAsClass(UserConfig.NewUserConfig.class);
|
||||
final var id = Long.parseLong(idString);
|
||||
|
||||
try {
|
||||
this.userDBService.updateConfig(id, body);
|
||||
} catch (DBException e) {
|
||||
logger.error("Updating user config with id: " + idString, e);
|
||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="de.hhhammer.dchat" level="DEBUG"/>
|
||||
<logger name="io.javalin.Javalin" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
Loading…
Reference in a new issue