Compare commits

...

60 commits

Author SHA1 Message Date
a398641172 Change type of user_id and server_id from long to string
JS don't support such big numbers.
2023-05-08 18:02:32 +02:00
4ce0f70d5d Fix not updateing user_id on config entity 2023-05-08 17:46:52 +02:00
568e175208 Use the system message from the config 2023-05-08 17:31:05 +02:00
e6b7f10b87 Fix grama 2023-05-08 16:34:00 +02:00
7d4ff3ce64 Add automatic migration on startup
Executes on ever start up the schema.sql file.
2023-05-08 16:13:43 +02:00
1060d4d577 Update Javalin to version 5.5.0 2023-05-05 01:25:15 +02:00
24adb8d0ce Add redirect to index.html if file was not found
This should help using the SPA
2023-05-05 01:23:24 +02:00
ceb2fe27a7 Add /api to api endpoints
To differentiate beween file access and api call.
2023-05-05 01:22:27 +02:00
bc4a5240d7 Fix reading API_PORT env 2023-05-05 01:21:07 +02:00
9b6409914a Add vuejs to build container image 2023-05-05 01:20:24 +02:00
391f3969c6 Add static fileserver for vue app 2023-05-05 00:18:41 +02:00
abbb02729a Implement CRUD for user config 2023-05-04 23:46:27 +02:00
0769040fc0 Fix deserialization of incomming new entities 2023-05-04 23:41:50 +02:00
5d34208625 Fix failing OPTIONS CORS browser request 2023-05-04 23:40:24 +02:00
e24f80cea5 Implement CRUD on ServerConfigs 2023-05-04 23:39:44 +02:00
40d33a45e6 Fix cutting system message on small messages
It will now only cut the message if its longer than 50 characters.
2023-05-04 23:26:13 +02:00
ac43032aab Fix table rendering with more than one entry 2023-05-04 23:25:24 +02:00
12e807c5ed Rework how actions work 2023-05-04 21:10:54 +02:00
44c6a3b1a7 Update UserConfigsView to have all entity fields 2023-05-03 20:35:07 +02:00
cf42778103 Add loading server configs 2023-05-03 20:34:45 +02:00
3c1a049dd7 Wrap RouterView in Suspend
This should allow us to use async/await in the component setup.
2023-05-03 20:29:02 +02:00
5509126896 Remove allowed views
The endpoints no longer exists.
2023-05-03 19:25:19 +02:00
2b3d812c00 Drop unused allowed tables 2023-05-03 19:15:38 +02:00
c1e2f4c4f3 Add time to configs tables
This should allow us to see when the entity was created.
2023-05-03 19:14:52 +02:00
aaffdda371 Fix resources paths 2023-05-03 19:08:16 +02:00
ed6ff54537 Remove usage of allowed entity from user and server resources 2023-05-03 19:03:28 +02:00
255a4ede3e Add CORS for in the API
We currently don't need cors.
2023-05-03 18:40:31 +02:00
806aee663d Remove message related views
There is currently no way to extract them with the api.
2023-05-03 17:28:44 +02:00
816eddd039 Remove before function to check login state on router 2023-04-30 03:14:33 +02:00
acfec04173 Add Caddy setup for serving the ui 2023-04-30 03:02:46 +02:00
86de126b41 Fix vite build
Removes remaining pina references from vuejs setup.
2023-04-30 01:58:20 +02:00
bfa5372ce4 Remove AccessManager from web API 2023-04-29 16:50:40 +02:00
a7388de9f5 Add Servers and Users to the nav menu 2023-04-29 16:41:27 +02:00
b82355ea1a Remove all login and auth related code
I am gonna use something like caddy as revers proxy to expose the service.
Caddy offers the functionality to secure resources with basic auth
and promt the browser on usage. I am gona go with it. It's the simplest
solution.

I still need to remove the basic auth code from the backend and craft
a configuration for caddy.
2023-04-29 16:34:58 +02:00
48cc667d26 Duplicate servers views for users 2023-04-29 15:53:53 +02:00
6cde6438d2 Remove unused action from ServerMessagesView
There are no actions for this view.
2023-04-29 15:43:37 +02:00
c096ec17c1 Make actions column in TableComponent optional 2023-04-29 15:42:53 +02:00
656e202abd Remove event handling in favor of passing function as prop 2023-04-28 15:10:41 +02:00
80b92dd1ea Add support for defining custom actions
Little bit tricky, because Vue will only allow the call of 'inject()'
from inside of the setup method or from a functional component. Using a
functional component is not an option since the value that we want to
inject is not provided by the app, its being provided by the parent
component. Using inject inside the component will also not work, since
we can not call it inside the setup. [1]

Using an handler function that the parent component provides is a
compromise to be still able to be as dynamic as possible.

