Compare commits

..

No commits in common. "a398641172fae4755781cc7d46223c1c36dfa5e6" and "8b2422dd1b5ae461dd8ebed7f5936fc6c7f96942" have entirely different histories.

68 changed files with 553 additions and 6084 deletions

View file

@ -1,13 +1,3 @@
*
!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 java application
# Stage 1: Build the application
FROM docker.io/maven:3.9-eclipse-temurin-19 AS maven
WORKDIR /app
COPY pom.xml .
@ -7,19 +7,9 @@ RUN mvn package
COPY src/ /app/src/
RUN mvn package
# 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
# Stage 2: 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,6 +22,12 @@ 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,6 +14,8 @@ 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.5.0</version>
<version>5.4.2</version>
</dependency>
<!-- logging -->
<dependency>

View file

@ -86,39 +86,4 @@ 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;
COMMIT

View file

@ -1,36 +0,0 @@
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(String.valueOf(server.get().getId()));
var tokens = this.serverDBService.tokensOfLast30Days(server.get().getId());
interactionOriginalResponseUpdater.setContent("" + tokens).update();
});
return;

View file

@ -2,26 +2,22 @@ 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, DBMigrator dbMigrator) {
public Main(DiscordBot discordBot, WebAPI webAPI) {
this.discordBot = discordBot;
this.webAPI = webAPI;
this.dbMigrator = dbMigrator;
}
@ -43,8 +39,13 @@ public class Main {
logger.error("Missing environment variables: POSTGRES_USER and/or POSTGRES_PASSWORD and/or POSTGRES_URL");
System.exit(1);
}
String apiPortStr = System.getenv("API_PORT") != null ? System.getenv("API_PORT") : "8080";
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";
int apiPort = Integer.parseInt(apiPortStr);
var chatGPTService = new ChatGPTService(openaiApiKey, HttpClient.newHttpClient());
@ -52,20 +53,16 @@ public class Main {
var userDBService = new UserDBService(postgresUrl, postgresUser, postgresPassword);
var discordBot = new DiscordBot(serverDBService, userDBService, chatGPTService, discordApiKey);
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();
var javalinConfig = new JavalinConfig(apiPort, apiUsername, apiPassword);
var webAPI = new WebAPI(serverDBService, userDBService, javalinConfig);
new Main(discordBot, webAPI).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,11 +2,13 @@ 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;
@ -17,12 +19,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 int port;
private final JavalinConfig javalinConfig;
public WebAPI(ServerDBService serverDBService, UserDBService userDBService, int port) {
public WebAPI(ServerDBService serverDBService, UserDBService userDBService, JavalinConfig javalinConfig) {
this.serverDBService = serverDBService;
this.userDBService = userDBService;
this.port = port;
this.javalinConfig = javalinConfig;
}
@Override
@ -32,10 +34,17 @@ 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.staticFiles.add(staticFileConfig -> {
staticFileConfig.hostedPath = "/";
staticFileConfig.location = Location.EXTERNAL;
staticFileConfig.directory = "./ui/dist/";
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);
});
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@ -46,28 +55,22 @@ public class WebAPI implements Runnable {
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.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.get("/", ctx -> ctx.result("""
{ "message": "Hello World"}
"""));
app.routes(() -> {
path("api", () -> {
path("servers", () -> {
crud("configs/{id}", new ConfigCrudHandler(this.serverDBService));
});
path("users", () -> {
crud("configs/{id}", new ConfigUserCrudHandler(this.userDBService));
});
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));
});
});
app.start(this.port);
app.start(this.javalinConfig.port());
}
}

View file

