546 lines
19 KiB
Java
546 lines
19 KiB
Java
package com.bernard.juliabot;
|
||
|
||
import java.io.*;
|
||
import java.lang.reflect.*;
|
||
import java.sql.*;
|
||
import java.util.*;
|
||
import java.util.jar.JarEntry;
|
||
import java.util.jar.JarFile;
|
||
import java.util.stream.Collectors;
|
||
|
||
import org.apache.commons.io.output.TeeOutputStream;
|
||
|
||
import com.bernard.juliabot.JuliaAddon.JarWithMultipleAddonsException;
|
||
import com.bernard.juliabot.JuliaAddon.JuliaClassLoader;
|
||
import com.bernard.juliabot.api.*;
|
||
import com.thedeanda.lorem.LoremIpsum;
|
||
|
||
import net.dv8tion.jda.core.*;
|
||
import net.dv8tion.jda.core.events.Event;
|
||
|
||
public class Julia {
|
||
|
||
public static final String juliaddonsFolder = "/home/julia/juliaddons/";
|
||
|
||
public static final String sysinPipe = "/home/julia/entree";
|
||
|
||
private static final String token = "";
|
||
|
||
static Julia theJulia;
|
||
|
||
public JDA jda;
|
||
|
||
Map<String,JuliaAddon> avalivableAddons;//pkg+version->addon
|
||
Map<Long,Character> laboratoriesIdentifieurs;//Store channels IDs ...
|
||
|
||
public Map<Character,Laboratory> laboratoires;
|
||
|
||
Map<String,Long> fileTrack;//<File name in juliaddonFolder, lastModified>
|
||
|
||
public EcouteurDEvents lecouteur;
|
||
ReadLoop sysinator;
|
||
|
||
Connection eventDatabase;
|
||
Connection juliaDatabase;
|
||
|
||
JuliaErrPrintStream syserr;
|
||
|
||
|
||
public Julia() {
|
||
|
||
avalivableAddons = new HashMap<>();
|
||
laboratoriesIdentifieurs = new HashMap<>();
|
||
laboratoires = new HashMap<>();
|
||
|
||
fileTrack = new HashMap<>();
|
||
|
||
}
|
||
|
||
public synchronized void startup() {
|
||
//Set up mysql cnnections (events & data)
|
||
Properties connectionProps = new Properties();
|
||
connectionProps.put("user", "julia");
|
||
connectionProps.put("password", "juliabestbotever");
|
||
connectionProps.put("serverTimezone", "UTC");
|
||
connectionProps.put("verifyServerCertificate", "false");
|
||
connectionProps.put("useSSL", "true");
|
||
connectionProps.put("requireSSL", "true");
|
||
try {
|
||
eventDatabase = DriverManager.getConnection("jdbc:mysql://192.168.1.41:3306/juliaEvents", connectionProps);
|
||
juliaDatabase = DriverManager.getConnection("jdbc:mysql://192.168.1.41:3306/julia", connectionProps);
|
||
} catch (SQLException e) {
|
||
System.err.println("Impossible de se connecter a la BDD, ca ne sers a rien de lancer JuL'IA du coup ... Bonne nuit");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
return;
|
||
}
|
||
|
||
System.out.println("La BDD est chargée" + eventDatabase+","+juliaDatabase);
|
||
|
||
|
||
// Démarrage de JDA et du Bot
|
||
try {
|
||
lecouteur = new EcouteurDEvents(this);
|
||
jda = new JDABuilder(AccountType.BOT).setToken(token).addEventListener(lecouteur).build();
|
||
jda.awaitReady();
|
||
} catch(Exception e) {
|
||
System.err.println("Impossible de démarrer JuL'IA");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
return;
|
||
}
|
||
|
||
jda.getGuildById(222947179017404416L).getTextChannelById(460935684669046784L).sendMessage(LoremIpsum.getInstance().getWords(10, 15)).complete();
|
||
|
||
syserr = new JuliaErrPrintStream(jda.getGuildById(222947179017404416L).getTextChannelById(576469792735756309L));
|
||
|
||
TeeOutputStream tos = new TeeOutputStream(System.err, syserr.printStream());
|
||
System.setErr(new PrintStream(tos));
|
||
|
||
Timer syserrtimer = new Timer("Syserr-refresh", true);
|
||
syserrtimer.schedule(new TimerTask() {
|
||
@Override
|
||
public void run() {Julia.theJulia().syserr.flushMessage();}
|
||
} , 0, 1000);
|
||
|
||
//Launch update to see every addon in juliaddon/
|
||
update();//Va remplir la map avalivableAddons
|
||
|
||
|
||
|
||
//Démarrage du sysin
|
||
sysinator = new ReadLoop();
|
||
Thread sysinThread = new Thread(sysinator, "sysin-reader");
|
||
sysinThread.start();
|
||
|
||
System.out.println("Sont disponibles :"+avalivableAddons);
|
||
//Loader l'internalddon (hard-coded)
|
||
//laboratoires.get('#').loadAddon("com.bernard.juliabot.internaddon", "beta", false);//Pas besoin d'update, il viens d'etre fait
|
||
|
||
//Load addons in # and in $ (récupere ce qu'il faut dans la BDD)
|
||
Set<String> toLoad = new HashSet<>();
|
||
try {
|
||
Statement statement = juliaDatabase.createStatement();
|
||
ResultSet result = statement.executeQuery("SELECT * FROM loadedJuliaddons");
|
||
while(result.next()) {
|
||
char laboratory = result.getString("laboratory").charAt(0);
|
||
String pkg = result.getString("pkg");
|
||
String version = result.getString("version");
|
||
toLoad.add(laboratory+pkg+":"+version);
|
||
}
|
||
} catch (SQLException e) {
|
||
System.err.println("Impossible de charger les plugins, vous aurez tout a faire a la main !");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
}
|
||
System.out.println("Les addons à charger : "+toLoad);
|
||
try {
|
||
juliaDatabase.createStatement().executeUpdate("DELETE FROM loadedJuliaddons");
|
||
} catch (SQLException e) {
|
||
System.err.println("Je n'ai pas pu vider la table ... atention aux doublons !");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
}
|
||
|
||
for(String l : toLoad) {
|
||
if(!laboratoires.containsKey(l.charAt(0)))
|
||
laboratoires.put(l.charAt(0), new Laboratory(l.charAt(0)));
|
||
Laboratory labo = laboratoires.get(l.charAt(0));
|
||
labo.loadAddon(l.substring(1, l.lastIndexOf(":")), l.substring(l.lastIndexOf(":")+1), false);//Pas besoin d'update, il viens d'etre fait
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
public synchronized void update() {
|
||
Map<String,JuliaAddon> avalivableAddons = new HashMap<>();
|
||
File juliaddonFolder = new File(juliaddonsFolder);
|
||
for(File f : juliaddonFolder.listFiles((parent,name)->name.toLowerCase().endsWith(".jar"))){
|
||
if(fileTrack.containsKey(f.getName()) && fileTrack.get(f.getName()) == f.lastModified())
|
||
continue;//Le fichier n'a pas bougé, pas besoin de le relire
|
||
fileTrack.put(f.getName(), f.lastModified());
|
||
try {
|
||
JarFile jar = new JarFile(f);
|
||
try {
|
||
JuliaAddon addon = new JuliaAddon(jar);
|
||
String id = addon.pkg+":"+addon.version;
|
||
if(avalivableAddons.containsKey(id))
|
||
System.err.println("L'addon "+id+" a déjà été chargée dans les fichiers "+avalivableAddons.get(id).jarFile.getName()+" et "+jar.getName()+", je garde que le deuxi<78><69><EFBFBD><EFBFBD><EFBFBD><EFBFBD>me !");
|
||
avalivableAddons.put(id, addon);
|
||
}catch(JarWithMultipleAddonsException e) {
|
||
Set<Set<JarEntry>> addonsEntries = e.getAddonsEntries();
|
||
for (Set<JarEntry> entry : addonsEntries) {
|
||
try {
|
||
JuliaAddon addon = new JuliaAddon(jar, entry);
|
||
String id = addon.pkg+":"+addon.version;
|
||
if(avalivableAddons.containsKey(id))
|
||
System.err.println("L'addon "+id+" a déjà été chargée dans les fichiers "+avalivableAddons.get(id).jarFile.getName()+" et "+jar.getName()+", je garde que le deuxi<78><69><EFBFBD><EFBFBD><EFBFBD><EFBFBD>me !");
|
||
avalivableAddons.put(id, addon);
|
||
} catch (JarWithMultipleAddonsException e1) {
|
||
System.err.println("THIS SHOULD NOT HAPPEN");
|
||
System.err.println("TOUT EST CASSÉ ALERTE ROUGE !");
|
||
System.err.println("C'est problematique, j'éteint tout, c'est mieux");
|
||
syserr.flush();
|
||
System.exit(31415926);
|
||
}
|
||
}
|
||
}catch(IllegalStateException e) {
|
||
System.err.println(e.getMessage());
|
||
syserr.flush();
|
||
}
|
||
} catch (IOException e) {
|
||
System.err.println("Impossible de lire le fichier jar "+f.getName());
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
}
|
||
syserr.flush();
|
||
}
|
||
this.avalivableAddons = avalivableAddons;
|
||
}
|
||
|
||
|
||
|
||
public Connection getJuliaDatabase() {
|
||
return juliaDatabase;
|
||
}
|
||
|
||
|
||
|
||
public class ReadLoop implements Runnable{
|
||
|
||
boolean running = false;
|
||
|
||
@Override
|
||
public void run() {
|
||
|
||
running = true;
|
||
try {
|
||
Scanner sc = new Scanner(new File(sysinPipe));
|
||
while(running) {
|
||
String request = sc.nextLine();
|
||
|
||
System.out.println("Je suis censée avoir lu la commande "+request);
|
||
//TODO réagir ...
|
||
|
||
|
||
|
||
}
|
||
|
||
sc.close();
|
||
} catch (FileNotFoundException e) {
|
||
System.err.println("Erreur de lecture du sysin, je deviens sourd");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
running=false;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
public class Laboratory{
|
||
char laboratory;
|
||
Map<String,JuliaAddon> loadedAddons;//<pkg,JuliaAddon> : les addons chargés dans ce laboratoire
|
||
Map<String,CommandeSurchargee> loadedCommands;//<commandName,Calling method> : les commandes chargées
|
||
Map<Class<?>,Object> callerObjects; // <Classe, objet de cette classe> : Les objets instanciés dans ce laboratoire
|
||
Map<String,String> aliases;// <alias, vrainom>
|
||
Map<Class<? extends Event>,Set<Method>> loadedEvents;// Les méthodes à appeler quand un event de cette classe est lancé
|
||
Map<String,JuliaClassLoader> clazzTrack;//<className,JuliaClassLoader which loaded ths class in this laboratory>
|
||
|
||
|
||
|
||
public Laboratory(char laboratory) {
|
||
this.laboratory = laboratory;
|
||
loadedAddons = new HashMap<>();
|
||
loadedCommands = new HashMap<>();
|
||
aliases = new HashMap<>();
|
||
loadedEvents = new HashMap<>();
|
||
clazzTrack = new HashMap<>();
|
||
callerObjects = new HashMap<>();
|
||
}
|
||
|
||
public JuliaClassLoader findClassLoader(String className) {
|
||
return clazzTrack.get(className);
|
||
}
|
||
|
||
public void reloadAddon(String pkg,String version) {
|
||
unloadAddon(pkg);
|
||
loadAddon(pkg, version);
|
||
}
|
||
|
||
public void loadAddon(String pkg,String version) {
|
||
this.loadAddon(pkg, version, true);
|
||
}
|
||
|
||
public synchronized void loadAddon(String pkg,String version,boolean update) {
|
||
if(update)Julia.this.update();
|
||
if(loadedAddons.containsKey(pkg))
|
||
throw new IllegalStateException("Impossible de charger le juliaddon "+pkg+" dans le laboratoire "+laboratory+", il est d<><64><EFBFBD><EFBFBD><EFBFBD><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD> charg<72><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
|
||
try {
|
||
PreparedStatement s = juliaDatabase.prepareStatement("INSERT INTO loadedJuliaddons (ID,laboratory,pkg,version) VALUES (NULL,?,?,?)");
|
||
s.setString(1, Character.toString(laboratory));
|
||
s.setString(2, pkg);
|
||
s.setString(3, version);
|
||
s.executeUpdate();
|
||
} catch (SQLException e) {
|
||
System.err.println("Je ne peux pas notifier la BDD que je load le plugin, je le load pas du coup ...");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
return;
|
||
}
|
||
|
||
JuliaAddon addon = Julia.this.avalivableAddons.get(pkg+":"+version);
|
||
if(addon == null)
|
||
throw new IllegalArgumentException("L'addon "+pkg+" n'est pas disponible dans sa version "+version);
|
||
JuliaClassLoader classLoader = addon.newClassLoader(this);
|
||
classLoader.registerCommandsAndEvents();
|
||
loadedAddons.put(pkg, addon);
|
||
}
|
||
|
||
public synchronized void unloadAddon(String pkg) {
|
||
if(!loadedAddons.containsKey(pkg))
|
||
return;//SI l'addon n'est pas chargé, on a rien besoin de faire
|
||
try {
|
||
PreparedStatement s = juliaDatabase.prepareStatement("DELETE FROM juliaddons WHERE laboratory=? AND pkg=?");
|
||
s.setString(1, Character.toString(laboratory));
|
||
s.setString(2, pkg);
|
||
if(s.executeUpdate() != 1) {
|
||
System.err.println("Ce juliaddon n'était pas chargé dans la BDD ... très bizzare");
|
||
}
|
||
} catch (SQLException e) {
|
||
System.err.println("Je ne peux pas notifier la BDD que je unload le plugin, je l'unload mais vous risquez d'avoir des problemes ...");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
return;
|
||
}
|
||
JuliaAddon addon = loadedAddons.get(pkg);
|
||
addon.classLoaders.get(this.laboratory).unregisterCommandsAndEvents();
|
||
loadedAddons.remove(pkg);
|
||
}
|
||
|
||
public synchronized void registerCommand(Method m,Command c) {
|
||
if(!loadedCommands.containsKey(c.name()))
|
||
loadedCommands.put(c.name(), new CommandeSurchargee(c));
|
||
|
||
//Verify that commands asks for good argument
|
||
if(!Arrays.stream(m.getParameterTypes()).map(CCommande.class::isAssignableFrom).reduce(Boolean::logicalAnd).get())
|
||
throw new IllegalArgumentException("Les paramètres d'une commande doivent tous implémenter CCommande");
|
||
|
||
try {
|
||
m.setAccessible(true);
|
||
}catch(SecurityException e) {
|
||
throw new IllegalArgumentException("Impossible de me donner le droit d'éxecuter "+m.toGenericString());
|
||
}
|
||
|
||
// Argument OK
|
||
//Register aliases
|
||
// print un message d'erreur si un alias a déjà été enregistré pour une autre commande
|
||
Arrays.stream(c.aliases())
|
||
.filter(aliases::containsKey).filter(alias -> !aliases.get(alias).equals(c.name()))
|
||
.forEach(alias -> System.err.println("Faut remplacer l'alias "+alias+", plus de deux addons l'utilisent"));
|
||
Arrays.stream(c.aliases())
|
||
.filter(a -> !aliases.containsKey(a)).forEach(alias -> aliases.put(alias, c.name()));
|
||
|
||
|
||
|
||
//Création de l'objet si ca n'a pas été fait
|
||
checkObject(m.getDeclaringClass());
|
||
|
||
//Enregistre la nouvelle surcharge
|
||
loadedCommands.get(c.name()).surcharger(Arrays.stream(m.getParameterTypes()).collect(Collectors.toSet()), m);
|
||
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")//It is, in reallity checked
|
||
public synchronized void registerEvent(Method m, Discord d) {
|
||
Class<?>[] parameters = m.getParameterTypes();
|
||
if(parameters.length != 1)
|
||
throw new IllegalArgumentException("Les catcheurs d'events ne doivent avoir qu'un seul paramétre, extends Event, la méthode fautive étant "+m.toGenericString());
|
||
if(!Event.class.isAssignableFrom(parameters[0]))
|
||
throw new IllegalArgumentException("Les catcheur d'events ne doivent avoir qu'un paramètre, qui doit extends Event, la méthode fautive étant "+m.toGenericString());
|
||
//La méthode est vérifiée
|
||
if(!loadedEvents.containsKey(parameters[0]))
|
||
loadedEvents.put((Class<? extends Event>) parameters[0], new HashSet<>());
|
||
checkObject(m.getDeclaringClass());
|
||
loadedEvents.get(parameters[0]).add(m);
|
||
}
|
||
|
||
public synchronized void unregisterCommand(Method m,Command c) {
|
||
if(loadedCommands.containsKey(c.name()))
|
||
throw new IllegalStateException("Une commade "+c.name()+" a déjà été enregistrée !");
|
||
//Unregister aliases
|
||
for(String alias:c.aliases())
|
||
aliases.remove(alias, c.name());//Evite de supprimer les alias d'autres commandes qui auraient overwrite
|
||
|
||
loadedCommands.remove(c.name());
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")//Pour un truc qui n'ets pas censé arriver, donc bon ...
|
||
public synchronized void unregisterEvent(Method m, Discord d) {
|
||
Class<?>[] parameters = m.getParameterTypes();
|
||
if(!loadedEvents.containsKey(parameters[0]))//N'est pas censé arriver, si l'event a été load, cette HashSet devrai exister
|
||
loadedEvents.put((Class<? extends Event>) parameters[0], new HashSet<>());
|
||
loadedEvents.get(parameters[0]).remove(m);
|
||
}
|
||
|
||
public void checkObject(Class<?> clazz) {
|
||
if(!callerObjects.containsKey(clazz)) {
|
||
//Chercher un bon constructeur
|
||
Object o = null;
|
||
constLoop : for(Constructor<?> constructor : clazz.getConstructors()) {
|
||
try {
|
||
Class<?>[] paramsTypes = constructor.getParameterTypes();
|
||
Object[] params = new Object[paramsTypes.length];
|
||
for (int i = 0; i < params.length; i++) {
|
||
switch(paramsTypes[i].getName()) {
|
||
case "java.lang.Character":
|
||
params[i] = this.laboratory;
|
||
break;
|
||
case "java.sql.Connection":
|
||
params[i] = Julia.this.juliaDatabase;
|
||
break;
|
||
case "net.dv8tion.jda.core.JDA":
|
||
params[i] = Julia.this.jda;
|
||
break;
|
||
//TODO completer les paramètres
|
||
|
||
default:
|
||
continue constLoop;
|
||
}
|
||
}
|
||
o = constructor.newInstance(params);
|
||
} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||
//Tant pis, constructeur suivant
|
||
continue constLoop;
|
||
}
|
||
if(o != null)break;//Sortir de la boucle, on a notre joli objet
|
||
}
|
||
if(o == null) {
|
||
System.err.println("Je ne peux pas créer l'objet, faudait mettre un costructeur valide (en mon sens :P)");
|
||
System.err.println("Je fais tout planter du coup, votrez addon sera pas chargé !");
|
||
throw new IllegalStateException("Pas de constructeur valable");
|
||
}
|
||
callerObjects.put(clazz, o);
|
||
}
|
||
}
|
||
|
||
public void trigger(Event event) {
|
||
Class<? extends Event> eventClass = event.getClass();
|
||
Set<Method> toCall = loadedEvents.get(eventClass);
|
||
for(Method m : toCall) {
|
||
Object callable = callerObjects.get(m.getDeclaringClass());
|
||
try {
|
||
m.invoke(callable, event);
|
||
} catch (IllegalAccessException e) {
|
||
System.err.println("C'est pas joli joli, je suis censé lancer une méthode à laquelle je n'ai pas accès : "+m.toGenericString());
|
||
throw new IllegalStateException(e);
|
||
} catch (IllegalArgumentException e) {
|
||
System.err.println("La méthode n'est pas censée recevoir de tels erreurs, je devrai avoir chécké la méthode, mais elle ne veut pas de mes arguments ... : "+m.toGenericString());
|
||
throw new IllegalStateException(e);
|
||
} catch (InvocationTargetException e) {
|
||
System.err.println("La méthode à lancé cette exception ... je la relance derrière du coup : "+m.toGenericString());
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
throw new IllegalStateException(e);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
public CommandCalled executeCommand(String name, CCommande content) {
|
||
|
||
String cname = name;
|
||
|
||
if(aliases.containsKey(name))
|
||
cname = aliases.get(name);
|
||
if(!loadedCommands.containsKey(cname))
|
||
throw new IllegalArgumentException("Je ne connais ni la commande, ni l'alias "+cname);
|
||
|
||
|
||
CommandeSurchargee scommande = loadedCommands.get(cname);
|
||
Method m = scommande.getExecutor(content);
|
||
if(m==null)
|
||
throw new IllegalArgumentException("La commande "+cname+" alias "+name+" n'est pas prévue pour s'éxecuter avec un "+content.getClass().getCanonicalName());
|
||
final Object[] parameters = new Object[m.getParameterCount()];
|
||
Arrays.fill(parameters, content);
|
||
final Object caller = callerObjects.get(m.getDeclaringClass());
|
||
|
||
final CommandCalled called = new CommandCalled(m, content, name, cname, caller);
|
||
|
||
|
||
Thread t = new Thread(() -> {
|
||
|
||
try {
|
||
|
||
m.invoke(caller, parameters);
|
||
|
||
} catch (IllegalAccessException e) {
|
||
System.err.println("Problème d'accès à la méthode "+m.toGenericString()+" ... c'est embêtant");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
called.setExitValue(e.getCause());
|
||
} catch (IllegalArgumentException e) {
|
||
System.err.println("Étrange, je pensais avoir check les arguments");
|
||
e.printStackTrace();
|
||
syserr.flush();
|
||
called.setExitValue(e.getCause());
|
||
} catch (InvocationTargetException e) {
|
||
//Lui a planté
|
||
called.setExitValue(e.getCause());
|
||
System.err.println("L'appel a la commande a planté ...");
|
||
e.getCause().printStackTrace();
|
||
}
|
||
syserr.flush();
|
||
called.ended();
|
||
|
||
}, "commande-"+cname+".aka."+name+"-"+System.nanoTime());
|
||
|
||
called.setThread(t);
|
||
|
||
t.start();
|
||
|
||
return called;
|
||
}
|
||
|
||
|
||
public Map<String, JuliaAddon> getLoadedAddons() {
|
||
return loadedAddons;
|
||
}
|
||
|
||
public Map<String, CommandeSurchargee> getLoadedCommands() {
|
||
return loadedCommands;
|
||
}
|
||
|
||
public Map<String, String> getAliases() {
|
||
return aliases;
|
||
}
|
||
|
||
public Map<Class<? extends Event>, Set<Method>> getLoadedEvents() {
|
||
return loadedEvents;
|
||
}
|
||
|
||
public char getL() {
|
||
return laboratory;
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
public static void main(String[] args) {
|
||
theJulia = new Julia();
|
||
theJulia.startup();
|
||
}
|
||
|
||
public static Julia theJulia() {
|
||
return theJulia;
|
||
}
|
||
|
||
}
|