Refs:
- [1] https://github.com/vueuse/vueuse/issues/1061
2023-04-28 02:44:28 +02:00
cb0633b9a2 Add home header
This is to make it look and feel like the server pages.
2023-04-28 00:53:27 +02:00
6d837c514a Fix license link
Point link to the license file on Forgejo.
2023-04-28 00:52:22 +02:00
3c8e2ed5e0 Add views for selecting server and viewing its messages 2023-04-28 00:51:34 +02:00
63b18c3f9f Add login elements
Missing logic though...
2023-04-28 00:19:08 +02:00
170300d67f Add server configs view
Including routing and linking.
2023-04-28 00:10:02 +02:00
56fb3bfe36 Add navigation concept from home to other pages 2023-04-28 00:00:46 +02:00
b0dabe0c61 Move DefaultLayout into App
No need to hold the layout in a extra file.
2023-04-27 23:24:50 +02:00
9bf662fc78 Move Footer and Nav component into DefaultLayout 2023-04-27 23:23:03 +02:00
e5194fed4f Move AllowedServer component into AllowedServersView 2023-04-27 23:20:50 +02:00
6d490ae25f Remove allowedServer table from home
The table/view was moved into the AllowedServersView.
2023-04-27 23:19:32 +02:00
a8fa525a30 Add a footer
The footer holds the copyright/license and source code informations.
2023-04-27 23:16:01 +02:00
0af571adf6 Remove unused export maps 2023-04-27 23:01:33 +02:00
6f8b682f0a Add DefaultLayout as default on App level 2023-04-27 22:59:55 +02:00
8323dd48ba Fix missing typescript script attribute
Not having the 'lang="ts"' produces an type warning.
2023-04-27 22:56:47 +02:00
dca11b2347 Add view for allowed servers 2023-04-27 22:51:14 +02:00
a0204e4da4 Change nav header to be app specific
Update content to match app requirements.
2023-04-27 17:29:26 +02:00
9a2861fc3f Move HomeView layout into DefaultLayout.vue
This is to make it easier to have the same layout on all views.
2023-04-27 17:25:34 +02:00
6c267b232c Add missing header and action buttons
The buttons currently don't provide any functionality.
2023-04-27 17:20:38 +02:00
d5a6da0647 Fix router
The router wouldn't work, there was a miss configuration regarding
imports and lazy loading components.
2023-04-27 17:19:59 +02:00
9914171dcb Add table component to show allowed servers entities 2023-04-27 15:29:11 +02:00
b96a544452 Add vue init 2023-04-16 20:40:48 +02:00
68 changed files with 6084 additions and 553 deletions

View file

@ -1,3 +1,13 @@
*
!src/
!pom.xml
!ui/package*.json
!ui/postcss.config.js
!ui/tailwind.config.js
!ui/env.d..ts
!ui/vite.config.ts
!ui/tsconfig.json
!ui/tsconfig.node.json
!ui/index.html
!ui/public/
!ui/src/

View file

@ -1,4 +1,4 @@
# Stage 1: Build the application
# Stage 1: Build java application
FROM docker.io/maven:3.9-eclipse-temurin-19 AS maven
WORKDIR /app
COPY pom.xml .
@ -7,9 +7,19 @@ RUN mvn package
COPY src/ /app/src/
RUN mvn package
# Stage 2: Create the jlink app
# Stage 2: Build vuejs application
FROM docker.io/node:18-slim AS vuejs
WORKDIR /app
COPY ./ui/package* .
RUN npm ci
COPY ./ui .
RUN npm run build-only
# Stage 3: Create the jlink app
FROM docker.io/eclipse-temurin:19-jdk
WORKDIR /app
COPY --from=maven /app/target/dchat-*-fat.jar /app/dchat.jar
COPY --from=vuejs /app/dist /app/ui/dist
EXPOSE 8080
CMD ["java", "--enable-preview", "-jar", "/app/dchat.jar"]

View file

@ -22,12 +22,6 @@ and navigate into it.
Change the environment variable inside the `docker-compose.yml`.
```shell
podman-compose pull
podman-compose start db
podman cp schema.sql dchat_db_1:/schema.sql
podman exec -it dchat_db_1 /bin/bash
psql --user dchat dchat < schema.sql
exit
podman-compose up -d
```

View file

@ -14,8 +14,6 @@ services:
- POSTGRES_PASSWORD=<postgres-password>
- POSTGRES_URL=jdbc:postgresql://db:5432/dchat
- API_PORT=8080
- API_USERNAME=<api-username>
- API_PASSWORD=<api-password>
db:
image: docker.io/postgres:15-alpine
restart: unless-stopped

View file

@ -40,7 +40,7 @@
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>5.4.2</version>
<version>5.5.0</version>
</dependency>
<!-- logging -->
<dependency>

View file

@ -0,0 +1,36 @@
package de.hhhammer.dchat;
import de.hhhammer.dchat.migration.DBMigrationException;
import de.hhhammer.dchat.migration.MigrationExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
public class DBMigrator implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(DBMigrator.class);
private final MigrationExecutor migrationExecutor;
private final String resourcePath;
public DBMigrator(MigrationExecutor migrationExecutor, String resourcePath) {
this.migrationExecutor = migrationExecutor;
this.resourcePath = resourcePath;
}
@Override
public void run() {
logger.info("Starting db migration");
ClassLoader classLoader = getClass().getClassLoader();
try (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");
}
}

View file

@ -56,7 +56,7 @@ public class DiscordBot implements Runnable {
if (server.isEmpty()) {
return;
}
var tokens = this.serverDBService.tokensOfLast30Days(server.get().getId());
var tokens = this.serverDBService.tokensOfLast30Days(String.valueOf(server.get().getId()));
interactionOriginalResponseUpdater.setContent("" + tokens).update();
});
return;

View file