@ -1,5 +1,6 @@
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;
@ -24,7 +25,116 @@ public class ServerDBService {
this.password = password;
}
public Optional<ServerConfig> getConfig(String serverId) {
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) {
var getServerConfig = """
SELECT * FROM server_configs WHERE server_id = ?
""";
@ -32,7 +142,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, serverId);
pstmt.setLong(1, serverId);
ResultSet resultSet = pstmt.executeQuery();
Iterable<ServerConfig> iterable = () -> new ResultSetIterator<>(resultSet, new ServerConfig.ServerConfigResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
@ -89,7 +199,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, newServerConfig.serverId());
pstmt.setLong(1, newServerConfig.serverId());
pstmt.setString(2, newServerConfig.systemMessage());
pstmt.setInt(3, newServerConfig.rateLimit());
int affectedRows = pstmt.executeUpdate();
@ -111,7 +221,7 @@ public class ServerDBService {
) {
pstmt.setString(1, newServerConfig.systemMessage());
pstmt.setInt(2, newServerConfig.rateLimit());
pstmt.setString(3, newServerConfig.serverId());
pstmt.setLong(3, newServerConfig.serverId());
pstmt.setLong(4, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
@ -140,7 +250,7 @@ public class ServerDBService {
}
}
public int countMessagesInLastMinute(String serverId) {
public int countMessagesInLastMinute(long serverId) {
var getServerConfig = """
SELECT count(*) FROM server_messages WHERE server_id = ? AND time <= ? and time >= ?
""";
@ -148,7 +258,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, serverId);
pstmt.setLong(1, serverId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
@ -172,7 +282,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, serverMessage.serverId());
pstmt.setLong(1, serverMessage.serverId());
pstmt.setLong(2, serverMessage.userId());
pstmt.setInt(3, serverMessage.tokens());
int affectedRows = pstmt.executeUpdate();
@ -184,7 +294,7 @@ public class ServerDBService {
}
}
public long tokensOfLast30Days(String serverId) {
public long tokensOfLast30Days(long serverId) {
var countTokensOfLast30Days = """
SELECT sum(tokens) FROM server_messages WHERE server_id = ? AND time < ? AND time >= ?
""";
@ -192,7 +302,7 @@ public class ServerDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
) {
pstmt.setString(1, serverId);
pstmt.setLong(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,5 +1,6 @@
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;
@ -24,7 +25,115 @@ public class UserDBService {
this.password = password;
}
public Optional<UserConfig> getConfig(String userId) {
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) {
var getServerConfig = """
SELECT * FROM user_configs WHERE user_id = ?
""";
@ -32,7 +141,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, userId);
pstmt.setLong(1, userId);
ResultSet resultSet = pstmt.executeQuery();
Iterable<UserConfig> iterable = () -> new ResultSetIterator<>(resultSet, new UserConfig.UserConfigResultSetTransformer());
return StreamSupport.stream(iterable.spliterator(), false).findFirst();
@ -90,7 +199,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, newUserConfig.userId());
pstmt.setLong(1, newUserConfig.userId());
pstmt.setString(2, newUserConfig.systemMessage());
pstmt.setInt(3, newUserConfig.contextLength());
pstmt.setInt(4, newUserConfig.rateLimit());
@ -105,7 +214,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 = ?, user_id = ? WHERE id = ?
UPDATE user_configs SET system_message = ?, context_length = ?, rate_limit = ? WHERE id = ?
""";
try (Connection con = DriverManager
.getConnection(this.jdbcConnectionString, this.username, this.password);
@ -114,8 +223,7 @@ public class UserDBService {
pstmt.setString(1, newUserConfig.systemMessage());
pstmt.setInt(2, newUserConfig.rateLimit());
pstmt.setLong(3, newUserConfig.contextLength());
pstmt.setString(4, newUserConfig.userId());
pstmt.setLong(5, id);
pstmt.setLong(4, id);
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
logger.error("No config update with id: " + id);
@ -143,7 +251,7 @@ public class UserDBService {
}
}
public int countMessagesInLastMinute(String userId) {
public int countMessagesInLastMinute(long userId) {
var getServerConfig = """
SELECT count(*) FROM user_messages WHERE user_id = ? AND time <= ? and time >= ?
""";
@ -151,7 +259,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, userId);
pstmt.setLong(1, userId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(1, ChronoUnit.MINUTES)));
@ -175,7 +283,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(getServerConfig)
) {
pstmt.setString(1, newUserMessage.userId());
pstmt.setLong(1, newUserMessage.userId());
pstmt.setString(2, newUserMessage.question());
pstmt.setString(3, newUserMessage.answer());
pstmt.setInt(4, newUserMessage.tokens());
@ -188,7 +296,7 @@ public class UserDBService {
}
}
public long tokensOfLast30Days(String userId) {
public long tokensOfLast30Days(long userId) {
var countTokensOfLast30Days = """
SELECT sum(tokens) FROM user_messages WHERE user_id = ? AND time < ? AND time >= ?
""";
@ -196,7 +304,7 @@ public class UserDBService {
.getConnection(this.jdbcConnectionString, this.username, this.password);
PreparedStatement pstmt = con.prepareStatement(countTokensOfLast30Days)
) {
pstmt.setString(1, userId);
pstmt.setLong(1, userId);
var now = Instant.now();
pstmt.setTimestamp(2, Timestamp.from(now));
pstmt.setTimestamp(3, Timestamp.from(now.minus(30, ChronoUnit.DAYS)));

View file

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

View file

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

View file

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

View file

@ -1,20 +1,19 @@
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 record UserMessage(long id, long 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 userId = resultSet.getLong("user_id");
var question = resultSet.getString("question");
var answer = resultSet.getString("answer");
var tokens = resultSet.getInt("tokens");
@ -23,7 +22,6 @@ public record UserMessage(long id, String userId, String question, String answer
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record NewUserMessage(String userId, String question, String answer, int tokens) {
public record NewUserMessage(long 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...");
event.getChannel().sendMessage("Rate limit hit - cooling down now...");
return;
}
this.messageHandler.handle(event);

View file

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

View file

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

View file

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

View file

@ -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 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,11 +7,14 @@ 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, String systemMessage) {
public ChatGPTRequest simpleRequest(String content) {
return new ChatGPTRequest(
model,
List.of(new ChatGPTRequest.Message("system", systemMessage), new ChatGPTRequest.Message("user", content)),
@ -19,7 +22,7 @@ public class ChatGPTRequestBuilder {
);
}
public ChatGPTRequest contextRequest(List<String> contextMessages, String message, String systemMessage) {
public ChatGPTRequest contextRequest(List<String> contextMessages, String message) {
List<ChatGPTRequest.Message> messages = new ArrayList<>();
messages.add(new ChatGPTRequest.Message("system", systemMessage));
var context = contextMessages.stream()

View file

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

View file

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

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

@ -1,12 +0,0 @@
*
!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
View file

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

View file

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

View file

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

View file

@ -1,16 +0,0 @@
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"]

View file

@ -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
```

View file

@ -1,31 +0,0 @@
#!/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
View file

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

View file

@ -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>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

File diff suppressed because it is too large Load diff

View file

@ -1,29 +0,0 @@
{
"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"
}
}

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -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">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>

View file

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

View file

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

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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')

View file

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

View file

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

View file

@ -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

View file

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

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

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -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"
}
]
}

View file

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

View file

@ -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))
}
}
})