Compare commits

..
11 Commits
27 changed files with 5389 additions and 466 deletions
+81
View File
@@ -0,0 +1,81 @@
/*
* This build file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java Library project to get you started.
* For more details take a look at the Java Libraries chapter in the Gradle
* user guide available at https://docs.gradle.org/4.3/userguide/java_library_plugin.html
*/
// Apply the java-library plugin to add support for Java Library
apply plugin: 'java-library'
apply plugin: 'eclipse'
apply plugin: 'java'
compileJava.options.encoding = 'UTF-8'
repositories {
mavenCentral()
maven {
name 'm2-dv8tion'
url 'https://m2.dv8tion.net/releases'
}
}
sourceSets {
main {
java {
srcDir '/src/main/java'
exclude 'com/bernard/juliabot/internalddon/**'
}
}
}
task apiJar(type: Jar) {
archiveFileName = "JuliabotAPI.jar"
group 'build'
description "Fait un jar avec juste l'api"
from(sourceSets.main.output) {
include "com/bernard/juliabot/api/**"
}
}
task internalddonJar(type: Jar) {
archiveFileName = "JuliabotInternaddon_beta.jar"
group 'build'
description "Fait un jar avec juste l'addon interne"
from(sourceSets.main.output) {
include "com/bernard/juliabot/internaddon/**"
}
}
task execute(type:JavaExec) {
group 'execution'
description "Compile et execute le fichier jar"
mainClass = 'com.bernard.juliabot.Julia'
classpath = sourceSets.main.runtimeClasspath
}
task copyDependencies(type: Copy) {
group 'build'
from configurations.default
into 'dependencies'
}
dependencies {
implementation 'org.slf4j:slf4j-nop:1.7.25'
implementation 'net.dv8tion:JDA:4.4.0_352'
implementation 'com.thedeanda:lorem:2.1'
implementation 'commons-io:commons-io:2.6'
implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.22'
implementation 'com.github.mpkorstanje:simmetrics-core:4.1.1'
}
+10
View File
@@ -0,0 +1,10 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user guide at https://docs.gradle.org/4.8.1/userguide/multi_project_builds.html
*/
rootProject.name = 'Juliabot'
@@ -0,0 +1,89 @@
package com.bernard.juliabot;
import static com.bernard.juliabot.api.Julia.erreur;
import java.lang.reflect.Method;
import com.bernard.juliabot.api.CCommande;
/**
* Objet utilitaire représentant l'appel à une commande, qui suivera l'execution de la
* commande et permet de les gérer dans des threads séparés.
* @author mysaa
*
*/
public class AppelACommande {
Method called;
CCommande argument;
String name;
String cname;
Object caller;
Thread commandThread;
Throwable exitValue = null;
volatile boolean running;
volatile boolean started;
/**
* Crée l'objet en spécifiant simplement ses paramètres.
* @param called La méthode qu'il faut appeler pour executer la commande.
* @param argument L'argument à cette méthode, «comment» la commande doit être lancée.
* @param name Le nom de la commande tel qu'il a été demandé.
* @param cname Le nom de la commande l'identifiant, sans alias.
* @param caller L'objet sur lequel doit être invoquée la commande.
*/
public AppelACommande(Method called, CCommande argument, String name, String cname, Object caller) {
this.called = called;
this.argument = argument;
this.name = name;
this.cname = cname;
this.caller = caller;
this.running = true;
}
/**
* Définit le therad d'appel de la fonction.
*/
public void setThread(Thread commandThread) {
this.commandThread = commandThread;
}
/**
* Définit la valeur de retour du programme (un Throwable en java)
* @param t
*/
public void setExitValue(Throwable t) {
this.exitValue = t;
}
/**
* impose la terminaison de l'execution de la commande.
*/
public void termine() {
running = false;
}
/**
* Teste si la commande est en cours de résolution
*/
public boolean isRunning() {
return running;
}
/**
* Met le thread courant en pause jusqu'à ce que la commande soit résolue.
*/
public void waitFin() {
while(running) {
try {
Thread.sleep(42);
} catch (InterruptedException e) {
erreur("On s'est fait intérrompre !",e);
}
}
}
}
@@ -0,0 +1,91 @@
package com.bernard.juliabot;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.bernard.juliabot.api.CCommande;
import com.bernard.juliabot.api.Commande;
//TODO la javadoc
public class CommandeSurchargee {
String nom;
Commande commande;
Map<Set<Class<?>>,Method> versions = new HashMap<>();
public CommandeSurchargee(Commande c) {
this.nom = c.name();
this.commande = c;
}
public Method getExecuteur(CCommande commande) {
Set<Class<?>> critargs = checkFiliature(Stream.of(commande.getClass()).collect(Collectors.toSet()));
return versions.get(versions.keySet().stream().filter(cr -> critargs.containsAll(cr)).findAny().orElse(null));
}
public void surcharger(Set<Class<?>> discriminant, Method m) {
Set<Class<?>> critargs = checkFiliature(discriminant);
versions.put(critargs, m);
}
public static Set<Class<?>> checkFiliature(Set<Class<?>> unorderedDiscriminant){
return unorderedDiscriminant.stream()
.map(CommandeSurchargee::getAllMamans)
.flatMap(Set::stream)
.filter(CCommande.class::isAssignableFrom)
.filter(c->c.isInterface())
.collect(Collectors.toSet());
}
public static Set<Class<?>> getAllMamans(Class<?> clazz) {
Set<Class<?>> res = new HashSet<>();
res.add(clazz);
if(clazz == Object.class)return res;
// First, add all the interfaces implemented by this class
Class<?>[] interfaces = clazz.getInterfaces();
res.addAll(Arrays.asList(interfaces));
for (Class<?> interfaze : interfaces) {
res.addAll(getAllMamans(interfaze));
}
// Add the super class
Class<?> superClass = clazz.getSuperclass();
// Interfaces does not have java,lang.Object as superclass, they have null, so break the cycle and return
if (superClass == null) {
return res;
}
// Now inspect the superclass
res.addAll(getAllMamans(superClass));
return res;
}
@Override
public String toString() {
return "CommandeSurchargee [name=" + nom + ", versions=" + versions + "]";
}
public String getDesc() {
return commande.description();
}
public Commande getCommande() {
return commande;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,7 @@
package com.bernard.juliabot;
import static com.bernard.juliabot.api.Julia.erreur;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -8,6 +10,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@@ -15,88 +18,212 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.bernard.juliabot.api.Command;
import com.bernard.juliabot.api.Discord;
import com.bernard.juliabot.Julia.Laboratory;
import com.bernard.juliabot.api.JuLIAddon;
import com.bernard.juliabot.Julia.Laboratoire;
import com.bernard.juliabot.api.Commande;
import com.bernard.juliabot.api.EventDiscord;
import com.bernard.juliabot.api.Juliaddon;
/**
* Un objet de cette classe représente un juliaddon, hors contexte de chargement.
* @author mysaa
*
*/
public class JuliaAddon {
private static final Pattern pkgFromClassName = Pattern.compile("^(\\w(\\.\\w){2,})\\.\\w(\\$\\w)*$");
private static final Pattern jarNameParser = Pattern.compile("^([^_])_([^_])(_(.*))?.jar$");
/**
* Un pattern qui permettre d'extraire le paquet dans lequel est un fichier .class
*/
private static final Pattern pkgFromClassName = Pattern.compile("^(\\w+(\\.\\w+){2,})\\.\\w+(\\$\\w+)*$");
/*
* Un pattern permettant d'extraire la version d'un juliaddon d'un fichier.
*/
private static final Pattern jarNameParser = Pattern.compile("^([^_]+)_([^_]+)(_(.+))?.jar$");
/**
* Le pkg de l'addon.
*/
String pkg;
/**
* La version de l'addon.
*/
String version;
JarFile jarFile;
long lastModified;
Set<JarEntry> loaderEntries;
/**
* Le fichier jar duquel a été chargé cet addon
*/
JarFile fichierJar;
/**
* Le timestamp de dernière modification du fichier jar au chargement de l'addon, afin de pouvoir tester
* plus rapidement si il est à jour avec le fichier jar associé.
*/
long derniereModif;
/**
* Liste des entrées du fichier jar à contenant l'addon, trèèèèès utile dans le cas d'un jar avec plusieurs addons.
*/
Set<JarEntry> entreesDuLoader;
Map<Character,JuliaClassLoader> classLoaders;
JuliaClassLoader unassignedClassLoader = null;
/**
* Liste les classloaders associés à chaque laboratoire dans lequel est chargé cet addon.
*/
Map<Laboratoire,JuliaClassLoader> classLoaders;
/**
* Un classloader non assigné, qui est créé afin de récupérer des informations sur l'addon à sa création, et qui
* peut être réutilisé pour un laboratoire.
*/
JuliaClassLoader classLoaderNonAssigne = null;
JuLIAddon addon;
/**
* L'annotation à l'origine de l'addon.
*/
Juliaddon addon;
/**
* La classe de cet annotation.
*/
Class<?> addonClass;
public JuliaAddon(JarFile jar) throws JarWithMultipleAddonsException {
/**
* Crée un addon à partir de toutes les entrées d'un fichier jar.
* @param jar Le fichier jar à lire.
* @throws JarAuxMultiplesAddonsException Si plusieurs addons sont présents dans le fichier jar.
*/
public JuliaAddon(JarFile jar) throws JarAuxMultiplesAddonsException {
this(jar,jar.stream().collect(Collectors.toSet()));
}
public JuliaAddon(JarFile jar,Set<JarEntry> entriesToRead) throws JarWithMultipleAddonsException {
jarFile = jar;
loaderEntries = entriesToRead;
lastModified = new File(jar.getName()).lastModified();
/**
* Crée un addon à partir des entrées jar spécifiées.
* @param jar Le fichier à lire.
* @param entreesALire Les entrées à lire.
* @throws JarAuxMultiplesAddonsException Si plusieurs addons sont dans ces entrées.
*/
public JuliaAddon(JarFile jar,Set<JarEntry> entreesALire) throws JarAuxMultiplesAddonsException {
fichierJar = jar;
entreesDuLoader = entreesALire;
derniereModif = new File(jar.getName()).lastModified();
classLoaders = new HashMap<>();
//Parsing du nom du fichier
Matcher jarMatcher = jarNameParser.matcher(jar.getName());
jarMatcher.matches();
if(!jarMatcher.matches())
throw new IllegalStateException("Le fichier n'a pas un nom valide, je ne peux pas récupérer la version du juliaddon : "+jar.getName().split("/")[jar.getName().split("/").length-1]);
//String jarAddonName = jarMatcher.group(1);
String jarVersion = jarMatcher.group(2);
//String jarExtra = jarMatcher.group(3);
version = jarVersion;
//Sets addon,pkg,version(si redefini) et addonClass
unassignedClassLoader = new JuliaClassLoader(jar, entriesToRead);
classLoaderNonAssigne = new JuliaClassLoader(jar, entreesALire,new LaboFactice());
}
public synchronized JuliaClassLoader newClassLoader(Laboratory laboratory) {
if(unassignedClassLoader==null)
/**
* Crée un classloader pour le laboratoire donné.
* @param laboratoire le laboratoire qui sera associé au classloader.
* @return Un classloader qui peut charger ce Juliaddon.
*/
public synchronized JuliaClassLoader nouveauClassLoader(Laboratoire laboratoire) {
if(classLoaderNonAssigne==null)
try {
JuliaClassLoader cl = new JuliaClassLoader(jarFile, loaderEntries);
cl.laboratory = laboratory;
classLoaders.put(laboratory.laboratory, cl);
JuliaClassLoader cl = new JuliaClassLoader(fichierJar, entreesDuLoader,laboratoire);
classLoaders.put(laboratoire, cl);
return cl;
} catch (JarWithMultipleAddonsException e) {
System.err.println("Cette erreur n'est pas censée arriver, alerte rouge");
} catch (JarAuxMultiplesAddonsException e) {
erreur("Cette erreur n'est pas censée arriver, alerte rouge");
System.exit(685533990);
}
JuliaClassLoader cl = unassignedClassLoader;
unassignedClassLoader = null;
cl.laboratory = laboratory;
classLoaders.put(laboratory.laboratory, cl);
JuliaClassLoader cl = classLoaderNonAssigne;
classLoaderNonAssigne = null;
cl.laboratoire = laboratoire;
classLoaders.put(laboratoire, cl);
return cl;
}
public String getName() {
return addon.name();
/**
* Récupère le nom de cet addon, tel que spécifié par le développeur.
*/
public String getNom() {
return addon.nom();
}
/**
* Renvoie la version de cet addon.
*/
public String getVersion() {
return version;
}
/**
* Renvoie le pkg de cet addon (qu'on peut voir comme son ID)
*/
public String getPkg() {
return pkg;
}
public JarFile getFichierJar() {
return fichierJar;
}
public long getDerniereModif() {
return derniereModif;
}
public JuliaClassLoader getLoader(Laboratoire labo) {
return classLoaders.get(labo);
}
/**
* Un classloader prévu pour lire des Juliaddons depuis des entrées de fichier jar.
* @author mysaa
*
*/
public class JuliaClassLoader extends ClassLoader {
Map<String,Class<?>> loadedClasses;
Laboratory laboratory = null;
/**
* Enregistres les classes déjà chargées pour un chargement plus rapide.
*/
Map<String,Class<?>> classesChargees;
/**
* L'ensemble des entrées que ce classloader à «le droit» de lire.
*/
Set<JarEntry> entrees;
/**
* Le fichier jar associé à ce classloader.
*/
JarFile jar;
/**
* Le laboratoire associé à ce classloader.
*/
Laboratoire laboratoire = null;
HashSet<Method> registeredCommands;
HashSet<Method> registeredEvents;
/**
* Liste les commandes enregistrées par ce classloader.
*/
Set<Method> commandesEnregistrees;
/**
* Liste les events enregistrées par ce classloader.
*/
Set<Method> eventsEnregistrees;
public JuliaClassLoader(JarFile jar,Set<JarEntry> entriesToRead) throws JarWithMultipleAddonsException {
/**
* Crée un classloader pour un certain jeu d'entrées jar dans un laboratoire.
* @param jar le fichier jar duquel lire les entrées.
* @param entreesALire Les entrées à lire pour ce classloader
* @param labo Le laboratoire dans lequel est plongé ce classloader.
* @throws JarAuxMultiplesAddonsException Si plusieurs addons sont trouvés dans le jar. Un classloader, un addon.
*/
public JuliaClassLoader(JarFile jar,Set<JarEntry> entreesALire,Laboratoire labo) throws JarAuxMultiplesAddonsException {
super();
entrees = entreesALire;
this.jar = jar;
classesChargees = new HashMap<>();
this.laboratoire = labo;
Set<Class<?>> readClasses = new HashSet<>();
Map<String,JarEntry> juliaddons = new HashMap<>();
for(JarEntry entry : entriesToRead) {
for(JarEntry entry : entreesALire) {
if(entry.isDirectory())
continue;
if(!entry.getName().endsWith(".class"))
@@ -104,49 +231,46 @@ public class JuliaAddon {
//Le fichier est un .class
try {
String className = entry.getName().replaceAll("/", ".").substring(0, entry.getName().length()-".class".length());
byte[] classData = new byte[(int) entry.getSize()];
InputStream reader = jar.getInputStream(entry);
reader.read(classData);
reader.close();
Class<?> clazz = defineClass(className,classData,0,(int) entry.getSize());
Class<?> clazz;
try {
clazz = loadClass(className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("J'ai demandé a charger une classe sachant que je l'ai dans le jar, mais j'ai quand meme pas réussi à la charger ...", e);
}
readClasses.add(clazz);
//Instantiated class
if(clazz.isAnnotationPresent(JuLIAddon.class)) {
if(clazz.isAnnotationPresent(Juliaddon.class)) {
//Declare package and new addon
Matcher m = pkgFromClassName.matcher(clazz.getName());
if(!m.matches())
throw new IllegalArgumentException("Le nom de la classe est invalide pour un Juliaddon (il faut au moins trois de profondeur dans le package) : "+clazz.getName());
String pkg = m.group(1);
if(pkg==null)
throw new IllegalArgumentException("Imposible de récupérer le pkg.");
if(juliaddons.containsKey(pkg))
throw new IllegalArgumentException("Il existe deja un juliaddon dans le package "+pkg+" , déplacez "+clazz.getName()+" ou "+juliaddons.get(pkg).getName());
juliaddons.put(pkg, entry);
//If overwritten, will JarWithMultipleAddonsException
JuliaAddon.this.addon = clazz.getAnnotation(JuLIAddon.class);
JuliaAddon.this.addon = clazz.getAnnotation(Juliaddon.class);
JuliaAddon.this.pkg = pkg;
JuliaAddon.this.version = addon.version().isEmpty()?version:addon.version();
JuliaAddon.this.addonClass = clazz;
}
}catch (ClassFormatError e) {
System.err.println("Impossible de lire cette classe, son format est incorrect ... veuillez recompiler le jar");
e.printStackTrace();
erreur("Impossible de lire cette classe, son format est incorrect ... veuillez recompiler le jar",e);
continue;
}catch(SecurityException e) {
System.err.println("Vous essayez de mettre cette classe dans un package interdit ! Honte a vous");
e.printStackTrace();
continue;
} catch (IOException e) {
System.err.println("Impossible de lire les données du fichier "+entry.getName()+" dans le jar "+jar.getName());
e.printStackTrace();
erreur("Vous essayez de mettre cette classe dans un package interdit ! Honte a vous",e);
continue;
} catch (IllegalArgumentException e) {
System.err.println("Le fichier "+entry.getName()+" ne décris pas un Juliaddon valide, il n'a donc pas été chargé, mais faites attention !");
e.printStackTrace();
erreur("Le fichier "+entry.getName()+" ne décris pas un Juliaddon valide, il n'a donc pas été chargé, mais faites attention !",e);
continue;
}
}
if(juliaddons.size()<1)
throw new IllegalStateException("Aucun addon présent dans le fichier !");
//Toutes les classes du Jar ont été lues : Si plusieurs addons, on rentre
//dans le if pour lever l'exception, sinon, on initialise le JuliaClassLoader
if(juliaddons.size() > 1) {
@@ -156,7 +280,7 @@ public class JuliaAddon {
for (String pkg : juliaddons.keySet())
addonsEntriesVector.put(pkg, new HashSet<>());
eLoop : for(JarEntry entry : entriesToRead) {
eLoop : for(JarEntry entry : entreesALire) {
if(entry.isDirectory())
continue;
if(!entry.getName().endsWith(".class"))
@@ -167,39 +291,54 @@ public class JuliaAddon {
while((lastPoint = pkg.lastIndexOf(".")) != -1) {//Il y a encore des dossier a monter
pkg = pkg.substring(0, lastPoint+1);//Recule de un dossier
if(addonsEntriesVector.containsKey(pkg)) {
//Pas besoin de tester la validite du pkg car si invalide il ne serai pas rentré dans le Set juliaddons
//Pas besoin de tester la validite du pkg car si invalide il ne serai pas rentr dans le Set juliaddons
addonsEntriesVector.get(pkg).add(entry);
continue eLoop;
}
}
//Si le code atteint ici, le JarEntry n'appartient a aucun pkg valide
System.err.println("Ce jar contiens plusieurs juliaddons. Nichtsdestotroz le fichier "+entry.getName()+" n'a pas de Juliaddon dans son package ou au dessus ... je ne charge donc pas la classe (attention, ca peux ammener a des ClassNotFoundException !!!)");
erreur("Ce jar contiens plusieurs juliaddons. "
+ "Nichtsdestotroz le fichier "+entry.getName()+" n'a pas de Juliaddon dans son package ou au dessus."
+ "je ne charge donc pas la classe (attention, ça peux ammener a des ClassNotFoundException !)");
continue;
}
addonsEntries = new HashSet<>(addonsEntriesVector.values());
throw new JarWithMultipleAddonsException(addonsEntries);
throw new JarAuxMultiplesAddonsException(addonsEntries);
}
// Initialisation du JuliaClassloader
//Les parametres addon, pkg,version et addonCLass ont déjà été initialisés lors de l'itération des fichiers du jar
loadedClasses = new HashMap<>();
for(Class<?> clazz : readClasses)
loadedClasses.put(clazz.getName(), clazz);
// Les paramètres addon, pkg,version et addonCLass ont déjà été initialisés lors de l'itération des fichiers du jar
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(loadedClasses.containsKey(name))
return loadedClasses.get(name);
JuliaClassLoader cl = laboratory.findClassLoader(name);
if(classesChargees.containsKey(name))
return classesChargees.get(name);
///Recherche d'une entry qui collerai
String fileName = name.replaceAll("\\.", "/")+".class";
Optional<JarEntry> oentry = entrees.stream().filter(je -> je.getName().equals(fileName)).findAny();
if(oentry.isPresent()) {
try {
JarEntry entry = oentry.get();
byte[] classData = new byte[(int) entry.getSize()];
InputStream reader = jar.getInputStream(entry);
reader.read(classData);
reader.close();
Class<?> clazz = defineClass(name,classData,0,(int) entry.getSize());
classesChargees.put(name, clazz);
return clazz;
} catch (IOException e) {
erreur("Impossible de lire les données du fichier "+oentry.get().getName()+" dans le jar "+jar.getName()+" je peux vous dire adieu je pense",e);
}
}
JuliaClassLoader cl = laboratoire.trouveLeClassLoader(name);
if(cl != null)
try {
return cl.loadClass(name, resolve);
}catch(ClassNotFoundException e) {
throw new ClassNotFoundException("J'ai délégué le chargement de la classe mais j'aurais pas du ... cette classe ne doit pas exister dans l'addon "+cl.getAddon().getName(),e);
throw new ClassNotFoundException("J'ai délégué le chargement de la classe mais j'aurais pas du ... cette classe ne doit pas exister dans l'addon "+cl.getAddon().getNom(),e);
}
else
try {
@@ -210,65 +349,113 @@ public class JuliaAddon {
}
public void registerCommandsAndEvents() {
registeredCommands = new HashSet<>();
registeredEvents = new HashSet<>();
/**
* Enregistre dans le laboratoire toutes les commandes et
* les events présents dans les classes de ce classloader
*/
public void enregistrerCommadesEtEvents() {
commandesEnregistrees = new HashSet<>();
eventsEnregistrees = new HashSet<>();
Set<Method> toWatch = new HashSet<>();
Collections.addAll(toWatch, JuliaAddon.this.addonClass.getMethods());
for (Class<?> clazz : JuliaAddon.this.addon.searchPath())
for (Class<?> clazz : JuliaAddon.this.addon.filles())
try {
Collections.addAll(toWatch, loadClass(clazz.getName()).getMethods());//loadClass(clazz.getName()) poir eviter les references cassees par les classloaders
} catch (SecurityException | ClassNotFoundException e) {
e.printStackTrace();
System.err.println("L'addon "+JuliaAddon.this.addonClass.getName()+" spécifie la classe "+clazz.getName());
erreur("L'addon "+JuliaAddon.this.addonClass.getName()+" spécifie la classe "+clazz.getName(),e);
}
for(Method m : toWatch) {
Command c = m.getAnnotation(Command.class);
Discord d = m.getAnnotation(Discord.class);
Commande c = m.getAnnotation(Commande.class);
EventDiscord d = m.getAnnotation(EventDiscord.class);
if(c != null) {
registeredCommands.add(m);
laboratory.registerCommand(m, c);
commandesEnregistrees.add(m);
laboratoire.enregistrerCommande(m, c);
}else if(d != null) {
registeredEvents.add(m);
laboratory.registerEvent(m, d);
eventsEnregistrees.add(m);
laboratoire.enregistrerEvent(m, d);
}
}
}
public void unregisterCommandsAndEvents() {
for(Method m : registeredCommands) {
Command c = m.getAnnotation(Command.class);
registeredCommands.add(m);
laboratory.unregisterCommand(m, c);
/**
* Désenregistre du laboratoire associé toutes les commandes et les
* events présents dans les classes de ce classloader.
*/
public void desenregistrerCommandesEtEvents() {
for(Method m : commandesEnregistrees) {
Commande c = m.getAnnotation(Commande.class);
commandesEnregistrees.add(m);
laboratoire.desenregistrerCommande(c);
}
for(Method m : registeredEvents) {
Discord d = m.getAnnotation(Discord.class);
registeredEvents.add(m);
laboratory.unregisterEvent(m, d);
for(Method m : eventsEnregistrees) {
eventsEnregistrees.add(m);
laboratoire.desenregistrerEvent(m);
}
registeredCommands.clear();
registeredEvents.clear();
commandesEnregistrees.clear();
eventsEnregistrees.clear();
}
/**
* Récupère l'addon pour lequel est créé ce classloader.
*/
public JuliaAddon getAddon() {
return JuliaAddon.this;
}
public Set<Method> getCommandesEnregistrees() {
return commandesEnregistrees;
}
public static final class JarWithMultipleAddonsException extends Exception{
public Set<Method> getEventsEnregistrees() {
return eventsEnregistrees;
}
}
/**
* Une exception trahissant plusieurs addons dans un jeu d'entrées jar.
*
* L'exception contient des données qui devraient suffire à sélectionner de manière
* plus judicieuse les entrées afin d'eviter le problème.
* @author mysaa
*
*/
public static final class JarAuxMultiplesAddonsException extends Exception{
private static final long serialVersionUID = -7557959851687207287L;
/**
* Partition des entrées qui devrait séparer correctement les addons
*/
Set<Set<JarEntry>> addonsEntries;
public JarWithMultipleAddonsException(Set<Set<JarEntry>> addonsEntries) {
public JarAuxMultiplesAddonsException(Set<Set<JarEntry>> addonsEntries) {
this.addonsEntries = addonsEntries;
}
public Set<Set<JarEntry>> getAddonsEntries() {
public Set<Set<JarEntry>> entreesDeLAddon() {
return addonsEntries;
}
}
/**
* Un laboratoire factice prévu pour observer les fichier jar sans les enregistrer nul part.
* @author mysaa
*
*/
public static final class LaboFactice extends Julia.Laboratoire{
public LaboFactice() {
Julia.theJulia().super("");
}
}
@Override
public String toString() {
return pkg + ":" + version;
}
}
@@ -0,0 +1,184 @@
package com.bernard.juliabot;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.entities.MessageChannel;
/**
* Stream de caractères dirigé vers un canal discord.
*
* Les utilisateurs doivent appller flushMessage() afin que soit envoyé le message.
* @author mysaa
*
*/
public class PrintStreamDiscord extends OutputStream {
/**
* Un buffer stoquant les prochains caractères à afficher.
*/
StringBuilder tas = new StringBuilder(2000);// Les 2000 caractères de Discord
/**
* Cadenas bloquant le write pendant la création du message discord.
*/
Lock cadenas;
/**
* Le channel dans lequel envoyer tous les caractères.
*/
MessageChannel logChannel;
/**
* La longueur maximale d'un message sur discord.
*/
public static final int maxlength = 1900;
/**
* Crée un canal dirigé vers le canal spécifié.
* @param logChan Un channel discord pour lequel julia à un accès en écriture.
*/
public PrintStreamDiscord(MessageChannel logChan) {
this.cadenas = new ReentrantLock();
this.logChannel = logChan;
}
@Override
public void write(int i) throws IOException {
cadenas.lock();
try {
tas.append((char)(i));
} finally {cadenas.unlock();}
}
@Override
public void flush() {
}
/**
* Envoie le buffer dans le salon discord.
*/
public synchronized void flushMessage() {
if(tas.length()==0)return;
this.cadenas.lock();
String str = tas.toString();
for(int i = 0; i*maxlength < tas.length();i++) {
MessageBuilder builder = new MessageBuilder();
builder.appendCodeBlock(str.substring(i*maxlength, Math.min((i+1)*maxlength,tas.length())), "");
logChannel.sendMessage(builder.build()).queue();
}
tas = new StringBuilder();
this.cadenas.unlock();
}
/**
* Une instance de printstream sauvegardée.
*/
PrintStream ps = null;
/**
* Crée si besoin une seule fois un PrintStream écrivant dans cet OutputStream.
*/
public PrintStream printStream() {
if(ps == null)
ps = new PrintStream(this);
return ps;
}
/**
* Envoie un message découpé en blocs dans le canal spécifié.
* Le message sera découpé en élements d'à peu près la taille max de message.
* @param message Le message à écrire, potentiellement long.
* @param logChannel Le canal de sortie.
*/
public static void logErrMessage(String message, MessageChannel logChannel) {
for(int i = 0; i*maxlength < message.length();i++) {
MessageBuilder builder = new MessageBuilder();
builder.appendCodeLine(message.substring(i*maxlength, Math.min((i+1)*maxlength,message.length())));
logChannel.sendMessage(builder.build()).queue();
builder = new MessageBuilder();
}
}
/**
* Envoie le message découpé en blocs dans le canal d'erreur de ce PrintStream.
* @param message Le message, potentiellement long, à afficher.
*/
public void errMessage(String message) {
logErrMessage(message, logChannel);
}
/**
* Log l'erreur d'entrée dans le canal spécifié. Le message est affiché en premier en texte
* brut, puis le Throwable est ensuite affiché dans un ou plusieurs blocs suivant sa taille.
* @param message Le message décrivant le contexte de l'erreur
* @param error L'erreur en elle-même
* @param logChannel Le canal dans lequel afficher l'erreur.
*/
public static void logErrMessageAndThrowable(String message, Throwable error, MessageChannel logChannel) {
StringWriter sw = new StringWriter();
error.printStackTrace(new PrintWriter(sw));
String errMessage = sw.toString();
if(errMessage.length()+message.length()+1<maxlength) {
MessageBuilder builder = new MessageBuilder();
builder.appendCodeLine(message);
builder.append('\n');
builder.appendCodeBlock(errMessage, "");
logChannel.sendMessage(builder.build()).queue();
}else {
Julia.theJulia().realErr().println("Leeeeeee Bricoleur");
// On doit faire du découpage.
String[] nommes = errMessage.split("\n");
MessageBuilder builder = new MessageBuilder();
builder.appendCodeLine(message);
builder.append('\n');
int depassement = message.length()+1;
StringBuilder reste = new StringBuilder();
for(String nm : nommes) {
if(depassement+nm.length()>=maxlength) {
// On envoie maintenant
builder.appendCodeBlock(reste.toString(), "");
logChannel.sendMessage(builder.build()).queue();
builder = new MessageBuilder();
reste = new StringBuilder();
depassement = 0;
}
if(nm.length() >= maxlength) { // Reste est nécessairement vide
// Pas le choix que d'envoyer avec découpage dégueulasse.
for(int i = 0; i*maxlength < nm.length();i++) {
builder.appendCodeBlock(nm.substring(i*maxlength, Math.min((i+1)*maxlength,nm.length())), "");
logChannel.sendMessage(builder.build()).queue();
builder = new MessageBuilder();
}
}else {
reste.append(nm+"\n");
depassement += nm.length();
}
}
builder.appendCodeBlock(reste.toString(), "");
logChannel.sendMessage(builder.build()).queue();
}
}
/**
* Log l'erreur d'entrée dans le canal de ce PrintStream. Le message est affiché en premier en texte
* brut, puis le Throwable est ensuite affiché dans un ou plusieurs blocs suivant sa taille.
* @param message Le message décrivant le contexte de l'erreur.
* @param error L'erreur en elle-même.
*/
public void errMessageAndThrowable(String message, Throwable error) {
logErrMessageAndThrowable(message, error, logChannel);
}
}
@@ -0,0 +1,93 @@
package com.bernard.juliabot.api;
import java.util.*;
/**
* Décrit des arguments bash d'une commande type bash.
*
* La commande est séparée en nommes, arguments et flags, ces deux derniers étant
* indépendant de leur position.
*
* Exemple: commande nomme1 nomme2 --flag1 --flag2 --arg1=val1 nomme3 --arg2=val2 --flag3
* @author mysaa
*
*/
public class ArgumentsComplexes {
List<String> nommes;
Map<String,String> arguments;
Set<String> flags;
/**
* Initialise cet argument avec les paramètres indiqués.
* @param nommes Les nommes, ordonnés de la commande
* @param arguments Map qui à chaque argument associe sa valeur.
* @param flags Ensemble non ordonné des flags.
*/
public ArgumentsComplexes(List<String> nommes, Map<String, String> arguments, Set<String> flags) {
this.nommes = nommes;
this.arguments = arguments;
this.flags = flags;
}
/**
* Renvoie la liste ordonnée des nommes.
* @return Liste des nommes.
*/
public List<String> getNommes() {
return Collections.unmodifiableList(nommes);
}
/**
* Renvoie une map qui à chaque argument spécifié associe sa valeur.
* @return La map des arguments.
*/
public Map<String, String> getArguments() {
return Collections.unmodifiableMap(arguments);
}
/**
* Renvoie l'ensemble des flags spécifiés.
* @return L'ensemble des flags.
*/
public Set<String> getFlags() {
return Collections.unmodifiableSet(flags);
}
/**
* Renvoie true si et seulement si le flag donné en paramètre a été spécifié.
* @param flag le flag a tester
* @return true ssi flag est spécifié.
*/
public boolean hasFlag(String flag) {
return flags.contains(flag);
}
/**
* Renvoie la valeur associée à l'argument «name», et null si l'argument n'a pas été spécifié.
* @param name le nom de l'argument.
* @return La valeur de l'argument ou null.
*/
public String getArgument(String name) {
return arguments.get(name);
}
/**
* Renvoie le n-ième nomme de la commande, le 0ème nomme correspondant typiquement au nom
* de la commande elle-même.
* @param pos L'indice du nomme.
* @return La valeur du nomme.
*/
public String getNomme(int pos) {
return nommes.get(pos);
}
/**
* Renvoie le nombre de nommes de la commande, utile pour tester si tous ont été spécifiés.
* @return Le nombre de nommes.
*/
public int getNommeCount() {
return nommes.size();
}
}
@@ -0,0 +1,16 @@
package com.bernard.juliabot.api;
/**
* Une super-interface décrivant les capacités d'une requête recue.
* @author mysaa
*
*/
public interface CCommande {
/**
* L'ID du laboratoire dans lequel a été lancé la commande
* @return La chaine de caractère de l'ID
*/
public String getLaboId();
}
@@ -0,0 +1,60 @@
package com.bernard.juliabot.api;
import java.time.OffsetDateTime;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.User;
/**
* Une CCommande décrivant une requète/commande envoyée depuis un message Discord.
* @author mysaa
*
*/
public interface CCommandeDiscord extends CCommandeString {
/**
* Renvoie le message qui a créé cette requête, tel que récupéré par la JDA.
*
* La consistance du message n'est pas assurée.
* @return Le message à l'origine de la requête.
*/
public Message getMessage();
/**
* Renvoie le canal dans lequel la requête a été formulée, tel que récupéré du JDA.
*
* La consistance du channel n'est pas assurée.
* @return Le canal de formulation de la requête.
*/
public MessageChannel getChannel();
/**
* Renvoie l'utilisateur ayant formulé cette requête, tel que récupéré par la JDA.
*
* La consistance de l'user n'est pas assurée.
* @return L'utilisateur à l'origine de la requête.
*/
public User getUser();
/**
* Renvoie la JDA qui a détécté l'évenement.
*
* @return La JDA initiante.
*/
public JDA getJDA();
/**
* Renvoie le contenu original du message ayant créé la requête.
* @return Le texte retiré de ses effets de style (markdown).
*/
public String getContentStripped();
/**
* Renvoie la date de récéption du message.
* @return La date de récéption du message.
*/
public OffsetDateTime getPostDate();
}
@@ -0,0 +1,23 @@
package com.bernard.juliabot.api;
/**
* Une CCommande décrite par une chaine de caractères.
* @author mysaa
*
*/
public interface CCommandeString extends CCommande{
/**
* La chaine de caractères décrivant la commande.
* @return La chaine sus-nommée.
*/
public String getStringCommand();
/**
* Renvoie les arguments complèxes associés à cette chaine de caractères.
* L'objet renvoyé peut être toujours le même ou généré à chaque fois.
* @return Les arguments complexes récupérés de la chaine de la commande.
*/
public ArgumentsComplexes getArguments();
}
@@ -1,14 +0,0 @@
package com.bernard.juliabot.api;
import java.lang.annotation.*;
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Command {
public String name();
public String[] aliases() default {};
public String description();
public boolean admin();//TODO a remplacer par les permissions
}
@@ -0,0 +1,41 @@
package com.bernard.juliabot.api;
import java.lang.annotation.*;
/**
* Annotation à appliquer à une méthode afin qu'elle réagisse aux commandes
* envoyées sur discord ou via le sysin.
*
* Tous les arguments de la méthodes doivent étendre
* {@link com.bernard.juliabot.api.CCommande}.
* @author mysaa
*
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Commande {
/**
* Le nom de la commande. Doit être à peu prêt unique, les nom plus
* «communs» doivent être mis dans les alias.
*/
public String name();
/**
* Tableau des alias possible de la commande, c'est moins grâve si
* il y a des doublons.
*/
public String[] alias() default {};
/**
* Une description d'une ligne environ de la commande, explique son effet.
*/
public String description();
/**
* Le synopsis de la commande dans un style proche des pages man.
*/
public String synopsis() default "";
/**
* true si la commande ne peut être éxecutée que par des admins.
*/
public boolean admin() default false;//TODO a remplacer par les permissions
}
@@ -1,11 +0,0 @@
package com.bernard.juliabot.api;
import java.lang.annotation.*;
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Discord {
public String description();
}
@@ -0,0 +1,20 @@
package com.bernard.juliabot.api;
import java.lang.annotation.*;
/**
* Annotation à appliquer à une méthode pour indiquer qu'elle doit
* être appelée lorsque la JDA recoit des évenements (et que ces évenements sont
* bien destinés au bon laboratoire).
*
* Les méthodes doivent avoir un unique argument qui étende
* {@link net.dv8tion.jda.api.events.GenericEvent}.
* @author mysaa
*
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventDiscord {
public String description();
}
@@ -1,19 +0,0 @@
package com.bernard.juliabot.api;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface JuLIAddon {
String name();
String version() default "";
Class<?>[] searchPath() default {};
public String[] devs();
public boolean important() default false;
}
@@ -0,0 +1,69 @@
package com.bernard.juliabot.api;
import java.sql.Connection;
import com.bernard.juliabot.PrintStreamDiscord;
import static com.bernard.juliabot.Julia.theJulia;
import net.dv8tion.jda.api.JDA;
/**
* Anciennement appelée Trukilie, permet d'interragir avec Julia avec uniquement
* des appels statics, et donc depuis n'importe où dans le programme.
* @author mysaa
*
*/
public class Julia {
/**
* Accès au JDA initialisé par Julia.
* @return La JDA.
*/
public static JDA jda() {
return theJulia().getJda();
}
/**
* Récupère la connection SQL à la base de données.
*
* Prévu pour que les juliaddons accèdent à leurs données.
* @return Une connection normalement ouverte.
*/
public static Connection juliaBDD() {
return theJulia().getJuliaBDD();
}
/**
* Envoie un message dans sysout et dans le canal «status» du Discord
* @param message Le message à envoyer, optimalement d'une seule ligne et assez court.
*/
public static void status(String message) {
System.out.println(message);
PrintStreamDiscord.logErrMessage(message, theJulia().statusTextChannel());
}
/**
* Envoie un message dans le syserr et dans le canal d'erreur du Discord
* @param message Le message décrivant l'erreur, optimalement d'une seule ligne et assez court.
*/
public static void erreur(String message) {
theJulia().realErr().println(message);
if(theJulia().getSyserr() != null)
theJulia().getSyserr().errMessage(message);
}
/**
* Envoie un message d'erreur avec un stacktrace dans le syserr et dans le canal d'erreur du Discord.
* @param message Le message décrivant l'erreur, optimalement d'une seule ligne et assez court.
* @param error L'erreur qui sera écrite à la suite du message, avec son stacktrace.
*/
public static void erreur(String message, Throwable error) {
theJulia().realErr().println(message);
error.printStackTrace(theJulia().realErr());
theJulia().getSyserr().errMessageAndThrowable(message, error);
}
}
@@ -0,0 +1,39 @@
package com.bernard.juliabot.api;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* Annotation à appliquer à une classe principale d'un Juliaddon.
* @author mysaa
*
*/
@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface Juliaddon {
/**
* Le nom de l'addon
*/
String nom();
/**
* La version de l'addon
*
* Par défaut: La version spécifiée dans le nom du fichier de l'addon sera utilisé.
*/
String version() default "";
/**
* Différentes classes, en plus de celle là, dans laquelle chercher des méthodes définissant des événements ou des commandes
*
* Par défaut: Aucune fille, donc uniquement cette classe.
*/
Class<?>[] filles() default {};
/**
* La liste des personnes ayant dévellopé cet addon
*/
public String[] devs();
}
@@ -0,0 +1,670 @@
package com.bernard.juliabot.internaddon;
import static com.bernard.juliabot.api.Julia.erreur;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.simmetrics.StringDistance;
import org.simmetrics.metrics.StringDistances;
import com.bernard.juliabot.AppelACommande;
import com.bernard.juliabot.CommandeSurchargee;
import com.bernard.juliabot.Julia;
import com.bernard.juliabot.Julia.Laboratoire;
import com.bernard.juliabot.JuliaAddon;
import com.bernard.juliabot.JuliaAddon.JuliaClassLoader;
import com.bernard.juliabot.api.ArgumentsComplexes;
import com.bernard.juliabot.api.CCommandeDiscord;
import com.bernard.juliabot.api.CCommandeString;
import com.bernard.juliabot.api.Commande;
import com.bernard.juliabot.api.EventDiscord;
import com.bernard.juliabot.api.Juliaddon;
import com.thedeanda.lorem.LoremIpsum;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.entities.GuildChannel;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import net.dv8tion.jda.internal.utils.tuple.Pair;
/**
* Cet addon est chargé de force dans chaque laboratoire. Il est celui qui permet de charger/décharger
* les autres addons et surtout de comprendre les commandes depuis les events reçus.
*
* Cet addon est fait pour être le seul qui accède directement aux méthodes de {@link com.bernard.juliabot.Julia}, sans
* passer par {@link com.bernard.juilabot.api.Julia}.
* @author mysaa
*/
@Juliaddon(nom="internaddon", devs = "Bernard", version="beta")
public class Internaddon {
public static final String COMMANDEUR = "!!";
/**
* Le catcheur d'evnts qui reconnais les commandes et demande à JuL'IA de les éxecuter.
*/
@EventDiscord(description = "LE catcheur des commandes discords. Les messages recus sont lus puis executes ici si ce sont des commandes")
public void getCommande(MessageReceivedEvent e) {
if(e.getMessage().getContentRaw().startsWith(COMMANDEUR)) {
String nom = e.getMessage().getContentRaw().split(" ")[0].substring(COMMANDEUR.length());
Laboratoire labo = Julia.theJulia().getLecouteur().getLabo(e);//Récupére le labo de l'évent
InternalddonCCommande ccommande = new InternalddonCCommande(e,labo);
try {
AppelACommande appelee = labo.executer(nom, ccommande);
System.out.println(appelee);
}catch(IllegalArgumentException ex) {
// La commande n'existe pas
e.getChannel().sendMessage("Moi pas connaitre `"+nom+"`. Vous apprendre moi ?").queue();
}
}
}
/**
* Une commande pour tester le ping de julia.
*/
@Commande(name = "julia", alias="juju", synopsis = "!!julia", description = "Répond à l'appel", admin = true)
public void julia(CCommandeDiscord commande) {
User user = commande.getUser();
MessageChannel channel = commande.getChannel();
String[] julias = new String[]{"Présent !", "Présente !", "Présentbleu !", "Présentavion !", "Présent(e) !", "Présent(bleu) !",
"Présent(avion) !", "Présent(e)(bleu) !", "Présent(e)(avion) !", "Présent(bleu)(avion) !", "Présent(e)(bleu)(avion) !",
"Oui, c'est moi", user.getName(), "ailuJ", "42 ... oups", "C'est à moi que tu parle ?!", "Entrez c'est ouvert", "Derrière toi !!!",
"J'ai tujorous pas cropmis a qoui ca sret l'otrahhgrope ...", "Laissez moi préparer la domination du monde en paix s'il vous plaét !",
"Je SuIS Un RObot BZzzBzZZZbZZbZZ", "Juliaaaa dadedidadeda dedadedi dadeda", "Tutturu", "Tadaaaaaaa !"};
String out = julias[(int)Math.floor(Math.random() * julias.length)];
channel.sendMessage(out).queue();
}
public static final DateFormat addonFileTimeDf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.FRANCE);
/**
* Liste les addons chargés ainsi que les commandes enregistrées.
*/
@Commande(name = "aide", alias= {"help","ausecours","?","allahuakbar"}, synopsis = "!!help [commandName | pkg] --labo=str", description = "Laissez-vous aider par le meilleur bot du monde", admin = true)
public void aide(CCommandeDiscord commande) {
ArgumentsComplexes args = commande.getArguments();
String lab = args.getArguments().getOrDefault("labo", commande.getLaboId());
Laboratoire l = Julia.theJulia().getLaboratoires().get(lab);
EmbedBuilder msg = new EmbedBuilder();
if(args.getNommeCount() == 1) {
// On veut décrire un laboratoire.
msg.setTitle(String.format("Description du laboratoire «%s»", l.getL()));
for(JuliaAddon e : l.getAddonsCharges()) {
String commandes = e.getLoader(l)
.getCommandesEnregistrees()
.stream()
.map(m -> m.getAnnotation(Commande.class))
.map(c -> c.name())
.collect(Collectors.joining("; "));
msg.addField(
String.format("%s en version %s (%s)",e.getNom(),e.getVersion(),e.getPkg()),
String.format("Liste des commandes disponibles: %s", commandes),
false);
}
} else if (args.getNommeCount() == 2) {
// Compréhension du premier argument das cet ordre:
// pkg > lastPkgName > commandName > alias > addon
String nom = args.getNomme(1);
String pkg = null;
CommandeSurchargee cs = null;
Set<String> pkgs = Julia.theJulia().pkgSet();
if(pkgs.contains(nom))
pkg=nom; // C'est déjà un pkg donc bon
else {
Optional<String> ops = pkgs.stream()
.filter(p -> p.toLowerCase(Locale.FRENCH).endsWith("."+nom.toLowerCase(Locale.FRENCH)))
.findAny();
if(ops.isPresent())
pkg = ops.get();
else {
// On teste une commande OU un alias.
cs = l.commandeAssociee(nom);
if(cs == null)
pkg = comprendreAddon(nom);
}
}
// On a «parse» le premier argument.
if(pkg != null) {
// On veut décrire un package.
String pkgf = pkg;
Optional<JuliaAddon> addonO = l.getAddonsCharges()
.stream()
.filter(a -> a.getPkg().equals(pkgf))
.findAny();
if(addonO.isEmpty()) {
// L'addon n'est pas chargé dans le laboratoire.
repondre(commande, "Vous voulez de l'aide sur l'internaddon "+pkg+" mais "
+ "il n'est pas chargé dans ce laboratoire ...").complete();
return;
}
JuliaAddon addon = addonO.get();
msg.setTitle(String.format("Description du juliaddon %s (%s)",addon.getNom(),addon.getPkg()));
msg.setDescription(String.format(
"Cet addon est chargé dans le laboratoire %s en version %s. "
+ "Il est extrait du fichier %s chargé pour la dernière fois %s",
l.getL(),addon.getVersion(),addon.getFichierJar().getName(), addonFileTimeDf.format(Date.from(Instant.ofEpochMilli(addon.getDerniereModif())))));
JuliaClassLoader loader = addon.getLoader(l);
for(Method m : loader.getEventsEnregistrees()) {
EventDiscord ed = m.getAnnotation(EventDiscord.class);
if(ed==null) {
erreur("Inconsistance dans le classLoader de l'addon "+addon+" : "
+ "la méthode "+m.getName()+" est enregistrée comme event.");
repondre(commande, "Une erreur de classes est survenue. Contactez un développeur du Julia.");
return;
}
msg.addField(
m.getName() ,
ed.description(),
false);
}
msg.addBlankField(false);
for(Method m : loader.getCommandesEnregistrees()) {
Commande ed = m.getAnnotation(Commande.class);
if(ed==null) {
erreur("Inconsistance dans le classLoader de l'addon "+addon+" : "
+ "la méthode "+m.getName()+" est enregistrée comme commande");
repondre(commande, "Une erreur de classes est survenue. Contactez un développeur du Julia.");
return;
}
msg.addField(
(ed.alias().length==0)
? String.format("Commande %s", ed.name())
: String.format("Commande %s (alias %s)", ed.name(),String.join(" ou ",ed.alias())) ,
ed.description(),
false);
}
} else if (cs != null){
// On veut décrire une commande.
Method executeur = cs.getExecuteur(commande);
if(! (executeur.getDeclaringClass().getClassLoader() instanceof JuliaClassLoader)) {
repondre(commande, "Impossible de récupérer l'addon correspondant à cette commande !");
return;
}
JuliaAddon addon = ((JuliaClassLoader)executeur.getDeclaringClass().getClassLoader()).getAddon();
msg.setTitle("Commande "+cs.getCommande().name());
msg.setDescription(cs.getDesc());
msg.addField("Synopsis",cs.getCommande().synopsis()
+ (cs.getCommande().admin()?"\nCette commande requiert les droits d'administrateur.":""),false);
msg.addField("Addon",
String.format("Cette commande est décrite par l'addon %s en version %s décrit "
+ "dans le fichier %s à la date %s. La méthode à executer est la méthode %s "
+ "de la classe %s.", addon.getNom(),addon.getVersion(),addon.getFichierJar().getName(),
addonFileTimeDf.format(Date.from(Instant.ofEpochMilli(addon.getDerniereModif()))),executeur.getName(),executeur.getDeclaringClass().getName()),false);
if(cs.getCommande().alias().length != 0)
msg.addField("Aliases", String.join(", ", cs.getCommande().alias()), false);
} else {
// On a pas compris ce qui devait être décrit.
repondre(commande, "Je n'ai pas compris ce sur quoi je devais vous aider ...").complete();
return;
}
}
//msg.setFooter("Réponse à "+authorName(commande));// -> C'est moche
repondre(commande, msg.build()).complete();
}
/**
* Fait que julia publie un gros bloc de texte de type Lorem ipsum.
*/
@Commande(name = "blabla", alias= {}, description = "Pour vous prouver que je parle", admin = true)//TODO ajouter des options peut-être.
public void blabla(CCommandeDiscord commande) {
Message m = commande.getMessage();
m.getChannel().sendMessage("```"+LoremIpsum.getInstance().getWords(150, 200)+"```").queue();
m.delete();
}
/**
* Renvoie la photo d'un bébé phoque mort pour soulager les développeurs stréssés.
*/
@Commande(name = "tuerBebePhoque", alias= {}, description = "Pour votre plaisir", admin = true)//TODO remplir un peu cette méthode.
public void tuerBebePhoque(CCommandeDiscord commande) {
Message m = commande.getMessage();
m.getChannel().sendMessage("http://toni.mi.free.fr/vanhebdo/actions/phoques/im_phoque_coupGourdinAssomme_Sang.jpg").queue();
}
/**
* Commande de test de la décomposition des commandes. //XXX: À supprimer
* @see ArgumentsComplexes
*/
@Commande(name = "args", description = "Décompose les messages à la Discord'", admin = true)
public void args(CCommandeDiscord commande) {
ArgumentsComplexes ca = commande.getArguments();// Réparer la commande.
StringBuilder s = new StringBuilder();
s.append("```"+commande.getStringCommand()+"```");
s.append("Liste des nommes : \n");
s.append(IntStream.range(0, ca.getNommeCount()).mapToObj(i -> "\t"+i+" : `"+ca.getNomme(i)+"`").collect(Collectors.joining("\n")));
s.append("\nListe des flags : \n");
s.append(ca.getFlags().stream().map(f->"\t`"+f+"`").collect(Collectors.joining("\n")));
s.append("\nListe des arguments : \n");
commande.getMessage().getChannel().sendMessage(s).queue();
}
/**
* Liste les guildes auquel Julia à accès.
*/
@Commande(name="guildes", alias= {"guilds"}, description = "Liste les guildes disponibles",admin=true)
public void guildes(CCommandeDiscord commande) {
MessageBuilder mb = new MessageBuilder();
mb.append("Liste des serveurs :\n");
Julia.theJulia().getJda().getGuilds()
.stream()
.map(g -> g.getName() + "(" + g.getMemberCount() + " membres, id:" + g.getId() + ")\n")
.forEach(mb::append);;
commande.getChannel().sendMessage(mb.build()).complete();
}
/**
* Met à jour la liste des addons disponibles, en lisant le dossier juliaddons.
*/
@Commande(name="maj", alias= {"update","miseAJour"},description = "Met a jour le dossier des addons",admin=true)
public void maj(CCommandeString commande) {
Julia.theJulia().addonsMaj();
if(CCommandeDiscord.class.isInstance(commande.getClass()))
repondre((CCommandeDiscord)commande, "La liste des addons disponibles a été mise à jour.");
}
/**
* Charge l'addon spécifié en ligne de commande dans le laboratoire d'appel.
*/
@Commande(name="charger", alias= {"load"}, description = "Charge l'addon dans ce laboratoire",admin=true)
public void charger(CCommandeDiscord commande) {
ArgumentsComplexes cc = commande.getArguments();
String lab = cc.getArguments().getOrDefault("labo", commande.getLaboId());
if(cc.getNommeCount()<2) {
repondre(commande, "Il vous faut préciser l'addon que vous voulez charger !").complete();
return;
}
String addonPkg = comprendreAddon(cc.getNomme(1));
if(addonPkg == null) {
repondre(commande, String.format("Je ne trouve pas d'addon correspondant à «%s» ...", cc.getNomme(1))).complete();
return;
}
String version = null;
if(cc.getNommeCount()<3)
version = Julia.theJulia().latest(addonPkg);
else
version = cc.getNomme(2);
if(! Julia.theJulia().checkVersion(addonPkg,version)) {
repondre(commande, "Je ne trouve pas la version "+version+" pour l'addon "+addonPkg).complete();
return;
}
Laboratoire l = Julia.theJulia().getLaboratoires().get(lab);
try {
l.loadAddon(addonPkg, version);
}catch (Exception e) {
repondre(commande, "Impossible de charger l'addon spécifié ! Contactez un admin julia.").complete();
erreur("Tentative de chargement depuis l'internaddon", e);
return;
}
repondre(commande, String.format("L'addon %s a été chargé dans sa version %s dans le laboratoire %s", addonPkg, version, lab)).complete();
}
/**
* Décharge l'addon spécifié du laboratoire d'appel.
*/
@Commande(name="decharger", alias= {"unload"}, description = "Décharge l'addon de ce laboratoire",admin=true)
public void decharger(CCommandeDiscord commande) {
ArgumentsComplexes cc = commande.getArguments();
String lab = cc.getArguments().getOrDefault("labo", commande.getLaboId());
if(cc.getNommeCount()<2) {
repondre(commande, "Il vous faut préciser l'addon que vous voulez charger !").complete();
return;
}
String addonPkg = comprendreAddon(cc.getNomme(1));
Laboratoire l = Julia.theJulia().getLaboratoires().get(lab);
try {
l.dechargerAddon(addonPkg);
}catch (Exception e) {
repondre(commande, "Impossible de décharger l'addon spécifié ! Contactez un admin julia.").complete();
erreur("Tentative de déchargement depuis l'internaddon", e);
return;
}
repondre(commande, String.format("L'addon %s a été déchargé du laboratoire %s", addonPkg, lab)).complete();
}
/**
* Décharge puis recharge l'addon du laboratoire spécifié.
*/
@Commande(name="recharger", alias= {"reload"}, description = "Recharge l'addon de ce laboratoire",admin=true)
public void recharger(CCommandeDiscord commande) {
ArgumentsComplexes cc = commande.getArguments();
String lab = cc.getArguments().getOrDefault("labo", commande.getLaboId());
if(cc.getNommeCount()<2) {
repondre(commande, "Il vous faut préciser l'addon que vous voulez charger !").complete();
return;
}
String addonPkg = comprendreAddon(cc.getNomme(1));
Laboratoire l = Julia.theJulia().getLaboratoires().get(lab);
try {
l.rechargeAddon(addonPkg, null);
}catch (Exception e) {
repondre(commande, "Impossible de recharger l'addon spécifié ! Contactez un admin julia.").complete();
erreur("Tentative de rechargement depuis l'internaddon", e);
return;
}
repondre(commande, String.format("L'addon %s a été rechargé dans le laboratoire %s", addonPkg, lab)).complete();
}
/**
* Une commande qui lance une IllegalStateException à des fins de tests.
*/
//XXX: À supprimer ou cacher
@Commande(name="caca",description = "Déclanche une exception ... Faut bien tester, hein !",admin=true)
public void caca(CCommandeDiscord commande) {
throw new IllegalStateException("Nan mais tout va bien ... je fait ce qu'on m'a demandé ^^");
}
StringDistance addonMetric = StringDistances.levenshtein();
final float maxAcceptableDistance = 8.0f;
/**
* Renvoie le pkg d'un addon disponible qui correspond le mieux au nom spécifié.
* @param nom Le nom, typiquement donné par l'utilisateur.
* @return Un pkg d'un internaddon, normalement chargeable.
*/
public String comprendreAddon(String nom) {
Set<String> pkgs = Julia.theJulia().pkgSet();
if(pkgs.contains(nom))
return nom; // C'est déjà un pkg donc bon
Optional<String> ops = pkgs.stream().filter(p -> p.endsWith("."+nom)).findAny();
if(ops.isPresent())
return ops.get();
List<Pair<String, Float>> listProches = pkgs.stream()
.map(s -> Pair.of(s, addonMetric.distance(s.substring(s.lastIndexOf(".")+1), nom)))
.filter(e -> e.getRight() < maxAcceptableDistance)
.sorted((a,b) -> Float.compare(a.getRight(), b.getRight()))
.collect(Collectors.toList());
if(listProches.size()>1)
if(listProches.get(0).getRight() < listProches.get(1).getRight())
return listProches.get(0).getLeft();
else
return null;
else if(listProches.size()==1)
return listProches.get(0).getLeft();
else
return null;
}
/**
* Fait répondre à Julia sa réponse du message source
* @param commande La ccommande de requête
* @param message Le message à renvoyer
* @return L'action d'envoi de la réponse.
*/
public MessageAction repondre(CCommandeDiscord commande, MessageEmbed message) {
return commande.getMessage().replyEmbeds(message);
}
/**
* Fait répondre à Julia sa réponse du message source
* @param commande La ccommande de requête
* @param message Le message à renvoyer
* @return L'action d'envoi de la réponse.
*/
public MessageAction repondre(CCommandeDiscord commande, Message message) {
return commande.getMessage().reply(message);
}
/**
* Fait répondre à Julia sa réponse du message source
* @param commande La ccommande de requête
* @param message Le contenu du message à renvoyer
* @return L'action d'envoi de la réponse.
*/
public MessageAction repondre(CCommandeDiscord commande, String message) {
return commande.getMessage().reply(message).mentionRepliedUser(true);
}
public String authorName(CCommandeDiscord commande) {
if(commande.getMessage().isFromGuild())
return ((GuildChannel)commande.getChannel()).getGuild().retrieveMember(commande.getUser()).complete().getEffectiveName();
else
return commande.getUser().getName();
}
/**
* Une simple implémentation de {@link CCommandeDiscord} et de {@link CCommandeString} afin d'appeler
* les commandes depuis les events jda.
* @author mysaa
*
*/
public class InternalddonCCommande implements CCommandeDiscord,CCommandeString{
Message m;
Laboratoire labo;
/**
* Crée une simple commande.
* @param e L'évenement trahissant la récéption de la commande.
* @param labo Le laboratoire associé à la commande.
*/
public InternalddonCCommande(MessageReceivedEvent e,Laboratoire labo) {
m = e.getMessage();
this.labo = labo;
}
@Override
public String getStringCommand() {
return m.getContentRaw();
}
@Override
public ArgumentsComplexes getArguments() {
return parseCommandArguments(getStringCommand());
}
@Override
public Message getMessage() {
return m;
}
@Override
public MessageChannel getChannel() {
return m.getChannel();
}
@Override
public User getUser() {
return m.getAuthor();
}
@Override
public JDA getJDA() {
return m.getJDA();
}
@Override
public String getContentStripped() {
return m.getContentStripped();
}
@Override
public OffsetDateTime getPostDate() {
return m.getTimeCreated();
}
@Override
public String getLaboId() {
return labo.getL();
}
}
/**
* Comprend une ligne de commande, en séparant les nommes, flags et arguments en
* un seul objet.
* @param raw La ligne de commande à parser
* @return L'objet arguments complexes contenant toutes les données séparées.
*/
public static ArgumentsComplexes parseCommandArguments(String raw) {
List<String> nommes = new ArrayList<String>();
Map<String,String> arguments = new HashMap<>();
Set<String> flags = new HashSet<>();
String state = "n";
String reading = "";
List<String> stringized = new ArrayList<>();
for(String morceau : raw.split(" ")) {
if(state.equals("n")) {
if(morceau.isEmpty())
continue;
if(morceau.startsWith("\"") || morceau.startsWith("`")) {
state = String.valueOf(morceau.charAt(0));
morceau = morceau.substring(1);
}else {
stringized.add(morceau);
continue;
}
}
if(state.equals("\"") || state.equals("`")) {
if(morceau.isEmpty()) {
reading += " ";
continue;
}
if(morceau.endsWith(state)) {
reading += morceau.substring(0, morceau.length()-1);
stringized.add(reading);
reading = "";
state = "n";
continue;
}
reading += morceau + " ";
continue;
}
}
for(String morceau : stringized) {
if(state.equals("n")) {
if(morceau.isEmpty())
continue;
if(morceau.startsWith("--")) {
state = "#"+morceau.substring(2);
continue;
}
if(morceau.startsWith("-")) {
flags.add(morceau.substring(1));
continue;
}
nommes.add(morceau);
continue;
}
if(state.startsWith("#")) {
arguments.put(state.substring(1), morceau);
continue;
}
}
return new ArgumentsComplexes(nommes, arguments, flags);
}
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
gradle jar
gradle internalddon
gradle apiJar
scp -r -P 27182 -4 build/libs/Juliabot.jar root@bernard.com.de:/srv/julia/
scp -r -P 27182 -4 build/libs/JuliabotInternaddon_beta.jar root@bernard.com.de:/srv/julia/juliaddons/