diff --git a/.gitignore b/.gitignore index 4f66365..c928582 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea logs build +*.db \ No newline at end of file diff --git a/app/src/main/java/xyz/vallat/louis/App.java b/app/src/main/java/xyz/vallat/louis/App.java index 7a22bea..45e77b1 100644 --- a/app/src/main/java/xyz/vallat/louis/App.java +++ b/app/src/main/java/xyz/vallat/louis/App.java @@ -2,6 +2,7 @@ package xyz.vallat.louis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import xyz.vallat.louis.managers.database.DBManager; import xyz.vallat.louis.managers.discord.DiscordManager; public class App { @@ -14,6 +15,8 @@ public class App { public static void main(String[] args) { logger.info("Starting {} ver. {}.", NAME, VERSION); + DBManager.testConnection(); + DBManager.initializeDatabase(); DiscordManager.login(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { diff --git a/app/src/main/java/xyz/vallat/louis/codes/ExitCodes.java b/app/src/main/java/xyz/vallat/louis/codes/ExitCodes.java new file mode 100644 index 0000000..2fd40ea --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/codes/ExitCodes.java @@ -0,0 +1,17 @@ +package xyz.vallat.louis.codes; + +public enum ExitCodes { + CANNOT_CONNECT_TO_DB(1), + SQL_FATAL_ERROR(2) + ; + + private final int code; + + ExitCodes(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/app/src/main/java/xyz/vallat/louis/commands/Inscription.java b/app/src/main/java/xyz/vallat/louis/commands/Inscription.java new file mode 100644 index 0000000..c913c9c --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/commands/Inscription.java @@ -0,0 +1,46 @@ +package xyz.vallat.louis.commands; + +import biweekly.component.VEvent; +import discord4j.common.util.Snowflake; +import discord4j.core.event.domain.message.MessageCreateEvent; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import xyz.vallat.louis.managers.calendar.CalendarManager; +import xyz.vallat.louis.managers.database.EventManager; +import xyz.vallat.louis.managers.database.dao.Student; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Inscription extends Command { + + private static final Logger logger = LoggerFactory.getLogger(Inscription.class.getCanonicalName()); + + public Inscription(String name) { + super(name, "S'inscrire aux rappels.", name + " "); + } + + @Override + public Mono execute(MessageCreateEvent event) { + String[] args = event.getMessage().getContent().split(" "); + if (args.length < 2 || !StringUtils.isNumeric(args[1])) + return event.getMessage().getChannel().flatMap(channel -> channel.createMessage("Error: " + getUsage())).then(); + return event.getMessage().getChannel().flatMap(messageChannel -> { + Snowflake snowflake = event.getMessage().getAuthor().isEmpty() ? null : + event.getMessage().getAuthor().get().getId(); + Student student = new Student(snowflake, Integer.parseInt(args[1])); + List events = CalendarManager.getEventsFromResource(Integer.parseInt(args[1])); + Map> studentListMap = new HashMap<>(); + studentListMap.put(student, events); + int importedEvents = EventManager.importEvents(studentListMap); + if (importedEvents == 0) return messageChannel.createMessage( + "On dirait qu'il y a eu une erreur lors de l'importation. Es-tu sûr que c'est bien ton identifiant ADE ?"); + else return messageChannel.createMessage("Hey " + + (student.getSnowflake() == null ? "" : "<@!" + student.getSnowflake().asString() + "> ") + + " tout est bon ! Je surveille maintenant tes " + importedEvents + " prochains évènements !"); + }).then().onErrorResume(throwable -> fatalError(event, throwable)); + } +} diff --git a/app/src/main/java/xyz/vallat/louis/commands/Subscribe.java b/app/src/main/java/xyz/vallat/louis/commands/Subscribe.java deleted file mode 100644 index 34e2f43..0000000 --- a/app/src/main/java/xyz/vallat/louis/commands/Subscribe.java +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.vallat.louis.commands; - -import discord4j.core.event.domain.message.MessageCreateEvent; -import reactor.core.publisher.Mono; - -public class Subscribe extends Command { - - public Subscribe(String name) { - super(name, "S'inscrire aux rappels.", name + " "); - } - - @Override - public Mono execute(MessageCreateEvent event) { - return event.getMessage().getChannel().flatMap(messageChannel -> messageChannel.createMessage("Ponnnng!")).then(); - } -} diff --git a/app/src/main/java/xyz/vallat/louis/environment/EnvironmentVariables.java b/app/src/main/java/xyz/vallat/louis/environment/EnvironmentVariables.java index fa21661..9de87f4 100644 --- a/app/src/main/java/xyz/vallat/louis/environment/EnvironmentVariables.java +++ b/app/src/main/java/xyz/vallat/louis/environment/EnvironmentVariables.java @@ -2,7 +2,13 @@ package xyz.vallat.louis.environment; public enum EnvironmentVariables { DISCORD_TOKEN("DISCORD_TOKEN"), - MOODLE_PRESENCE_LINK("MOODLE_PRESENCE_LINK") + MOODLE_PRESENCE_LINK("MOODLE_PRESENCE_LINK"), + DB_USERNAME("DB_USERNAME"), + DB_PASSWORD("DB_PASSWORD"), + DB_NAME("DB_NAME"), + DB_HOST("DB_HOST"), + DB_PORT("DB_PORT"), + ADE_URL("ADE_URL") ; diff --git a/app/src/main/java/xyz/vallat/louis/managers/calendar/CalendarManager.java b/app/src/main/java/xyz/vallat/louis/managers/calendar/CalendarManager.java new file mode 100644 index 0000000..31af988 --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/managers/calendar/CalendarManager.java @@ -0,0 +1,52 @@ +package xyz.vallat.louis.managers.calendar; + +import biweekly.Biweekly; +import biweekly.component.VEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.vallat.louis.environment.EnvironmentVariables; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; + +public final class CalendarManager { + + private static final Logger logger = LoggerFactory.getLogger(CalendarManager.class.getCanonicalName()); + public static final Calendar TZ_UTC = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + private CalendarManager() { + } + + public static List getEventsFromResource(int resource) { + logger.debug("Getting agenda from student with id '{}'.", resource); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(System.getenv( + EnvironmentVariables.ADE_URL.getValue()).replace("{}", String.valueOf(resource)))).build(); + try { + HttpResponse response = getHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 200 && response.statusCode() < 400) { + return Biweekly.parse(response.body()).first().getEvents(); + } else logger.error("Got status code {}.", response.statusCode()); + } catch (IOException e) { + logger.error("IOException:", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return new ArrayList<>(); + } + + private static HttpClient getHttpClient() { + return HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + } + +} diff --git a/app/src/main/java/xyz/vallat/louis/managers/database/DBManager.java b/app/src/main/java/xyz/vallat/louis/managers/database/DBManager.java new file mode 100644 index 0000000..97ba220 --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/managers/database/DBManager.java @@ -0,0 +1,66 @@ +package xyz.vallat.louis.managers.database; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.vallat.louis.codes.ExitCodes; +import xyz.vallat.louis.environment.EnvironmentVariables; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public final class DBManager { + + private static final Logger logger = LoggerFactory.getLogger(DBManager.class.getCanonicalName()); + private static final String CONNECTION_PREFIX = "postgresql"; + private static final String USERNAME = System.getenv(EnvironmentVariables.DB_USERNAME.getValue()); + private static final String PASSWORD = System.getenv(EnvironmentVariables.DB_PASSWORD.getValue()); + private static final String DATABASE = System.getenv(EnvironmentVariables.DB_NAME.getValue()); + private static final int PORT = System.getenv(EnvironmentVariables.DB_PORT.getValue()) == null ? 5432 : + Integer.parseInt(System.getenv(EnvironmentVariables.DB_PORT.getValue())); + private static final String HOST = System.getenv(EnvironmentVariables.DB_HOST.getValue()); + + private DBManager() {} + + public static Connection getConnection() { + try { + return DriverManager.getConnection("jdbc:" + CONNECTION_PREFIX + "://" + HOST + ":" + PORT + "/" + + DATABASE, USERNAME, PASSWORD); + } catch (SQLException e) { + logger.error("Could not connect to database. Reason: {}.", e.getMessage()); + System.exit(ExitCodes.CANNOT_CONNECT_TO_DB.getCode()); + } + return null; + } + + public static void testConnection() { + logger.debug("Testing database connection."); + getConnection(); + logger.info("Database connection OK."); + } + + public static void initializeDatabase() { + logger.info("Initializing database if needed."); + try (Connection connection = getConnection()) { + connection.setAutoCommit(false); + StudentManager.initialize(connection); + EventManager.initialize(connection); + attemptCommit(connection); + } catch (SQLException e) { + fatalSQLError(e); + } + } + + public static void attemptCommit(Connection connection) throws SQLException { + try { + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + } + } + + public static void fatalSQLError(SQLException e) { + logger.error("Fatal SQL Error:", e); + System.exit(ExitCodes.SQL_FATAL_ERROR.getCode()); + } +} diff --git a/app/src/main/java/xyz/vallat/louis/managers/database/EventManager.java b/app/src/main/java/xyz/vallat/louis/managers/database/EventManager.java new file mode 100644 index 0000000..c43898c --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/managers/database/EventManager.java @@ -0,0 +1,83 @@ +package xyz.vallat.louis.managers.database; + +import biweekly.component.VEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.vallat.louis.managers.calendar.CalendarManager; +import xyz.vallat.louis.managers.database.dao.Student; + +import java.sql.*; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; + +public final class EventManager { + + private static final Logger logger = LoggerFactory.getLogger(EventManager.class.getCanonicalName()); + + private EventManager() { + } + + public static int importEvents(Map> events) { + int imported_events = 0; + try (Connection connection = DBManager.getConnection()) { + connection.setAutoCommit(false); + for (Map.Entry> e : events.entrySet()) { + if (e.getValue().isEmpty()) continue; + e.getKey().setId(StudentManager + .addStudent(e.getKey().getSnowflake().asString(), e.getKey().getAde(), connection)); + for (VEvent event : e.getValue()) + if (importEvent(e.getKey(), event, connection) != 0) imported_events++; + } + try { + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + return 0; + } + } catch (SQLException e) { + logger.error("Couldn't import events.", e); + return 0; + } + return imported_events; + } + + private static int importEvent(Student student, VEvent event, Connection connection) throws SQLException { + String sql = "INSERT INTO events(students_id, summary, start_event, end_event) VALUES (?, ?, ?, ?);"; + try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + Instant start = event.getDateStart().getValue().toInstant(); + Instant end = event.getDateEnd().getValue().toInstant(); + stmt.setInt(1, student.getId()); + stmt.setString(2, event.getSummary().getValue()); + stmt.setTimestamp(3, start == null ? null : new Timestamp(start.toEpochMilli()), event.getDateStart().getValue().getRawComponents().); + stmt.setTimestamp(4, end == null ? null : new Timestamp(end.toEpochMilli()), CalendarManager.TZ_UTC); + stmt.executeUpdate(); + stmt.getGeneratedKeys().next(); + assert start != null; + logger.debug("Imported event with id '{}'.", stmt.getGeneratedKeys().getInt(1)); + return stmt.getGeneratedKeys().getInt(1); + } + } + + public static void initialize(Connection connection) throws SQLException { + String sql = """ + CREATE TABLE IF NOT EXISTS events + ( + id int GENERATED ALWAYS AS IDENTITY, + students_id int NOT NULL, + summary text NOT NULL, + start_event timestamptz NOT NULL, + end_event timestamptz NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (students_id) + REFERENCES students (id) + ON DELETE CASCADE + ); + """; + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(sql); + } + } +} diff --git a/app/src/main/java/xyz/vallat/louis/managers/database/StudentManager.java b/app/src/main/java/xyz/vallat/louis/managers/database/StudentManager.java new file mode 100644 index 0000000..962df19 --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/managers/database/StudentManager.java @@ -0,0 +1,45 @@ +package xyz.vallat.louis.managers.database; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +public final class StudentManager { + + private static final Logger logger = LoggerFactory.getLogger(StudentManager.class.getCanonicalName()); + + private StudentManager() { + } + + public static void initialize(Connection connection) throws SQLException { + String sql = """ + CREATE TABLE IF NOT EXISTS students + ( + id int GENERATED ALWAYS AS IDENTITY, + snowflake text NOT NULL, + ade_resource int NOT NULL, + PRIMARY KEY (id), + UNIQUE (snowflake) + ); + """; + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(sql); + } + } + + public static int addStudent(String snowflake, int ade, Connection connection) throws SQLException { + String sql = "INSERT INTO students(snowflake, ade_resource) VALUES (?, ?);"; + try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, snowflake); + stmt.setInt(2, ade); + stmt.executeUpdate(); + stmt.getGeneratedKeys().next(); + logger.debug("Inserted student with id '{}'.", stmt.getGeneratedKeys().getInt(1)); + return stmt.getGeneratedKeys().getInt(1); + } + } +} diff --git a/app/src/main/java/xyz/vallat/louis/managers/database/dao/Student.java b/app/src/main/java/xyz/vallat/louis/managers/database/dao/Student.java new file mode 100644 index 0000000..71867e6 --- /dev/null +++ b/app/src/main/java/xyz/vallat/louis/managers/database/dao/Student.java @@ -0,0 +1,32 @@ +package xyz.vallat.louis.managers.database.dao; + +import discord4j.common.util.Snowflake; + +public class Student { + + private int id; + private final Snowflake snowflake; + private final int ade; + + public Student(Snowflake snowflake, int ade) { + this.snowflake = snowflake; + this.ade = ade; + this.id = 0; + } + + public Snowflake getSnowflake() { + return snowflake; + } + + public int getAde() { + return ade; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/app/src/main/java/xyz/vallat/louis/managers/discord/DiscordManager.java b/app/src/main/java/xyz/vallat/louis/managers/discord/DiscordManager.java index 1f248cd..784ca1d 100644 --- a/app/src/main/java/xyz/vallat/louis/managers/discord/DiscordManager.java +++ b/app/src/main/java/xyz/vallat/louis/managers/discord/DiscordManager.java @@ -9,10 +9,9 @@ import discord4j.core.object.presence.Activity; import discord4j.core.object.presence.Presence; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; import xyz.vallat.louis.commands.Command; +import xyz.vallat.louis.commands.Inscription; import xyz.vallat.louis.commands.Lien; -import xyz.vallat.louis.commands.Subscribe; import xyz.vallat.louis.environment.EnvironmentVariables; import java.util.ArrayList; @@ -20,14 +19,14 @@ import java.util.List; import static xyz.vallat.louis.App.PREFIX; -public class DiscordManager { +public final class DiscordManager { private static final List commands = new ArrayList<>(); private static final Logger logger = LoggerFactory.getLogger(DiscordManager.class.getCanonicalName()); private static GatewayDiscordClient discordClient; static { - commands.add(new Subscribe(PREFIX + "subscribe")); + commands.add(new Inscription(PREFIX + "inscription")); commands.add(new Lien(PREFIX + "lien")); } @@ -69,10 +68,6 @@ public class DiscordManager { discordClient.onDisconnect().block(); } - public static Mono getGuilds() { - return discordClient.getGuilds().count(); - } - public static List getCommands() { return new ArrayList<>(commands); }