@ -2,22 +2,26 @@ package de.hhhammer.dchat;
import de.hhhammer.dchat.db.ServerDBService;
import de.hhhammer.dchat.db.UserDBService;
import de.hhhammer.dchat.migration.MigrationExecutor;
import de.hhhammer.dchat.openai.ChatGPTService;
import de.hhhammer.dchat.web.JavalinConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.http.HttpClient;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
private static final String DB_MIGRATION_PATH = "db/schema.sql";
private final DiscordBot discordBot;
private final WebAPI webAPI;
private final DBMigrator dbMigrator;
public Main(DiscordBot discordBot, WebAPI webAPI) {
public Main(DiscordBot discordBot, WebAPI webAPI, DBMigrator dbMigrator) {
this.discordBot = discordBot;
this.webAPI = webAPI;
this.dbMigrator = dbMigrator;
}
@ -39,13 +43,8 @@ public class Main {
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
System.exit(1);
}
String apiUsername = System.getenv("API_USERNAME");
String apiPassword = System.getenv("API_PASSWORD");
if (apiUsername == null || apiPassword == null) {
logger.error("Missing environment variables: API_USERNAME and/or API_PASSWORD");
System.exit(1);
}
String apiPortStr = System.getenv("PAI_PORT") != null ? System.getenv("API_PORT") : "8080";
String apiPortStr = System.getenv("API_PORT") != null ? System.getenv("API_PORT") : "8080";
int apiPort = Integer.parseInt(apiPortStr);
var chatGPTService = new ChatGPTService(openaiApiKey, HttpClient.newHttpClient());
@ -53,16 +52,20 @@ public class Main {
var userDBService = new UserDBService(postgresUrl, postgresUser, postgresPassword);
var discordBot = new DiscordBot(serverDBService, userDBService, chatGPTService, discordApiKey);
var javalinConfig = new JavalinConfig(apiPort, apiUsername, apiPassword);
var webAPI = new WebAPI(serverDBService, userDBService, javalinConfig);
new Main(discordBot, webAPI).run();
var webAPI = new WebAPI(serverDBService, userDBService, apiPort);
var dbMigrator = new DBMigrator(new MigrationExecutor(postgresUrl, postgresUser, postgresPassword), DB_MIGRATION_PATH);
new Main(discordBot, webAPI, dbMigrator).run();
}
public void run() {
logger.info("Starting services...");
try (var executor = Executors.newFixedThreadPool(2)) {
var migrationResult = executor.submit(dbMigrator);
migrationResult.get();
executor.submit(discordBot);
executor.submit(webAPI);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -2,13 +2,11 @@ package de.hhhammer.dchat;
import de.hhhammer.dchat.db.ServerDBService;
import de.hhhammer.dchat.db.UserDBService;
import de.hhhammer.dchat.web.JavalinConfig;
import de.hhhammer.dchat.web.server.AllowedCrudHandler;
import de.hhhammer.dchat.web.server.ConfigCrudHandler;
import de.hhhammer.dchat.web.user.AllowedUserCrudHandler;
import de.hhhammer.dchat.web.user.ConfigUserCrudHandler;
import io.javalin.Javalin;
import io.javalin.http.HttpStatus;
import io.javalin.http.staticfiles.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -19,12 +17,12 @@ public class WebAPI implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(WebAPI.class);
private final ServerDBService serverDBService;
private final UserDBService userDBService;
private final JavalinConfig javalinConfig;
private final int port;
public WebAPI(ServerDBService serverDBService, UserDBService userDBService, JavalinConfig javalinConfig) {
public WebAPI(ServerDBService serverDBService, UserDBService userDBService, int port) {
this.serverDBService = serverDBService;
this.userDBService = userDBService;
this.javalinConfig = javalinConfig;
this.port = port;
}
@Override
@ -34,17 +32,10 @@ public class WebAPI implements Runnable {
config.plugins.enableDevLogging();
config.http.prefer405over404 = true; // return 405 instead of 404 if path is mapped to different HTTP method
config.http.defaultContentType = "application/json";
config.accessManager((handler, context, set) -> {
var creds = context.basicAuthCredentials();
if (creds == null) {
context.status(HttpStatus.UNAUTHORIZED);
return;
}
if (!javalinConfig.username().equals(creds.getUsername()) || !javalinConfig.password().equals(creds.getPassword())) {
context.status(HttpStatus.UNAUTHORIZED);
return;
}
handler.handle(context);
config.staticFiles.add(staticFileConfig -> {
staticFileConfig.hostedPath = "/";
staticFileConfig.location = Location.EXTERNAL;
staticFileConfig.directory = "./ui/dist/";
});
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@ -55,22 +46,28 @@ public class WebAPI implements Runnable {
event.serverStopping(() -> logger.info("Stopping web service"));
event.serverStopped(() -> logger.info("Stopped web service"));
});
app.get("/", ctx -> ctx.result("""
{ "message": "Hello World"}
"""));
app.before(ctx -> {
ctx.header("Access-Control-Allow-Origin", "*");
ctx.header("Access-Control-Allow-Methods", "*");
});
app.after(ctx -> {
if (!ctx.path().startsWith("/api") && (ctx.status().equals(HttpStatus.NOT_FOUND) || ctx.status().equals(HttpStatus.METHOD_NOT_ALLOWED))) {
ctx.redirect("/index.html");
}
});
app.options("*", ctx -> ctx.status(HttpStatus.OK));
app.routes(() -> {
path("server", () -> {
crud("allowed/{id}", new AllowedCrudHandler(this.serverDBService));
crud("configs/{id}", new ConfigCrudHandler(this.serverDBService));
});
path("user", () -> {
crud("allowed/{id}", new AllowedUserCrudHandler(this.userDBService));
crud("configs/{id}", new ConfigUserCrudHandler(this.userDBService));
path("api", () -> {
path("servers", () -> {
crud("configs/{id}", new ConfigCrudHandler(this.serverDBService));
});
path("users", () -> {
crud("configs/{id}", new ConfigUserCrudHandler(this.userDBService));
});
});
});
app.start(this.javalinConfig.port());
app.start(this.port);
}
}

View file

@ -1,6 +1,5 @@
package de.hhhammer.dchat.db;
import de.hhhammer.dchat.db.models.server.AllowedServer;
import de.hhhammer.dchat.db.models.server.ServerConfig;
import de.hhhammer.dchat.db.models.server.ServerMessage;
import org.slf4j.Logger;
@ -25,116 +24,7 @@ public class ServerDBService {
this.password = password;
}
public boolean isAllowed(long serverId) {
var getAllowedServerSql = """
SELECT * FROM allowed_servers WHERE server_id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
pstmt.setLong(1, serverId);
ResultSet result = pstmt.executeQuery();
Iterable<AllowedServer> iterable = () -> new ResultSetIterator<>(result, new AllowedServer.AllowedServerResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).count() == 1;
} catch (SQLException e) {
logger.error("Searching for allowed server with id: " + serverId, e);
} catch (ResultSetIteratorException e) {
logger.error("Iterating over AllowedServer ResultSet for server with id: " + serverId, e);
return false;
}
return false;
}
public Optional<AllowedServer> getAllowedBy(long id) throws DBException {
var getAllowedServerSql = """
SELECT * FROM allowed_servers WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
pstmt.setLong(1, id);
ResultSet result = pstmt.executeQuery();
Iterable<AllowedServer> iterable = () -> new ResultSetIterator<>(result, new AllowedServer.AllowedServerResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
} catch (SQLException e) {
throw new DBException("Not found allowed server entry with id: " + id, e);
} catch (ResultSetIteratorException e) {
throw new DBException("Iterating over AllowedServer ResultSet searching for id: " + id, e);
}
}
public List<AllowedServer> getAllAllowed() throws DBException {
var getAllowedServerSql = """
SELECT * FROM allowed_servers
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
ResultSet result = pstmt.executeQuery();
Iterable<AllowedServer> iterable = () -> new ResultSetIterator<>(result, new AllowedServer.AllowedServerResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).toList();
} catch (SQLException e) {
throw new DBException("Not found allowed server entries", e);
} catch (ResultSetIteratorException e) {
throw new DBException("Iterating over AllowedServer ResultSet", e);
}
}
public void addAllowed(AllowedServer.NewAllowedServer newAllowedServer) throws DBException {
var insertAllowedServerSql = """
INSERT INTO allowed_servers (server_id, comment) VALUES (?, ?)
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setLong(1, newAllowedServer.serverId());
pstmt.setString(2, newAllowedServer.comment());
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No server inserted to allowed_servers with id: " + newAllowedServer.serverId());
}
} catch (SQLException e) {
throw new DBException("Allowing new server with id: " + newAllowedServer.serverId(), e);
}
}
public void updateAllowed(long id, AllowedServer.NewAllowedServer newAllowedServer) throws DBException {
var insertAllowedServerSql = """
UPDATE allowed_servers SET comment = ?, server_id = ? WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setString(1, newAllowedServer.comment());
pstmt.setLong(2, newAllowedServer.serverId());
pstmt.setLong(3, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No comment updated on server with id: " + newAllowedServer.serverId());
}
} catch (SQLException e) {
throw new DBException("Updating comment on allowed server with id: " + newAllowedServer.serverId(), e);
}
}
public void deleteAllowed(long id) throws DBException {
var insertAllowedServerSql = """
DELETE FROM allowed_servers WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setLong(1, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No server deleted with id: " + id);
}
} catch (SQLException e) {
throw new DBException("Deleting allowed server with id: " + id, e);
}
}
public Optional<ServerConfig> getConfig(long serverId) {
public Optional<ServerConfig> getConfig(String serverId) {
var getServerConfig = """
SELECT * FROM server_configs WHERE server_id = ?
""";
@ -142,7 +32,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, serverId);
pstmt.setString(1, serverId);
ResultSet resultSet = pstmt.executeQuery();
Iterable<ServerConfig> iterable = () -> new ResultSetIterator<>(resultSet, new ServerConfig.ServerConfigResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
@ -199,7 +89,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, newServerConfig.serverId());
pstmt.setString(1, newServerConfig.serverId());
pstmt.setString(2, newServerConfig.systemMessage());
pstmt.setInt(3, newServerConfig.rateLimit());
int affectedRows = pstmt.executeUpdate();
@ -221,7 +111,7 @@ public class ServerDBService {
) {
pstmt.setString(1, newServerConfig.systemMessage());
pstmt.setInt(2, newServerConfig.rateLimit());
pstmt.setLong(3, newServerConfig.serverId());
pstmt.setString(3, newServerConfig.serverId());
pstmt.setLong(4, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
@ -250,7 +140,7 @@ public class ServerDBService {
}
}
public int countMessagesInLastMinute(long serverId) {
public int countMessagesInLastMinute(String serverId) {
var getServerConfig = """
SELECT count(*) FROM server_messages WHERE server_id = ? AND time <= ? and time >= ?
""";
@ -258,7 +148,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, serverId);
pstmt.setString(1, serverId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
@ -282,7 +172,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, serverMessage.serverId());
pstmt.setString(1, serverMessage.serverId());
pstmt.setLong(2, serverMessage.userId());
pstmt.setInt(3, serverMessage.tokens());
int affectedRows = pstmt.executeUpdate();
@ -294,7 +184,7 @@ public class ServerDBService {
}
}
public long tokensOfLast30Days(long serverId) {
public long tokensOfLast30Days(String serverId) {
var countTokensOfLast30Days = """
SELECT sum(tokens) FROM server_messages WHERE server_id = ? AND time < ? AND time >= ?
""";
@ -302,7 +192,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
) {
pstmt.setLong(1, serverId);
pstmt.setString(1, serverId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(30, ChronoUnit.DAYS)));

View file

@ -1,6 +1,5 @@
package de.hhhammer.dchat.db;
import de.hhhammer.dchat.db.models.user.AllowedUser;
import de.hhhammer.dchat.db.models.user.UserConfig;
import de.hhhammer.dchat.db.models.user.UserMessage;
import org.slf4j.Logger;
@ -25,115 +24,7 @@ public class UserDBService {
this.password = password;
}
public boolean isAllowed(long userId) {
var getAllowedServerSql = """
SELECT * FROM allowed_users WHERE user_id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
pstmt.setLong(1, userId);
ResultSet result = pstmt.executeQuery();
Iterable<AllowedUser> iterable = () -> new ResultSetIterator<>(result, new AllowedUser.AllowedUserResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).count() == 1;
} catch (SQLException e) {
logger.error("Searching for allowed user with id: " + userId, e);
} catch (ResultSetIteratorException e) {
logger.error("Iterating over AllowedServer ResultSet for user with id: " + userId, e);
return false;
}
return false;
}
public Optional<AllowedUser> getAllowedBy(long id) throws DBException {
var getAllowedServerSql = """
SELECT * FROM allowed_users WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
pstmt.setLong(1, id);
ResultSet result = pstmt.executeQuery();
Iterable<AllowedUser> iterable = () -> new ResultSetIterator<>(result, new AllowedUser.AllowedUserResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
} catch (SQLException e) {
throw new DBException("Searching for allowed with id: " + id, e);
} catch (ResultSetIteratorException e) {
throw new DBException("Iterating over AllowedServer ResultSet with id: " + id, e);
}
}
public List<AllowedUser> getAllAllowed() throws DBException {
var getAllowedServerSql = """
SELECT * FROM allowed_users
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getAllowedServerSql)
) {
ResultSet result = pstmt.executeQuery();
Iterable<AllowedUser> iterable = () -> new ResultSetIterator<>(result, new AllowedUser.AllowedUserResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).toList();
} catch (SQLException e) {
throw new DBException("Searching all allowed users", e);
} catch (ResultSetIteratorException e) {
throw new DBException("Iterating over all AllowedServer ResultSet ", e);
}
}
public void addAllowed(AllowedUser.NewAllowedUser newAllowedUser) throws DBException {
var insertAllowedServerSql = """
INSERT INTO allowed_users (user_id, comment) VALUES (?, ?)
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setLong(1, newAllowedUser.userId());
pstmt.setString(2, newAllowedUser.comment());
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No server inserted to allowed_users with id: " + newAllowedUser.userId());
}
} catch (SQLException e) {
throw new DBException("Allowing new user with id: " + newAllowedUser.userId(), e);
}
}
public void updateAllowed(long id, AllowedUser.NewAllowedUser newAllowedUser) throws DBException {
var insertAllowedServerSql = """
UPDATE allowed_users SET comment = ? WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setString(1, newAllowedUser.comment());
pstmt.setLong(2, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No comment updated on user with id: " + newAllowedUser.userId());
}
} catch (SQLException e) {
throw new DBException("Updating comment on allowed user with id: " + id, e);
}
}
public void deleteAllowed(long id) throws DBException {
var insertAllowedServerSql = """
DELETE FROM allowed_users WHERE id = ?
""";
try (Connection con = DriverManager.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(insertAllowedServerSql)
) {
pstmt.setLong(1, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No user deleted with id: " + id);
}
} catch (SQLException e) {
throw new DBException("Deleting allowed user with id: " + id, e);
}
}
public Optional<UserConfig> getConfig(long userId) {
public Optional<UserConfig> getConfig(String userId) {
var getServerConfig = """
SELECT * FROM user_configs WHERE user_id = ?
""";
@ -141,7 +32,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, userId);
pstmt.setString(1, userId);
ResultSet resultSet = pstmt.executeQuery();
Iterable<UserConfig> iterable = () -> new ResultSetIterator<>(resultSet, new UserConfig.UserConfigResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
@ -199,7 +90,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, newUserConfig.userId());
pstmt.setString(1, newUserConfig.userId());
pstmt.setString(2, newUserConfig.systemMessage());
pstmt.setInt(3, newUserConfig.contextLength());
pstmt.setInt(4, newUserConfig.rateLimit());
@ -214,7 +105,7 @@ public class UserDBService {
public void updateConfig(long id, UserConfig.NewUserConfig newUserConfig) throws DBException {
var getServerConfig = """
UPDATE user_configs SET system_message = ?, context_length = ?, rate_limit = ? WHERE id = ?
UPDATE user_configs SET system_message = ?, context_length = ?, rate_limit = ?, user_id = ? WHERE id = ?
""";
try (Connection con = DriverManager
.getConnection(this.jdbcConnectionString, this.username, this.password);
@ -223,7 +114,8 @@ public class UserDBService {
pstmt.setString(1, newUserConfig.systemMessage());
pstmt.setInt(2, newUserConfig.rateLimit());
pstmt.setLong(3, newUserConfig.contextLength());
pstmt.setLong(4, id);
pstmt.setString(4, newUserConfig.userId());
pstmt.setLong(5, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No config update with id: " + id);
@ -251,7 +143,7 @@ public class UserDBService {
}
}
public int countMessagesInLastMinute(long userId) {
public int countMessagesInLastMinute(String userId) {
var getServerConfig = """
SELECT count(*) FROM user_messages WHERE user_id = ? AND time <= ? and time >= ?
""";
@ -259,7 +151,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, userId);
pstmt.setString(1, userId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
@ -283,7 +175,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setLong(1, newUserMessage.userId());
pstmt.setString(1, newUserMessage.userId());
pstmt.setString(2, newUserMessage.question());
pstmt.setString(3, newUserMessage.answer());
pstmt.setInt(4, newUserMessage.tokens());
@ -296,7 +188,7 @@ public class UserDBService {
}
}
public long tokensOfLast30Days(long userId) {
public long tokensOfLast30Days(String userId) {
var countTokensOfLast30Days = """
SELECT sum(tokens) FROM user_messages WHERE user_id = ? AND time < ? AND time >= ?
""";
@ -304,7 +196,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
) {
pstmt.setLong(1, userId);
pstmt.setString(1, userId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(30, ChronoUnit.DAYS)));

View file

@ -1,25 +0,0 @@
package de.hhhammer.dchat.db.models.server;
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 AllowedServer(long id, long serverId, Instant time, @Nullable String comment) {
public static class AllowedServerResultSetTransformer implements ResultSetTransformer<AllowedServer> {
@Override
public AllowedServer transform(ResultSet resultSet) throws SQLException {
var id = resultSet.getLong("id");
var serverId = resultSet.getLong("server_id");
var time = resultSet.getTimestamp("time").toInstant();
var comment = resultSet.getString("comment");
return new AllowedServer(id, serverId, time, comment);
}
}
public record NewAllowedServer(long serverId, @Nullable String comment) {
}
}

View file

@ -1,25 +1,29 @@
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, long serverId, String systemMessage, int rateLimit) {
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.getLong("server_id");
var serverId = resultSet.getString("server_id");
var systemMessage = resultSet.getString("system_message");
var rateLimit = resultSet.getInt("rate_limit");
return new ServerConfig(id, serverId, systemMessage, rateLimit);
var time = resultSet.getTimestamp("time").toInstant();
return new ServerConfig(id, serverId, systemMessage, rateLimit, time);
}
}
public record NewServerConfig(long serverId, String systemMessage, @Nullable int rateLimit) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record NewServerConfig(String serverId, String systemMessage, @Nullable int rateLimit) {
}
}

View file

@ -1,19 +1,20 @@
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, long serverId, long userId, int tokens, Instant time) {
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.getLong("server_id");
var serverId = resultSet.getString("server_id");
var userId = resultSet.getLong("user_id");
var tokens = resultSet.getInt("tokens");
var time = resultSet.getTimestamp("time").toInstant();
@ -21,6 +22,7 @@ public record ServerMessage(long id, long serverId, long userId, int tokens, Ins
}
}
public record NewServerMessage(long serverId, long userId, int tokens) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record NewServerMessage(String serverId, long userId, int tokens) {
}
}

View file

@ -1,25 +0,0 @@
package de.hhhammer.dchat.db.models.user;
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 AllowedUser(long id, long userId, Instant time, @Nullable String comment) {
public static class AllowedUserResultSetTransformer implements ResultSetTransformer<AllowedUser> {
@Override
public AllowedUser transform(ResultSet resultSet) throws SQLException {
var id = resultSet.getLong("id");
var userId = resultSet.getLong("user_id");
var time = resultSet.getTimestamp("time").toInstant();
var comment = resultSet.getString("comment");
return new AllowedUser(id, userId, time, comment);
}
}
public record NewAllowedUser(long userId, @Nullable String comment) {
}
}

View file

@ -1,27 +1,31 @@
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, long userId, String systemMessage, int contextLength, int rateLimit) {
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.getLong("user_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");
return new UserConfig(id, userId, systemMessage, contextLength, rateLimit);
var time = resultSet.getTimestamp("time").toInstant();
return new UserConfig(id, userId, systemMessage, contextLength, rateLimit, time);
}
}
public record NewUserConfig(long userId, String systemMessage, @Nullable int contextLength,
@JsonIgnoreProperties(ignoreUnknown = true)
public record NewUserConfig(String userId, String systemMessage, @Nullable int contextLength,
@Nullable int rateLimit) {
}
}

View file

@ -1,19 +1,20 @@
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, long userId, String question, String answer, int tokens, Instant time) {
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.getLong("user_id");
var userId = resultSet.getString("user_id");
var question = resultSet.getString("question");
var answer = resultSet.getString("answer");
var tokens = resultSet.getInt("tokens");
@ -22,6 +23,7 @@ public record UserMessage(long id, long userId, String question, String answer,
}
}
public record NewUserMessage(long userId, String question, String answer, int tokens) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record NewUserMessage(String userId, String question, String answer, int tokens) {
}
}

View file

@ -24,7 +24,7 @@ public class MessageCreateHandler implements MessageCreateListener {
return;
}
if (this.messageHandler.exceedsRate(event)) {
event.getChannel().sendMessage("Rate limit hit - cooling down now...");
event.getChannel().sendMessage("Rate limit hit - cooling down...");
return;
}
this.messageHandler.handle(event);

View file

@ -29,6 +29,8 @@ public class ServerMessageHandler implements MessageHandler {
@Override
public void handle(MessageCreateEvent event) {
String content = extractContent(event);
var serverId = event.getServer().get().getId();
var systemMessage = this.serverDBService.getConfig(String.valueOf(serverId)).get().systemMessage();
var request = event.getMessage().getType() == MessageType.REPLY ?
new ChatGPTRequestBuilder()
.contextRequest(event.getMessage()
@ -37,8 +39,8 @@ public class ServerMessageHandler implements MessageHandler {
.flatMap(m -> m)
.map(Message::getReadableContent)
.stream().toList(),
content) :
new ChatGPTRequestBuilder().simpleRequest(content);
content, systemMessage) :
new ChatGPTRequestBuilder().simpleRequest(content, systemMessage);
try {
var response = this.chatGPTService.submit(request);
if (response.choices().size() < 1) {
@ -56,21 +58,22 @@ public class ServerMessageHandler implements MessageHandler {
@Override
public boolean isAllowed(MessageCreateEvent event) {
if (event.getServer().isPresent()) {
var serverId = event.getServer().get().getId();
var allowed = this.serverDBService.isAllowed(serverId);
if (!allowed) {
logger.debug("Not allowed with id: " + serverId);
}
return allowed;
if (event.getServer().isEmpty()) {
return false;
}
// only support server messages
return false;
var serverId = event.getServer().get().getId();
var 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(MessageCreateEvent event) {
var serverId = event.getServer().get().getId();
var serverId = String.valueOf(event.getServer().get().getId());
var config = this.serverDBService.getConfig(serverId);
if (config.isEmpty()) {
logger.error("Missing configuration for server with id: " + serverId);
@ -91,7 +94,7 @@ public class ServerMessageHandler implements MessageHandler {
var serverId = event.getServer().map(DiscordEntity::getId).get();
var userId = event.getMessageAuthor().getId();
var serverMessage = new ServerMessage.NewServerMessage(serverId, userId, tokens);
var serverMessage = new ServerMessage.NewServerMessage(String.valueOf(serverId), userId, tokens);
this.serverDBService.addMessage(serverMessage);
}

View file

@ -24,7 +24,9 @@ public class UserMessageHandler implements MessageHandler {
@Override
public void handle(MessageCreateEvent event) {
String content = event.getReadableMessageContent();
var request = new ChatGPTRequestBuilder().simpleRequest(content);
var userId = event.getMessageAuthor().getId();
var systemMessage = this.userDBService.getConfig(String.valueOf(userId)).get().systemMessage();
var request = new ChatGPTRequestBuilder().simpleRequest(content, systemMessage);
try {
var response = this.chatGPTService.submit(request);
if (response.choices().size() < 1) {
@ -47,16 +49,17 @@ public class UserMessageHandler implements MessageHandler {
}
var userId = event.getMessageAuthor().getId();
var allowed = this.userDBService.isAllowed(userId);
if (!allowed) {
var config = this.userDBService.getConfig(String.valueOf(userId));
if (config.isEmpty()) {
logger.debug("Not allowed with id: " + userId);
return false;
}
return allowed;
return true;
}
@Override
public boolean exceedsRate(MessageCreateEvent event) {
var userId = event.getMessageAuthor().getId();
var userId = String.valueOf(event.getMessageAuthor().getId());
var config = this.userDBService.getConfig(userId);
if (config.isEmpty()) {
logger.error("Missing configuration for userId with id: " + userId);
@ -76,7 +79,7 @@ public class UserMessageHandler implements MessageHandler {
private void logUserMessage(MessageCreateEvent event, String question, String answer, int tokens) {
var userId = event.getMessageAuthor().getId();
var userMessage = new UserMessage.NewUserMessage(userId, question, answer, tokens);
var userMessage = new UserMessage.NewUserMessage(String.valueOf(userId), question, answer, tokens);
this.userDBService.addMessage(userMessage);
}
}

View file

@ -0,0 +1,7 @@
package de.hhhammer.dchat.migration;
public class DBMigrationException extends Exception {
public DBMigrationException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,38 @@
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 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(String jdbcConnectionString, String username, String password) {
this.jdbcConnectionString = jdbcConnectionString;
this.username = username;
this.password = password;
}
public void migrate(InputStream input) throws DBMigrationException {
try (Connection con = DriverManager
.getConnection(this.jdbcConnectionString, this.username, this.password);
Statement stmp = con.createStatement();
) {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
stmp.execute(content);
} catch (SQLException | IOException e) {
throw new DBMigrationException(e);
}
}
}

View file

@ -7,14 +7,11 @@ import java.util.List;
public class ChatGPTRequestBuilder {
private final String model = "gpt-3.5-turbo";
private final String systemMessage = """
You are Jarvis, a helpful and friendly AI. People interact with you over Discord, a chatting platform. Your default language to answer is German. You format your responses in markdown and your answers don´t need to be formal.
""";
public ChatGPTRequestBuilder() {
}
public ChatGPTRequest simpleRequest(String content) {
public ChatGPTRequest simpleRequest(String content, String systemMessage) {
return new ChatGPTRequest(
model,
List.of(new ChatGPTRequest.Message("system", systemMessage), new ChatGPTRequest.Message("user", content)),
@ -22,7 +19,7 @@ public class ChatGPTRequestBuilder {
);
}
public ChatGPTRequest contextRequest(List<String> contextMessages, String message) {
public ChatGPTRequest contextRequest(List<String> contextMessages, String message, String systemMessage) {
List<ChatGPTRequest.Message> messages = new ArrayList<>();
messages.add(new ChatGPTRequest.Message("system", systemMessage));
var context = contextMessages.stream()

View file

@ -1,4 +0,0 @@
package de.hhhammer.dchat.web;
public record JavalinConfig(int port, String username, String password) {
}

View file

@ -1,84 +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.AllowedServer;
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;
public class AllowedCrudHandler implements CrudHandler {
private static final Logger logger = LoggerFactory.getLogger(AllowedCrudHandler.class);
private final ServerDBService serverDBService;
public AllowedCrudHandler(ServerDBService serverDBService) {
this.serverDBService = serverDBService;
}
@Override
public void create(@NotNull Context context) {
var body = context.bodyAsClass(AllowedServer.NewAllowedServer.class);
try {
this.serverDBService.addAllowed(body);
} catch (DBException e) {
logger.error("Adding new allowed server", e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
context.status(HttpStatus.CREATED);
}
@Override
public void delete(@NotNull Context context, @NotNull String s) {
try {
this.serverDBService.deleteAllowed(Long.parseLong(s));
context.status(HttpStatus.NO_CONTENT);
} catch (DBException e) {
logger.error("Deleting server with id: " + s, e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
public void getAll(@NotNull Context context) {
try {
var allowedServers = this.serverDBService.getAllAllowed();
context.json(allowedServers);
} catch (DBException e) {
logger.error("Getting all allowed servers", e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
public void getOne(@NotNull Context context, @NotNull String s) {
var id = Long.parseLong(s);
try {
var server = this.serverDBService.getAllowedBy(id);
if (server.isEmpty()) {
context.status(HttpStatus.NOT_FOUND);
return;
}
context.json(server.get());
} catch (DBException e) {
logger.error("Searching with id: " + s, e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
public void update(@NotNull Context context, @NotNull String idString) {
var newAllowedServer = context.bodyAsClass(AllowedServer.NewAllowedServer.class);
var id = Long.parseLong(idString);
try {
this.serverDBService.updateAllowed(id, newAllowedServer);
} catch (DBException e) {
logger.error("Updating allowed server with id: " + idString, e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -1,84 +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.AllowedUser;
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;
public class AllowedUserCrudHandler implements CrudHandler {
private static final Logger logger = LoggerFactory.getLogger(AllowedUserCrudHandler.class);
private final UserDBService userDBService;
public AllowedUserCrudHandler(UserDBService userDBService) {
this.userDBService = userDBService;
}
@Override
public void create(@NotNull Context context) {
var body = context.bodyAsClass(AllowedUser.NewAllowedUser.class);
try {
this.userDBService.addAllowed(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 Context context, @NotNull String s) {
try {
this.userDBService.deleteAllowed(Long.parseLong(s));
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 Context context) {
try {
var allowedServers = this.userDBService.getAllAllowed();
context.json(allowedServers);
} catch (DBException e) {
logger.error("Getting all server configs", e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
public void getOne(@NotNull Context context, @NotNull String s) {
var id = Long.parseLong(s);
try {
var server = this.userDBService.getAllowedBy(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) {
var body = context.bodyAsClass(AllowedUser.NewAllowedUser.class);
var id = Long.parseLong(idString);
try {
this.userDBService.updateAllowed(id, body);
} catch (DBException e) {
logger.error("Updating allowed server with id: " + idString, e);
context.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -86,4 +86,39 @@ BEGIN;
ALTER TABLE user_configs
ADD COLUMN IF NOT EXISTS context_length INT NOT NULL DEFAULT 5;
COMMIT
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;

12
ui/.containerignore Normal file
View file

@ -0,0 +1,12 @@
*
!Caddyfile
!package*.json
!postcss.config.js
!tailwind.config.js
!env.d..ts
!vite.config.ts
!tsconfig.json
!tsconfig.node.json
!index.html
!public/
!src/

28
ui/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# 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/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
ui/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

11
ui/Caddyfile Normal file
View file

@ -0,0 +1,11 @@
{$SITE_ADDRESS} {
basicauth * {
{$AUTH_PASSWORD} {$AUTH_PASSWORD}
}
encode zstd gzip
root * /app
try_files {path} /index.html
file_server
}

16
ui/Containerfile Normal file
View file

@ -0,0 +1,16 @@
FROM docker.io/node:18-slim AS build
WORKDIR /app
COPY ./package* .
RUN npm ci
COPY . .
RUN npm run build-only
FROM docker.io/caddy:2-alpine
WORKDIR /app
COPY ./Caddyfile /
COPY --from=build /app/dist /app
EXPOSE 80
CMD ["caddy", "run", "--config", "/Caddyfile"]

40
ui/README.md Normal file
View file

@ -0,0 +1,40 @@
# 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
```

31
ui/build.sh Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
REPO="git.hhhammer.de/hamburghammer/dchat-ui"
AUTHORS="Augusto Dwenger J. <dwenger@posteo.de>"
URL="https://${REPO}"
VENDOR="hamburghammer"
TITLE="dchat-ui"
DESCRIPTION="The Web-UI for dchat a Discord bot to chat with ChatGPT from OpenAI"
CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
REVISION=$(git rev-parse HEAD)
IMAGE_LATEST="${REPO}:latest"
# shellcheck disable=SC2086
podman build \
$DOCKER_BUILD_ARGS \
-t $IMAGE_LATEST \
--label "org.opencontainers.image.revision=${REVISION}" \
--label "org.opencontainers.image.version=${TAG}" \
--label "org.opencontainers.image.authors=${AUTHORS}" \
--label "org.opencontainers.image.created=${CREATED}" \
--label "org.opencontainers.image.source=${URL}" \
--label "org.opencontainers.image.vendor=${VENDOR}" \
--label "org.opencontainers.image.title=${TITLE}" \
--label "org.opencontainers.image.description=${DESCRIPTION}" \
. || exit $?
if [ "$PUSH_LATEST" == "1" ]; then
podman push "${IMAGE_LATEST}"
fi

1
ui/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
ui/index.html Normal file
View file

@ -0,0 +1,13 @@
<!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>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

5046
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
ui/package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "dchat-ui",
"version": "0.0.0",
"private": true,
"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"
}
}

6
ui/postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

37
ui/src/App.vue Normal file
View file

@ -0,0 +1,37 @@
<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">MIT 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>

3
ui/src/assets/base.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

1
ui/src/assets/main.css Normal file
View file

@ -0,0 +1 @@
@import './base.css';

View file

@ -0,0 +1,36 @@
<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>

View file

@ -0,0 +1,35 @@
<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>

View file

@ -0,0 +1,4 @@
<script setup lang="ts">
const {id} = defineProps<{id: any}>()
debugger
</script>

View file

@ -0,0 +1,34 @@
<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>

View file

@ -0,0 +1,17 @@
<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>

View file

@ -0,0 +1,41 @@
<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>

View file

@ -0,0 +1,35 @@
<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>

View file

@ -0,0 +1,17 @@
<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>

View file

@ -0,0 +1,44 @@
<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>

11
ui/src/main.ts Normal file
View file

@ -0,0 +1,11 @@
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')

8
ui/src/models/server.ts Normal file
View file

@ -0,0 +1,8 @@
export type ServerConfig = {
id: number,
serverId: string,
systemMessage: string,
rateLimit: number,
time: string
}

8
ui/src/models/user.ts Normal file
View file

@ -0,0 +1,8 @@
export type UserConfig = {
id: number,
userId: string,
systemMessage: string,
contextLength: number,
rateLimit: number,
time: string
}

39
ui/src/router/index.ts Normal file
View file

@ -0,0 +1,39 @@
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

View file

@ -0,0 +1,39 @@
import type { ServerConfig } from "@/models/server";
const configUrl = "http://localhost:7070/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}`)
}

View file

@ -0,0 +1,40 @@
import type { UserConfig } from '@/models/user'
const configUrl = "http://localhost:7070/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}`)
}

13
ui/src/views/HomeView.vue Normal file
View file

@ -0,0 +1,13 @@
<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>

View file

@ -0,0 +1,5 @@
<script setup lang="ts">
</script>
<template>
</template>

View file

@ -0,0 +1,12 @@
<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>

View file

@ -0,0 +1,27 @@
<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>

View file

@ -0,0 +1,12 @@
<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>

View file

@ -0,0 +1,27 @@
<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>

9
ui/tailwind.config.js Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [require("daisyui")],
}

16
ui/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

8
ui/tsconfig.node.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

14
ui/vite.config.ts Normal file
View file

@ -0,0 +1,14 @@
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))
}
}
})