Compare commits

...

5 commits

9 changed files with 119 additions and 59 deletions

13
.env.example Normal file
View file

@ -0,0 +1,13 @@
# No need to change
POSTGRES_DB=dchat
# "db" being the name of the postgresql service inside the docker-compose.yml, "5432" is the port on which postgres
# listens for new connections and "dchat" is the database name.
POSTGRES_URL=jdbc:postgresql://db:5432/dchat
# Please change
DISCORD_API_KEY=<discord-api-key>
OPENAI_API_KEY=<openai-api-key>
# Those values can not change after the first start
POSTGRES_USER=<postgres-user>
POSTGRES_PASSWORD=<postgres-password>

1
.gitignore vendored
View file

@ -125,3 +125,4 @@ fabric.properties
# Android studio 3.1+ serialized cache file # Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
.env

View file

@ -7,28 +7,67 @@ A ChatGPT Bot for Discord.
Requirements: Requirements:
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [Podman](https://podman.io/) or [Docker](https://www.docker.com/) - [Docker](https://www.docker.com/)
- [OpenAI API Key](https://platform.openai.com/account/api-keys) - [OpenAI API Key](https://platform.openai.com/account/api-keys)
- [Discord Bot Token](https://javacord.org/wiki/getting-started/creating-a-bot-account.html) - [Discord Bot Token](https://javacord.org/wiki/getting-started/creating-a-bot-account.html)
Clone the project: ### Clone the project
```shell ```shell
git clone https://git.hhhammer.de/hamburghammer/dchat.git git clone https://git.hhhammer.de/hamburghammer/dchat.git
cd dchat
``` ```
and navigate into it. ### Obtain the images
Change the environment variable inside the `docker-compose.yml`. #### Build the images
```shell ```shell
podman-compose up -d docker compose build
``` ```
#### Pull the prebuilt images
```shell
docker compose pull
```
### Configure environment
```shell
cp .env.example .env
```
Fill the required variables.
### Start
For the fist time we want to start the containers in the following order:
First crate the DB.
```shell
docker compose up -d db
```
Crate the required tables and migrate already existing data.
```shell
docker compose up -d migration
```
Start the final apps.
```shell
docker compose up -d
```
### Invite
Invite the bot through the link provided in the container logs. Invite the bot through the link provided in the container logs.
```shell ```shell
podman container logs dchat_db_1 docker compose logs bot
``` ```
## LICENSE ## LICENSE

View file

@ -1,10 +1,17 @@
package de.hhhammer.dchat.bot.discord; package de.hhhammer.dchat.bot.discord;
import de.hhhammer.dchat.bot.openai.ResponseException;
import org.javacord.api.entity.message.MessageType; import org.javacord.api.entity.message.MessageType;
import org.javacord.api.event.message.MessageCreateEvent; import org.javacord.api.event.message.MessageCreateEvent;
import org.javacord.api.listener.message.MessageCreateListener; import org.javacord.api.listener.message.MessageCreateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class MessageCreateHandler implements MessageCreateListener { public class MessageCreateHandler implements MessageCreateListener {
private static final Logger logger = LoggerFactory.getLogger(MessageCreateHandler.class);
private final MessageHandler messageHandler; private final MessageHandler messageHandler;
public MessageCreateHandler(MessageHandler messageHandler) { public MessageCreateHandler(MessageHandler messageHandler) {
@ -27,7 +34,12 @@ public class MessageCreateHandler implements MessageCreateListener {
event.getChannel().sendMessage("Rate limit hit - cooling down..."); event.getChannel().sendMessage("Rate limit hit - cooling down...");
return; return;
} }
this.messageHandler.handle(event); try {
this.messageHandler.handle(event);
} catch (ResponseException | IOException | InterruptedException e) {
logger.error("Reading a message from the listener", e);
event.getMessage().reply("Sorry but something went wrong :(");
}
}); });
} }
} }

View file

@ -1,9 +1,12 @@
package de.hhhammer.dchat.bot.discord; package de.hhhammer.dchat.bot.discord;
import de.hhhammer.dchat.bot.openai.ResponseException;
import org.javacord.api.event.message.MessageCreateEvent; import org.javacord.api.event.message.MessageCreateEvent;
import java.io.IOException;
public interface MessageHandler { public interface MessageHandler {
void handle(MessageCreateEvent event); void handle(MessageCreateEvent event) throws ResponseException, IOException, InterruptedException;
boolean isAllowed(MessageCreateEvent event); boolean isAllowed(MessageCreateEvent event);

View file

@ -27,7 +27,7 @@ public class ServerMessageHandler implements MessageHandler {
} }
@Override @Override
public void handle(MessageCreateEvent event) { public void handle(MessageCreateEvent event) throws ResponseException, IOException, InterruptedException {
String content = extractContent(event); String content = extractContent(event);
var serverId = event.getServer().get().getId(); var serverId = event.getServer().get().getId();
var systemMessage = this.serverDBService.getConfig(String.valueOf(serverId)).get().systemMessage(); var systemMessage = this.serverDBService.getConfig(String.valueOf(serverId)).get().systemMessage();
@ -41,19 +41,14 @@ public class ServerMessageHandler implements MessageHandler {
.stream().toList(), .stream().toList(),
content, systemMessage) : content, systemMessage) :
new ChatGPTRequestBuilder().simpleRequest(content, systemMessage); new ChatGPTRequestBuilder().simpleRequest(content, systemMessage);
try { var response = this.chatGPTService.submit(request);
var response = this.chatGPTService.submit(request); if (response.choices().size() < 1) {
if (response.choices().size() < 1) { event.getMessage().reply("No response available");
event.getChannel().sendMessage("No response available"); return;
return;
}
var answer = response.choices().get(0).message().content();
logServerMessage(event, response.usage().totalTokens());
event.getMessage().reply(answer);
} catch (IOException | InterruptedException | ResponseException e) {
logger.error("Reading a message from the listener", e);
event.getChannel().sendMessage("Sorry but something went wrong :(");
} }
var answer = response.choices().get(0).message().content();
logServerMessage(event, response.usage().totalTokens());
event.getMessage().reply(answer);
} }
@Override @Override

View file

@ -1,10 +1,10 @@
package de.hhhammer.dchat.bot.discord; package de.hhhammer.dchat.bot.discord;
import de.hhhammer.dchat.db.UserDBService;
import de.hhhammer.dchat.db.models.user.UserMessage;
import de.hhhammer.dchat.bot.openai.ChatGPTRequestBuilder; import de.hhhammer.dchat.bot.openai.ChatGPTRequestBuilder;
import de.hhhammer.dchat.bot.openai.ChatGPTService; import de.hhhammer.dchat.bot.openai.ChatGPTService;
import de.hhhammer.dchat.bot.openai.ResponseException; import de.hhhammer.dchat.bot.openai.ResponseException;
import de.hhhammer.dchat.db.UserDBService;
import de.hhhammer.dchat.db.models.user.UserMessage;
import org.javacord.api.event.message.MessageCreateEvent; import org.javacord.api.event.message.MessageCreateEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -22,24 +22,19 @@ public class UserMessageHandler implements MessageHandler {
} }
@Override @Override
public void handle(MessageCreateEvent event) { public void handle(MessageCreateEvent event) throws ResponseException, IOException, InterruptedException {
String content = event.getReadableMessageContent(); String content = event.getReadableMessageContent();
var userId = event.getMessageAuthor().getId(); var userId = event.getMessageAuthor().getId();
var systemMessage = this.userDBService.getConfig(String.valueOf(userId)).get().systemMessage(); var systemMessage = this.userDBService.getConfig(String.valueOf(userId)).get().systemMessage();
var request = new ChatGPTRequestBuilder().simpleRequest(content, systemMessage); var request = new ChatGPTRequestBuilder().simpleRequest(content, systemMessage);
try { var response = this.chatGPTService.submit(request);
var response = this.chatGPTService.submit(request); if (response.choices().size() < 1) {
if (response.choices().size() < 1) { event.getMessage().reply("No response available");
event.getChannel().sendMessage("No response available"); return;
return;
}
var answer = response.choices().get(0).message().content();
logUserMessage(event, content, answer, response.usage().totalTokens());
event.getMessage().reply(answer);
} catch (IOException | InterruptedException | ResponseException e) {
logger.error("Reading a message from the listener", e);
event.getChannel().sendMessage("Sorry but something went wrong :(");
} }
var answer = response.choices().get(0).message().content();
logUserMessage(event, content, answer, response.usage().totalTokens());
event.getMessage().reply(answer);
} }
@Override @Override

View file

@ -12,7 +12,7 @@ import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
public class ChatGPTService { public class ChatGPTService {
private final String url = "https://api.openai.com/v1/chat/completions"; private static final String url = "https://api.openai.com/v1/chat/completions";
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
private final String apiKey; private final String apiKey;
private final HttpClient httpClient; private final HttpClient httpClient;
@ -29,15 +29,14 @@ public class ChatGPTService {
.POST(HttpRequest.BodyPublishers.ofByteArray(data)) .POST(HttpRequest.BodyPublishers.ofByteArray(data))
.setHeader("Content-Type", "application/json") .setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer " + this.apiKey) .setHeader("Authorization", "Bearer " + this.apiKey)
.timeout(Duration.ofSeconds(30)) .timeout(Duration.ofSeconds(90))
.build(); .build();
var responseStream = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); var responseStream = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (responseStream.statusCode() != 200) { if (responseStream.statusCode() != 200) {
throw new ResponseException("Response status code was not 200: " + responseStream.statusCode()); throw new ResponseException("Response status code was not 200: " + responseStream.statusCode());
} }
ChatGPTResponse response = mapper.readValue(responseStream.body(), ChatGPTResponse.class);
return response; return mapper.readValue(responseStream.body(), ChatGPTResponse.class);
} }
} }

View file

@ -3,46 +3,49 @@ version: "3"
services: services:
bot: bot:
image: git.hhhammer.de/hamburghammer/dchat/bot:latest image: git.hhhammer.de/hamburghammer/dchat/bot:latest
build: ./bot/ build:
dockerfile: Containerfile
context: .
target: bot
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- db - db
- migration
environment: environment:
- DISCORD_API_KEY=<discord-api-key>
- OPENAI_API_KEY=<openai-api-key>
- POSTGRES_USER=<postgres-user>
- POSTGRES_PASSWORD=<postgres-password>
- POSTGRES_URL=jdbc:postgresql://db:5432/dchat
- JDK_JAVA_OPTIONS="--enable-preview" - JDK_JAVA_OPTIONS="--enable-preview"
web: web:
image: git.hhhammer.de/hamburghammer/dchat/web:latest image: git.hhhammer.de/hamburghammer/dchat/web:latest
build: ./web/ build:
dockerfile: Containerfile
context: .
target: web
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- db - db
- migration
ports:
- 8080:8080
environment: environment:
- POSTGRES_USER=<postgres-user>
- POSTGRES_PASSWORD=<postgres-password>
- POSTGRES_URL=jdbc:postgresql://db:5432/dchat
- JDK_JAVA_OPTIONS="--enable-preview" - JDK_JAVA_OPTIONS="--enable-preview"
migration: migration:
image: git.hhhammer.de/hamburghammer/dchat/migration:latest image: git.hhhammer.de/hamburghammer/dchat/migration:latest
build: ./migration/ build:
dockerfile: Containerfile
context: .
target: migration
depends_on: depends_on:
- db - db
environment:
- POSTGRES_USER=<postgres-user>
- POSTGRES_PASSWORD=<postgres-password>
- POSTGRES_URL=jdbc:postgresql://db:5432/dchat
db: db:
image: docker.io/postgres:15-alpine image: docker.io/postgres:15-alpine
restart: unless-stopped restart: unless-stopped
environment:
- POSTGRES_USER=<postgres-user>
- POSTGRES_PASSWORD=<postgres-password>
- POSTGRES_DB=dchat
volumes: volumes:
- ./data/postgres:/var/lib/postgresql/data:rw - ./data/postgres:/var/lib/postgresql/data:rw
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-d", $POSTGRES_DB ]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s