🎃 Halloween Update

master
Lars Mueller 2019-10-31 21:54:15 +01:00
parent 59c2058ac4
commit b90373a139
212 changed files with 1961 additions and 2478 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/MinetestChatBridgeBot/.gradle/
/MinetestChatBridgeBot/build/
/MinetestChatBridgeIRCBot/.gradle/
/MinetestChatBridgeIRCBot/build/

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>MinetestChatBridgeBot</name>
<comment>Project MinetestChatBridgeBot created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1))
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,37 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
}
}
apply plugin: 'java'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'jacoco'
apply plugin: 'application'
description = 'Minetest Chat Bridge Discord Bot'
mainClassName = 'appguru.Main'
repositories {
mavenCentral()
jcenter()
}
dependencies {
compile "net.dv8tion:JDA:4.0.0_39", 'ch.qos.logback:logback-classic:1.3.0-alpha4'
compile group: 'com.google.guava', name: 'guava', version: '23.5-jre'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
jar {
manifest {
attributes(
'Created-By': "Gradle ${gradle.gradleVersion}",
'Main-Class': "appguru.Main"
)
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
rootProject.name = 'MinetestChatBridgeBot'

View File

@ -1,6 +1,8 @@
package appguru;
import bridge.FileBridge;
import bridge.ProcessBridge;
import bridge.SocketBridge;
import chat.Bot;
import commands.StatusCommand;
import misc.GarbageCollector;
@ -15,12 +17,15 @@ import java.util.Date;
public class Main {
public static long STARTED_AT;
public static int GARBAGE_COLLECTION=5000; //5s
public static long PING_WAIT=20000; //20s
public static long PING_WAIT=5000; //5s
public static String PREFIX="!";
public static String DISCORD_PREFIX="?";
public static String GUILD_ID=null;
public static PrintStream OUT=System.out;
public static ProcessBridge PROCESS_BRIDGE;
public static void main(String[] args) throws IOException {
if (args.length > 4) {
@ -60,16 +65,22 @@ public class Main {
OUT.println("INFO: Starting Minetest chat bridge");
String token=args[0];
String channelname=args[1];
File in=new File(args[2]);
File out=new File(args[3]);
if (!in.isFile() || !out.isFile() || !in.canWrite() || !in.canRead() || !out.canWrite() || !out.canRead()) {
OUT.println("ERR: Input or output files do not exist or can't be read/written.");
OUT.close();
System.exit(0);
if (args[2].length() == 0) {
int socket_port=Integer.parseInt(args[3]);
PROCESS_BRIDGE=new SocketBridge("localhost", socket_port);
} else {
File in=new File(args[2]);
File out=new File(args[3]);
if (!in.isFile() || !out.isFile() || !in.canWrite() || !in.canRead() || !out.canWrite() || !out.canRead()) {
OUT.println("ERR: Input or output files do not exist or can't be read/written.");
System.exit(0);
}
PROCESS_BRIDGE=new FileBridge(in, out);
}
ProcessBridge pb=new ProcessBridge(in, out);
try {
Bot i=new Bot(token, pb, channelname);
Bot i=new Bot(token, PROCESS_BRIDGE, channelname);
i.registerInfo("status", "Status", "", Color.CYAN, null);
i.registerCommand("status", new StatusCommand());

View File

@ -5,21 +5,18 @@ import appguru.Main;
import java.io.*;
import java.util.function.Consumer;
public class ProcessBridge {
public static long PING_WAIT=20000; //20s
public class FileBridge extends ProcessBridge {
public long last_ping;
public void ping() {
last_ping=System.currentTimeMillis();
}
public File out_file;
public File in;
public PrintWriter out;
private long last_ping_sent;
public ProcessBridge(File in, File out) throws IOException {
public FileBridge(File in, File out) throws IOException {
this.out = new PrintWriter(new BufferedWriter(new FileWriter(out, true)));
this.out_file=out;
this.in=in;
this.last_ping_sent=System.currentTimeMillis();
}
public void kill(String reason) {
@ -30,9 +27,11 @@ public class ProcessBridge {
fw.write("");
fw.close();
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
Main.OUT.close();
out.write("[KIL]"+reason);
out.close();
System.exit(0);
}
@ -47,15 +46,18 @@ public class ProcessBridge {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
Main.OUT.flush();
if (System.currentTimeMillis()-last_ping_sent >= 1000) {
out.write("[PIN]");
}
// Main.OUT.flush();
out.flush();
out.close();
try {
out = new PrintWriter(new BufferedWriter(new FileWriter(out_file, true)));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
}
@ -71,19 +73,19 @@ public class ProcessBridge {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
BufferedReader r = null;
try {
r = new BufferedReader(new FileReader(in));
} catch (FileNotFoundException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
String line = null;
try {
line = r.readLine();
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
boolean one_line = line != null;
while (line != null) {
@ -99,7 +101,7 @@ public class ProcessBridge {
try {
line = r.readLine();
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
if (System.currentTimeMillis()-last_ping > Main.PING_WAIT) {
@ -109,7 +111,7 @@ public class ProcessBridge {
try {
FileWriter fw = new FileWriter(in);fw.write("");fw.close();
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
}

View File

@ -0,0 +1,18 @@
package bridge;
import java.util.function.Consumer;
public abstract class ProcessBridge {
public long last_ping;
public void ping() {
last_ping=System.currentTimeMillis();
}
public abstract void kill(String reason);
public abstract void write(String out);
public abstract void serve();
public abstract void listen(Consumer<String> line_consumer);
}

View File

@ -0,0 +1,112 @@
package bridge;
import appguru.Main;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author lars
*/
public class SocketBridge extends ProcessBridge {
private Socket socket;
private final BufferedWriter writer;
private BufferedReader reader;
public SocketBridge(String host, int port) throws IOException {
socket = new Socket();
socket.connect(new InetSocketAddress(host, port));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
@Override
public void kill(String reason) {
Main.OUT.println("INFO: "+reason);
if (socket.isConnected()) {
try {
writer.write("[KIL]"+reason);
} catch (IOException ex) {
ex.printStackTrace(Main.OUT);
}
}
try {
writer.close();
reader.close();
socket.close();
} catch (IOException ex) {
ex.printStackTrace(Main.OUT);
} finally {
Main.OUT.close();
System.exit(1);
}
}
@Override
public void write(String out) {
synchronized (writer) {
try {
writer.write(out+"\n");
writer.flush();
} catch (IOException ex) {
ex.printStackTrace(Main.OUT);
}
}
}
@Override
public void serve() {
}
@Override
public void listen(Consumer<String> line_consumer) {
ping();
new Thread(() -> {
while(true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace(Main.OUT);
}
String line = null;
try {
line = reader.readLine();
} catch (IOException e) {
e.printStackTrace(Main.OUT);
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
kill("Socket connection lost");
}
}
boolean one_line = line != null;
while (line != null) {
if (line.startsWith("[PIN]")) { // A PING YAY
ping();
} else if (line.startsWith("[KIL]")) {
kill("Minetest server shutting down; shutting down as well.");
} else {
line_consumer.accept(line);
}
try {
line = reader.readLine();
} catch (IOException e) {
e.printStackTrace(Main.OUT);
}
}
if (System.currentTimeMillis()-last_ping > Main.PING_WAIT) {
kill("No ping during the last "+(Main.PING_WAIT/1000)+"s; shutting down.");
}
}
}).start();
}
}

View File

@ -5,44 +5,32 @@ import bridge.ProcessBridge;
import com.google.common.collect.HashBiMap;
import commands.Command;
import misc.Utils;
import net.dv8tion.jda.core.*;
import net.dv8tion.jda.core.entities.*;
import net.dv8tion.jda.core.events.ReadyEvent;
import net.dv8tion.jda.core.events.channel.text.TextChannelCreateEvent;
import net.dv8tion.jda.core.events.channel.text.TextChannelDeleteEvent;
import net.dv8tion.jda.core.events.channel.text.update.*;
import net.dv8tion.jda.core.events.guild.GuildJoinEvent;
import net.dv8tion.jda.core.events.guild.member.*;
import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
import net.dv8tion.jda.core.events.message.priv.PrivateMessageReceivedEvent;
import net.dv8tion.jda.core.events.message.priv.react.PrivateMessageReactionAddEvent;
import net.dv8tion.jda.core.events.role.RoleCreateEvent;
import net.dv8tion.jda.core.events.role.RoleDeleteEvent;
import net.dv8tion.jda.core.events.role.update.RoleUpdateColorEvent;
import net.dv8tion.jda.core.events.role.update.RoleUpdateNameEvent;
import net.dv8tion.jda.core.events.user.update.UserUpdateNameEvent;
import net.dv8tion.jda.core.hooks.ListenerAdapter;
import net.dv8tion.jda.core.utils.cache.MemberCacheView;
import net.dv8tion.jda.api.*;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.ReadyEvent;
import net.dv8tion.jda.api.events.channel.text.TextChannelCreateEvent;
import net.dv8tion.jda.api.events.channel.text.TextChannelDeleteEvent;
import net.dv8tion.jda.api.events.channel.text.update.*;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.member.*;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.role.RoleCreateEvent;
import net.dv8tion.jda.api.events.role.RoleDeleteEvent;
import net.dv8tion.jda.api.events.role.update.RoleUpdateColorEvent;
import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import javax.security.auth.login.LoginException;
import java.io.*;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.awt.Color;
import java.util.function.Consumer;
import java.util.function.Predicate;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
public class Bot extends ListenerAdapter {
public static int DEFAULT_COLOR=Integer.parseInt("7289DA",16); // Discord color
public long last_ping;
public void ping() {
last_ping=System.currentTimeMillis();
}
public String text_channel;
public ProcessBridge bridge;
public JDA jda;
@ -88,20 +76,27 @@ public class Bot extends ListenerAdapter {
return this.jda.getGuildById(Main.GUILD_ID);
}
@Override
public void onTextChannelDelete(TextChannelDeleteEvent event) {
if (event.getChannel().getIdLong() == global_channel) {
System.err.println("Error ! Global channel was deleted !");
System.exit(1);
}
}
@Override
public void onTextChannelUpdatePermissions(TextChannelUpdatePermissionsEvent event) {
if (event.getChannel().getIdLong() == global_channel && !event.getChannel().canTalk()) {
System.err.println("Error ! Cannot talk in global channel !");
System.exit(1);
}
}
@Override
public void onTextChannelUpdateNSFW(TextChannelUpdateNSFWEvent event) {}
@Override
public void onTextChannelUpdateParent(TextChannelUpdateParentEvent event) {}
@Override
public void onTextChannelCreate(TextChannelCreateEvent event) {}
@Override
@ -112,11 +107,15 @@ public class Bot extends ListenerAdapter {
}*/
// IDEA: leave
}
public String escapeName(String name) {
return name.replace(" ", "_").replace(",", "_");
}
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent e) {
Member m=e.getMember();
String name=m.getEffectiveName().replace(" ", "_");
String name=escapeName(m.getEffectiveName());
members.put(Utils.getFreeKey(name, members), m.getUser().getIdLong());
bridge.write("[JOI]"+name+" #"+ Utils.getColorString(m.getColorRaw()));
}
@ -169,10 +168,10 @@ public class Bot extends ListenerAdapter {
if (chosen == null) {
List<Guild> guilds=event.getJDA().getGuilds();
String guild_id=guilds.get(0).getId();
OffsetDateTime min_join_time=guilds.get(0).getMember(event.getJDA().getSelfUser()).getJoinDate();
OffsetDateTime min_join_time=guilds.get(0).getMember(event.getJDA().getSelfUser()).getTimeJoined();
for (int i=1; i < guilds.size(); i++) {
Guild g=guilds.get(i);
OffsetDateTime join_time=guilds.get(i).getMember(event.getJDA().getSelfUser()).getJoinDate();
OffsetDateTime join_time=guilds.get(i).getMember(event.getJDA().getSelfUser()).getTimeJoined();
if (join_time.isBefore(min_join_time)) {
min_join_time=join_time;
guild_id=g.getId();
@ -182,16 +181,16 @@ public class Bot extends ListenerAdapter {
Main.GUILD_ID=guild_id;
}
setGlobalChannel(getGuild().getTextChannelsByName(text_channel, true).get(0).getIdLong());
event.getJDA().getPresence().setGame(net.dv8tion.jda.core.entities.Game.playing("Minetest"));
event.getJDA().getPresence().setActivity(Activity.playing("Minetest"));
for (Member m:getGuild().getMemberCache()) {
String name=m.getEffectiveName().replace(" ", "_");
String name=escapeName(m.getEffectiveName());
String finalname=Utils.getFreeKey(name, members);
members.put(finalname, m.getUser().getIdLong());
int color=m.getColor() == null ? DEFAULT_COLOR:m.getColorRaw();
bridge.write("[LIS]"+finalname+" #"+ Utils.getColorString(color));
}
for (Role r:getGuild().getRoles()) {
String name=r.getName().replace(" ", "_");
String name=escapeName(r.getName());
String finalname=Utils.getFreeKey(name, roles);
String output="[ROL]"+finalname+" #"+Utils.getColorString(r.getColorRaw());
for (Member m:getGuild().getMembersWithRoles(r)) {
@ -229,7 +228,7 @@ public class Bot extends ListenerAdapter {
@Override
public void onRoleCreate(RoleCreateEvent event) {
String name=event.getRole().getName().replace(" ", "_");
String name=escapeName(event.getRole().getName());
String output="[ROL]"+Utils.getFreeKey(name, roles)+" #"+Utils.getColorString(event.getRole().getColorRaw());
bridge.write(output);
}
@ -238,7 +237,7 @@ public class Bot extends ListenerAdapter {
@Override
public void onRoleUpdateName(RoleUpdateNameEvent event) {
String oldname=roles.inverse().get(event.getRole().getIdLong());
String name=Utils.getFreeKey(event.getNewName().replace(" ", "_"), roles);
String name=Utils.getFreeKey(escapeName(event.getNewName()), roles);
roles.inverse().remove(event.getRole().getIdLong());
roles.put(name, event.getRole().getIdLong());
String output="[NAM]"+oldname+" "+name;
@ -270,21 +269,18 @@ public class Bot extends ListenerAdapter {
}
}
public void onGuildMemberNickChange(GuildMemberNickChangeEvent event) {
String newnick=(event.getNewNick() != null ? event.getNewNick():event.getUser().getName()).replace(" ", "_");
@Override
public void onGuildMemberUpdateNickname(GuildMemberUpdateNicknameEvent event) {
String newnick=escapeName((event.getNewNickname() != null ? event.getNewNickname():event.getUser().getName()));
if (members.containsKey(newnick)) {
getGuild().getController().setNickname(event.getMember(), event.getPrevNick()).queue();
event.getMember().getUser().openPrivateChannel().queue(pc -> pc.sendMessage("Your nickname could not be changed to `"+event.getNewNick()+"` as there already is another guild member with a similar nickname.").queue());
getGuild().modifyNickname(event.getMember(), event.getOldNickname()).queue();
event.getMember().getUser().openPrivateChannel().queue(pc -> pc.sendMessage("Your nickname could not be changed to `"+event.getNewNickname()+"` as there already is another guild member with a similar nickname.").queue());
} else {
members.inverse().remove(event.getMember().getUser().getIdLong());
members.put(newnick, event.getMember().getUser().getIdLong());
}
}
public String getIdentifier(Member m) {
return null; // TODO replace spaces by underscores, check for duplicates, if yes use actual username or even discriminator
}
public static String getName(Member m) {
return m.getEffectiveName()+(m.getNickname() == null ? "":" aka "+m.getUser().getName())+" #"+m.getUser().getDiscriminator();
}

View File

@ -1,7 +1,7 @@
package commands;
import chat.Bot;
import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public abstract class Command {

View File

@ -2,17 +2,20 @@ package commands;
import appguru.Main;
import chat.Bot;
import net.dv8tion.jda.core.EmbedBuilder;
import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class StatusCommand extends Command {
@Override
public int getMinArgs() {
return 0;
}
@Override
public int getMaxArgs() {
return 0;
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>MinetestChatBridgeIRCBot</name>
<comment>Project MinetestChatBridgeIRCBot created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1))
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,25 @@
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'application'
description = 'Minetest Chat Bridge IRC Bot'
mainClassName = 'appguru.Main'
repositories {
jcenter()
}
dependencies {
testCompile 'junit:junit:4.12'
}
jar {
manifest {
attributes(
'Created-By': "Gradle ${gradle.gradleVersion}",
'Main-Class': "appguru.Main"
)
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
rootProject.name = 'MinetestChatBridgeIRCBot'

View File

@ -1,6 +1,8 @@
package appguru;
import bridge.FileBridge;
import bridge.ProcessBridge;
import bridge.SocketBridge;
import commands.Command;
import commands.InfoCommand;
import handlers.NumericTimeoutResponseHandler;
@ -24,11 +26,13 @@ public class Main {
public static String IRC_PREFIX="?";
public static String PREFIX="!";
public static long PING_WAIT=20000; //20s
public static long PING_WAIT=5000; //5s
public static PrintStream OUT = System.out;
public static IRCBot chat_bridge;
public static IRCBot CHAT_BRIDGE;
public static ProcessBridge PROCESS_BRIDGE;
public static void main(String[] args) throws IOException {
String project_url="https://github.com/appgurueu/adv_chat";
@ -72,20 +76,23 @@ public class Main {
String nickname=args[3];
String channelname=args[4];
File in=new File(args[5]);
File out=new File(args[6]);
if (!in.isFile() || !out.isFile() || !in.canWrite() || !in.canRead() || !out.canWrite() || !out.canRead()) {
OUT.println("ERR: Input or output files do not exist or can't be read/written.");
System.exit(0);
if (args[5].length() == 0) {
int socket_port=Integer.parseInt(args[6]);
PROCESS_BRIDGE=new SocketBridge("localhost", socket_port);
} else {
File in=new File(args[5]);
File out=new File(args[6]);
if (!in.isFile() || !out.isFile() || !in.canWrite() || !in.canRead() || !out.canWrite() || !out.canRead()) {
OUT.println("ERR: Input or output files do not exist or can't be read/written.");
System.exit(0);
}
PROCESS_BRIDGE=new FileBridge(in, out);
}
/* Open Process Bridge */
ProcessBridge pb=new ProcessBridge(in, out);
/* Create IRC Bot */
chat_bridge=new IRCBot(port, network, ssl.equals("true"));
CHAT_BRIDGE=new IRCBot(port, network, ssl.equals("true"));
chat_bridge.commands.put("PRIVMSG", (bot, tags, source, params) -> {
CHAT_BRIDGE.commands.put("PRIVMSG", (bot, tags, source, params) -> {
if (source == null || params.size() < 2) {
return;
}
@ -130,13 +137,13 @@ public class Main {
mentionstring+=",irc";
}
}
pb.write((params.get(0).startsWith("#") ? "[CGM]":"[GMS]")+nick+" "+mentionstring+" "+msg_content);
PROCESS_BRIDGE.write((params.get(0).startsWith("#") ? "[CGM]":"[GMS]")+nick+" "+mentionstring+" "+msg_content);
} else {
String command="PRIVMSG "+nick+" :No message given. Use '@mentions message'.";
try {
chat_bridge.send(command, new TryAgainHandler(command));
CHAT_BRIDGE.send(command, new TryAgainHandler(command));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
return;
@ -146,12 +153,12 @@ public class Main {
if (commandname_and_params[0].length() == 0) {
String reply="PRIVMSG "+nick+" :No commandname given !";
try {
chat_bridge.send(reply, new TryAgainHandler(command));
CHAT_BRIDGE.send(reply, new TryAgainHandler(command));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
} else {
pb.write("[CMD]"+nick+" "+String.join(" ", commandname_and_params)+(commandname_and_params.length == 1 ? " ":""));
PROCESS_BRIDGE.write("[CMD]"+nick+" "+String.join(" ", commandname_and_params)+(commandname_and_params.length == 1 ? " ":""));
}
return;
} else if (params.get(1).startsWith(Main.IRC_PREFIX)) {
@ -180,13 +187,13 @@ public class Main {
return;
}
if (params.get(0).startsWith("#")) {
pb.write("[MSG]"+nick+" "+params.get(1));
PROCESS_BRIDGE.write("[MSG]"+nick+" "+params.get(1));
} else {
bot.sendTryAgain("PRIVMSG "+nick+" :I can only deliver your message if you use '@mentions'.");
}
});
chat_bridge.commands.put("JOIN", (bot, tags, source, params) -> {
CHAT_BRIDGE.commands.put("JOIN", (bot, tags, source, params) -> {
if (source == null || params.isEmpty()) {
return;
}
@ -200,42 +207,42 @@ public class Main {
for (byte b=0; b < colorstring.length()-6; b++) {
colorstring="0"+colorstring;
}
pb.write("[JOI]"+nick+" #"+colorstring+" "+channelname);
PROCESS_BRIDGE.write("[JOI]"+nick+" #"+colorstring+" "+channelname);
});
chat_bridge.commands.put("NICK", (bot, tags, source, params) -> {
CHAT_BRIDGE.commands.put("NICK", (bot, tags, source, params) -> {
if (source == null || params.isEmpty()) {
return;
}
int indexOf=source.indexOf('!');
String nick=indexOf >= 0 ? source.substring(0, indexOf):source;
pb.write("[NCK]"+nick+" "+params.get(0));
PROCESS_BRIDGE.write("[NCK]"+nick+" "+params.get(0));
});
chat_bridge.commands.put("QUIT", (bot, tags, source, params) -> {
CHAT_BRIDGE.commands.put("QUIT", (bot, tags, source, params) -> {
if (source == null || params.isEmpty()) {
return;
}
int indexOf=source.indexOf('!');
String nick=indexOf >= 0 ? source.substring(0, indexOf):source;
pb.write("[EXT]"+nick+" "+(params.size() >= 2 ? params.get(1):"no reason"));
PROCESS_BRIDGE.write("[EXT]"+nick+" "+(params.size() >= 2 ? params.get(1):"no reason"));
});
chat_bridge.commands.put("PART", (bot, tags, source, params) -> {
CHAT_BRIDGE.commands.put("PART", (bot, tags, source, params) -> {
if (source == null || params.isEmpty()) {
return;
}
int indexOf=source.indexOf('!');
String nick=indexOf >= 0 ? source.substring(0, indexOf):source;
pb.write("[BYE]"+nick+" "+(params.size() >= 2 ? params.get(1):"no reason"));
PROCESS_BRIDGE.write("[BYE]"+nick+" "+(params.size() >= 2 ? params.get(1):"no reason"));
});
// TODO Probably Ident & SASL negotiation ?
//chat_bridge.send("CAP LS 302");
chat_bridge.send("NICK "+nickname);
chat_bridge.send("USER Minetest null null :Minetest Chat Bridge"); // 0 *
CHAT_BRIDGE.send("NICK "+nickname);
CHAT_BRIDGE.send("USER Minetest null null :Minetest Chat Bridge"); // 0 *
chat_bridge.send("JOIN "+channelname, new NumericTimeoutResponseHandler(20000) {
CHAT_BRIDGE.send("JOIN "+channelname, new NumericTimeoutResponseHandler(20000) {
@Override
public HandledResponse handleNumeric(IRCBot bot, Numeric num, List<String> params) {
switch (num) {
@ -256,7 +263,7 @@ public class Main {
for (byte b=0; b < colorstring.length()-6; b++) {
colorstring="0"+colorstring;
}
pb.write("[JOI]"+nick+" #"+colorstring+" "+channelname);
PROCESS_BRIDGE.write("[JOI]"+nick+" #"+colorstring+" "+channelname);
}
return HandledResponse.BREAK;
case RPL_ENDOFNAMES:
@ -282,7 +289,7 @@ public class Main {
};
}
};
chat_bridge.chatcommands.put("help", help_command);
CHAT_BRIDGE.chatcommands.put("help", help_command);
InfoCommand about_command=new InfoCommand() {
@Override
@ -292,7 +299,7 @@ public class Main {
};
}
};
chat_bridge.chatcommands.put("about", about_command);
CHAT_BRIDGE.chatcommands.put("about", about_command);
InfoCommand status_command=new InfoCommand() {
@Override
@ -308,31 +315,31 @@ public class Main {
};
}
};
chat_bridge.chatcommands.put("status", status_command);
CHAT_BRIDGE.chatcommands.put("status", status_command);
OUT.println("INFO: Starting client");
Main.STARTED_AT=System.currentTimeMillis();
chat_bridge.listen();
CHAT_BRIDGE.listen();
OUT.println("INFO: Starting server");
pb.serve();
PROCESS_BRIDGE.serve();
OUT.println("INFO: Starting listener");
pb.listen(line -> {
PROCESS_BRIDGE.listen(line -> {
if (line.startsWith("[MSG]")) {
try {
String command="PRIVMSG #mtchatbridgetest :"+line.substring(5);
chat_bridge.send(command, new TryAgainHandler(command));
CHAT_BRIDGE.send(command, new TryAgainHandler(command));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
} else if (line.startsWith("[PMS]")) { // GMS = PMS with comma separated list of targets
String line_content=line.substring(5);
String[] parts=line_content.split(" ", 2);
String command="PRIVMSG "+parts[0]+" :"+parts[1];
try {
chat_bridge.send(command, new TryAgainHandler(command));
CHAT_BRIDGE.send(command, new TryAgainHandler(command));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
});

View File

@ -0,0 +1 @@
/home/lars/.minetest/mods/adv_chat/MinetestChatBridgeBot/src/main/java/bridge/FileBridge.java

View File

@ -0,0 +1,25 @@
package bridge;
import appguru.Main;
import java.io.File;
import java.io.IOException;
/**
*
* @author lars
*/
public class IRCFileBridge extends FileBridge {
public IRCFileBridge(File in, File out) throws IOException {
super(in, out);
}
@Override
public void kill(String reason) {
super.kill(reason);
Main.CHAT_BRIDGE.shutdown(reason);
System.exit(0);
}
}

View File

@ -0,0 +1,23 @@
package bridge;
import appguru.Main;
import java.io.IOException;
/**
*
* @author lars
*/
public class IRCSocketBridge extends SocketBridge {
public IRCSocketBridge(String host, int port) throws IOException {
super(host, port);
}
@Override
public void kill(String reason) {
super.kill(reason);
Main.CHAT_BRIDGE.shutdown(reason);
System.exit(0);
}
}

View File

@ -0,0 +1 @@
/home/lars/.minetest/mods/adv_chat/MinetestChatBridgeBot/src/main/java/bridge/ProcessBridge.java

View File

@ -0,0 +1 @@
/home/lars/.minetest/mods/adv_chat/MinetestChatBridgeBot/src/main/java/bridge/SocketBridge.java

View File

@ -10,6 +10,8 @@ import java.net.Socket;
import java.util.*;
import static irc.HandledResponse.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class IRCBot {
public Map<String, commands.Command> chatcommands;
@ -39,7 +41,7 @@ public class IRCBot {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
}));
@ -158,6 +160,16 @@ public class IRCBot {
}
public void listen() {
Thread listenerThread = new Thread(() -> {
BufferedReader reader=null;
try {
reader=new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
} catch (IOException e) {
e.printStackTrace(Main.OUT);
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
Main.PROCESS_BRIDGE.kill("Socket connection lost");
}
System.exit(1);
}
while (true) {
try {
Thread.sleep(20);
@ -165,34 +177,29 @@ public class IRCBot {
return;
}
try {
String message = "";
int c = socket.getInputStream().read();
while (c >= 0) {
if (c == '\n' && message.charAt(message.length() - 1) == '\r') {
// message completed !
message = message.substring(0, message.length() - 1);
try {
processMessage(message);
} catch (InvalidMessageException e) {
e.printStackTrace();
}
message = "";
} else {
message += (char) c;
String message;
while ((message = reader.readLine()) != null) {
try {
processMessage(message);
} catch (InvalidMessageException e) {
e.printStackTrace(Main.OUT);
}
c = socket.getInputStream().read();
}
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
Main.PROCESS_BRIDGE.kill("Socket connection lost");
}
}
}
});
listenerThread.start();
}
public void send(String message) throws IOException {
for (int i = 0; i < message.length(); i++) {
socket.getOutputStream().write(message.charAt(i));
if (socket.isClosed() || socket.isOutputShutdown()) {
Main.PROCESS_BRIDGE.kill("Socket connection lost");
}
socket.getOutputStream().write(message.getBytes("UTF-8"));
socket.getOutputStream().write('\r');
socket.getOutputStream().write('\n');
}
@ -204,7 +211,21 @@ public class IRCBot {
try {
send(command, new TryAgainHandler(command));
} catch (IOException e) {
e.printStackTrace();
e.printStackTrace(Main.OUT);
}
}
public void shutdown(String reason) {
try {
this.send("QUIT :" + reason);
} catch (IOException e) {
e.printStackTrace(Main.OUT);
}
if (!this.socket.isClosed()) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace(Main.OUT);
}
}
}
}

View File

@ -1,9 +1,9 @@
# Advanced Chat(`adv_chat`)
# Advanced Chat (`adv_chat`)
> One Mod to rule them all, One Mod to find them,
> One Mod to bring them all, and in the darkness bind them
\- adapted quote from "Lord of the Rings"
With "all" other chat mods are meant.
Adds roles, colors, unicode, hud notifications, and chat bridges (IRC & discord).
@ -15,26 +15,56 @@ Depends on [`modlib`](https://github.com/appgurueu/modlib). Modlib has been upda
Code licensed under the GPLv3 (GNU Public License Version 3). Written by Lars Mueller alias LMD or appguru(eu).
## Terminology
## Links
Chatter : Participant in chat, be it a Minetest player, IRC user, or Discord member
Role : "Group" of chatters
Targets/Mentions : Roles or chatters mentioned using `@`
* [GitHub](https://github.com/appgurueu/voxelizer) - sources, issue tracking, contributing
* [Discord](https://discord.gg/ysP74by) - discussion, chatting
* [Minetest Forum](https://forum.minetest.net/viewtopic.php?f=9&t=22845) - (more organized) discussion
* [ContentDB](https://content.minetest.net/packages/LMD/voxelizer/) - releases (downloading from GitHub is recommended)
## Setup
In order to properly use `adv_chat`, you'll have to meet the following prerequisites:
* `modlib` Minetest mod installed and enabled as hard dependency and additionally also `cmdlib` (recommended)
* `adv_chat` needs to be installed, enabled and added to the trusted mods in settings/`minetest.conf`
* [LuaSocket](https://luarocks.org/modules/luasocket/luasocket) should be installed (`sudo luarocks install luasocket` on Ubuntu)
* Complete [Java](https://www.java.com/de/) 8 or ideally newer installation under your system path (accessible from terminal via `java`)
Then just install it like any other mod and enjoy your greatly improved chat experience!
## Terminology
Chatter: Participant in chat, be it a Minetest player, IRC user, or Discord member
Role: "Group" of chatters
Targets/Mentions: Roles or chatters mentioned using `@`
## Features
* Discord & IRC chat bridges
* Discord & IRC chat bridges, login & commands
* Blocking
* Colorization
* Style preservation
* Unicode
* Mentions
* HUD channels/notifications
* Scheduled messages for offline players
## Changes
### 🎃 Halloween Update
* Proper formatting support
* More configuration options
* Remote login for chatcommand execution
* Many under-the-hood changes cleaning up stuff & fixing bugs (improving the code & architecture)
* See `config_help.md` and the sources for all details
## API
### HUD notifications
See the `hud_channels.lua` for how it works and `test.lua` for a score change demo running with random values.
See `hud_channels.lua` for how it works and `test.lua` for a score change demo running with random values.
### Votes
@ -58,14 +88,14 @@ See the code and `config_help.md`. Feel free to contact me.
### Unicode support
This mod adds unicode support. Simply use the unicode codepoint in hexadecimal format prefixed by `U+`. To get a "slight smile" (🙂), you'd use `U+1F643`. Note that not all fonts fully support Unicode.
This mod adds unicode support. Simply use the unicode codepoint in hexadecimal format prefixed by `U+`. To get a "slight smile" (🙂), you'd use `U+1F642`. Note that not all fonts fully support Unicode.
Use the `/chat say` command to open a text entry field to paste text.
### Real-time chat
Use `@` at the beginning to message players or roles before your message.
There are 3 special mentions : `minetest`, `irc` and `discord`.
Can be separated by comma **&** whitespace. Examples :
Can be separated by comma **&** whitespace. Examples:
* `@singleplayer hi, singleplayer !` - message `hi, singleplayer !` is sent to singleplayer
* `lol(or whitespaces) @singleplayer hi` - message is just sent in global chat
@ -103,7 +133,7 @@ Summarized, the Discord Chat Bridge works quite similar to the IRC one, with som
Making Minetest & IRC chat compatible with Discord required the introduction of restrictions to simplify and reduce confusion.
* No double nicknames on Discord. If there are double nicknames, one of them gets an appendix, which is not guaranteed to be the same each time. So better make sure this doesn't happen.
* Spaces (` `) in Discord nicknames are replaced by underscores (`_`)
* Spaces (` `) and commata (`,`) in Discord nicknames are replaced by underscores (`_`)
### Internal process bridge protocol

73
chatcommands.lua Normal file
View File

@ -0,0 +1,73 @@
minetest.original_get_player_privs = minetest.get_player_privs
function minetest.get_player_privs(playername)
if chatters[playername] then
return {chatter=true}
end
return minetest.original_get_player_privs(playername)
end
if cmd_ext then
function call_chatcommand(chatter, call)
local last_space, next_space = 1, call:find(" ")
local command_trie, command_name = cmd_ext.chatcommands
local cmd, suggestion
local total_command_name = {}
repeat
next_space = next_space or call:len()+1
command_name = call:sub(last_space, next_space-1)
table.insert(total_command_name, command_name)
local concat = table.concat(total_command_name, " ")
if bridges.command_blacklist[total_command_name] then
return false, "Command only available from Minetest."
end
total_command_name = {concat}
if command_name == "" and cmd and not cmd.params then break end
cmd, suggestion, _ = trie.search(command_trie, command_name)
if not cmd then
return false, "No such chatcommand."..((suggestion and " Did you mean \""..call:sub(0, last_space-1)..suggestion.."\" ?") or "")
elseif cmd.subcommands and not cmd.implicit_call then
command_trie = cmd.subcommands
last_space, next_space = next_space + 1, call:find(" ", next_space+1)
else
last_space = next_space + 1
break
end
until next_space == call:len()
local params = call:sub(last_space)
if cmd.privs and cmd.privs.chatter then
return cmd.func(chatter, params)
end
return cmd.func((chatters[chatter] and chatters[chatter].login) or chatter, params)
end
else
function call_chatcommand(chatter, call)
local name, params = unpack(string_ext.split(call, " ", 2))
if bridges.command_blacklist[name] then
return false, "Command only available from Minetest."
end
local command = minetest.registered_chatcommands[name]
if not command then
return false, "No such chatcommand."
end
local privs = minetest.get_player_privs(chatter)
local to_lose, to_gain = {}, {}
for priv, val in pairs(command.privs) do
if val ~= privs[val] then
table.insert((val and to_gain) or to_lose, priv)
end
end
if #to_lose ~= 0 or #to_gain ~= 0 then
if #to_lose == 0 then
return false, "Missing privileges : "..table.concat(to_gain, ", ")
end
if #to_gain == 0 then
return false, "Privileges to be lost : "..table.concat(to_lose, ", ")
end
return false, "Missing privileges : "..table.concat(to_gain, ", ")..", privileges to be lost : "..table.concat(to_lose, ", ")
end
if cmd.privs.chatter then
return cmd.func(chatter, params)
end
return command.func((chatters[chatter] and chatters[chatter].login) or chatter, params)
end
end

1
closest_color.lua Symbolic link
View File

@ -0,0 +1 @@
/home/lars/.minetest/mods/voxelizer/closest_color.lua

View File

@ -1,35 +1,39 @@
-- Converts "#XXXXXX" color codes to colors
function colorize_message(message)
local rope={}
--IFNDEF bridge
--IFNDEF discord
local otherrope={}
--ENDIF
local last_index=1
for i=1, string.len(message) do
local c=string.byte(message:sub(i,i))
if c == hashtag then
local i=1
while i <= message:len() do
local c=message:sub(i,i)
if c == string.char(0x1b) and message:sub(i+1, i+4) == "(c@#" and message:sub(i+11, i+11) == ")" then
table.insert(rope, message:sub(i, i+11))
i=i+11
goto continue
elseif c == "#" then
for j=i+1, i+6 do
local c2=string.byte(string.upper(message:sub(j,j)))
if c2:len() == 0 or not string_ext.is_hexadecimal(c2) then
i=j
local c2=message:sub(j,j):upper()
if c2=="" or not ((c2 >= "0" and c2 <= "9") or (c2 >= "A" and c2 <= "F")) then
goto nocolor
end
end
local colorstring=minetest.get_color_escape_sequence(string.sub(message, i, i+6))
table.insert(rope, message:sub(last_index, i-1))
--IFNDEF bridge
table.insert(otherrope, message:sub(last_index, i-1))
--ENDIF
table.insert(rope, colorstring)
last_index=i+7
::nocolor::
table.insert(rope, minetest.get_color_escape_sequence(message:sub(i, i+6)))
i=i+6
goto continue
end
::nocolor::
table.insert(rope, c)
--IFNDEF discord
table.insert(otherrope, c)
--ENDIF
::continue::
i=i+1
end
table.insert(rope, message:sub(last_index))
--IFNDEF bridge
table.insert(otherrope, message:sub(last_index))
return table.concat(rope)
--IFNDEF discord
, table.concat(otherrope)
--ENDIF
return table.concat(rope, "")
--IFNDEF bridge
, table.concat(otherrope, "")
--ENDIF
end
end
load_schemes()

View File

@ -1,25 +1,36 @@
local schemedef={mention_prefix={type="string"},
mention_delim={type="string"},
delim={type="string"}}
local schemedef={
message_prefix={type="string"},
mention_prefix={type="string"},
mention_delim={type="string"},
content_prefix={type="string"},
message_suffix={type="string"},
}
local conf_spec={type="table", children={
scheme={type="table", required_children={
schemes={type="table", required_children={
minetest=schemedef
}, possible_children={
other=schemedef
irc=schemedef,
discord=schemedef
}},
bridges={
type="table",
possible_children={
irc={type="table", children={
port={type="number", range={0, 65535}},
network={type="string"},
nickname={type="string"},
channelname={type="string"},
ssl={type="boolean"},
prefix={type="string"},
minetest_prefix={type="string"}
}},
irc={type="table", required_children={
port={type="number", range={0, 65535}},
network={type="string"},
nickname={type="string"},
channelname={type="string"},
ssl={type="boolean"},
prefix={type="string"},
minetest_prefix={type="string"}
},
possible_children={
bridge={type="string", possible_values={"files", "sockets"}},
convert_minetest_colors={type="string", possible_values={"disabled", "hex", "safer", "safest"}},
handle_discord_markdown={type="boolean"},
handle_minetest_markdown={type="boolean"}
}},
discord={type="table", required_children={
token={type="string"},
channelname={type="string"},
@ -29,9 +40,15 @@ local conf_spec={type="table", children={
possible_children={
blacklist={type="table", keys={type="string"}},
whitelist={type="table", keys={type="string"}},
guild_id={type="string"}
guild_id={type="string"},
bridge={type="string", possible_values={"files", "sockets"}},
convert_internal_markdown={type="boolean"},
convert_minetest_markdown={type="boolean"},
handle_irc_styles={type="string", possible_values={"escape_markdown", "convert", "disabled"}}
}
}
},
command_blacklist={type="table", keys={type="number"}, values={type="string"}},
command_whitelist={type="table", keys={type="number"}, values={type="string"}}
}
}
}}
@ -39,6 +56,30 @@ local conf_spec={type="table", children={
local config=conf.import("adv_chat", conf_spec)
table_ext.add_all(getfenv(1), config)
function load_schemes()
for k, v in pairs(schemes.minetest) do
schemes.minetest[k] = colorize_message(v)
end
for _,s in pairs({"irc", "discord"}) do
if not schemes[s] then
schemes[s] = {}
for k, v in pairs(schemes.minetest) do
schemes[s][k] = minetest.strip_colors(v)
end
end
end
load_schemes = nil
end
if not bridges.irc.style_conversion then
bridges.irc.style_conversion={}
if not bridges.irc.style_conversion.colors then
bridges.irc.style_conversion.colors="disabled"
end
end
if bridges.discord then
local blacklist_empty=table_ext.is_empty(bridges.discord.blacklist or {})
@ -58,4 +99,23 @@ if bridges.discord then
end
end
end
if bridges.discord or bridges.irc then
bridges.command_blacklist = table_ext.set(bridges.command_blacklist or {})
bridges.command_whitelist = table_ext.set(bridges.command_whitelist or {})
local blacklist_empty=table_ext.is_empty(bridges.command_blacklist)
local whitelist_empty=table_ext.is_empty(bridges.command_whitelist or {})
if blacklist_empty then
if not whitelist_empty then
bridges.command_blacklist=setmetatable(bridges.command_blacklist, {__index=function(value)
if bridges.command_whitelist[value] then
return nil
end
return true
end})
end
end
end

View File

@ -11,14 +11,15 @@ Explaining document(this, Markdown) : `<modpath/gamepath>/adv_chat/config_help.m
Readme : `<modpath/gamepath>/adv_chat/Readme.md`
## Default Configuration
Located under `<modpath/gamepath>/adv_chat/default_config.json`
```json
{
"scheme" : {
"minetest" : {"mention_prefix":"#FFFF00@", "mention_delim":"#FFFF00, ", "delim":"#FFFF00 : "},
"schemes" : {
"minetest" : {"message_prefix": "", "mention_prefix": "#FFFF00@", "mention_delim": "#FFFF00, ", "content_prefix": "#FFFF00: #FFFFFF"},
"other" : null
},
"bridges" : {
"discord" : null,
"irc" : null
@ -27,13 +28,13 @@ Located under `<modpath/gamepath>/adv_chat/default_config.json`
```
## Example Configuration
```json
{
"scheme" : {
"minetest" : {"mention_prefix":"#FFFF00@", "mention_delim":"#FFFF00, ", "delim":"#FFFF00 : "},
"schemes" : {
"minetest" : {"message_prefix": "Somebody - namely ", "mention_prefix": "#FFFF00 - wrote to ", "mention_delim": "#FFFF00 and ", "content_prefix": "#FFFF00: #FFFFFF", "message_suffix": " :D"},
"other" : null
},
"bridges" : {
"discord" : {"channelname":"allgemein", "prefix": "?", "minetest_prefix": "!","token":"S.U.Pxxs.E.R.T.9998OKEN", "blacklist":{"~~new_role~~":true}, "guild_id": 580416319703351296},
"irc" : {"channelname":"#mtchatbridgetest", "prefix": "?", "minetest_prefix": "!", "nickname": "MT_Chat_Bridge", "network": "irc.freenode.net", "port": 7000, "ssl": true}
@ -44,28 +45,40 @@ Located under `<modpath/gamepath>/adv_chat/default_config.json`
## Usage
### `scheme`
### `schemes`
Specifies the chat message format, `minetest` is for the one used on the Minetest chat, and `other` is for Discord/IRC.
Specifies the chat message format, `minetest` is for the one used on the Minetest chat, `irc` is IRC, and `discord` for Discord.
* `message_prefix` - Prefix for the message
* `mention_prefix` - Prefix for mentionpart.
* `mention_delim` - Mention delimiter.
* `delim` - Message/sendername delimiter.
If you want to use color escape sequences, type `\x1B(c@#66FF00)`, and replace `#66FF00` with your color of choice in hex format.
* `content_prefix` - Message/sendername delimiter.
* `message_suffix` - Suffix for the message
Messages are formatted as `sendername + mention_prefix + {mentions, mention_delim} + delim + message`
If you want to use color escape sequences, type something like `#66FF00 colorized text here`, and replace `#66FF00` with your color of choice in hex format.
Messages are formatted as `message_prefix + sendername + mention_prefix + {mentions, mention_delim} + delim + message + message_suffix`
### `bridges`
Configuration for IRC/Discord chat bridges. If `irc` or `discord` are set to `false` or `null`, the corresponding chat bridges aren't created.
#### `discord`
Table with the following entries :
* `token` : Discord bot token, required
* `channelname` : Name of bridge channel, required as well
* `prefix`, `minetest_prefix` : Prefixes for Discord/Minetest commands, required
* `role_blacklist`/`role_whitelist` : Blacklist/whitelist of Discord roles. If both or none are set, Discord roles are ignored.
* `guild_id` : Guild ID, string. If swines add your bot to other servers, force it to use the server with the specified Guild ID. Optional. If unset, bot will use the guild it joined first.
Example :
Table with the following entries :
* `token`: Discord bot token, required
* `channelname`: Name of bridge channel, required as well
* `prefix`, `minetest_prefix`: Prefixes for Discord/Minetest commands, required
* `role_blacklist`/`role_whitelist`: Blacklist/whitelist of Discord roles. If both or none are set, Discord roles are ignored.
* `guild_id`: Guild ID, string. If swines add your bot to other servers, force it to use the server with the specified Guild ID. Optional. If unset, bot will use the guild it joined first.
* `bridge`: Optional. Forces type of process bridge to use. Choices are `"file"` and `"socket"`. Sockets are recommended but require `luasocket`.
* `convert_internal_markdown`/`convert_minetest_markdown`: Optional boolean. Whether Markdown sent from Minetest/internal chat messages should be left untouched as if it was Discord Markdown
* `handle_irc_styles`: Optional string. How IRC styles should be converted to Discord Markdown. Possible values: `"disabled"`, `"escape_markdown"` and `"convert"`
* `strip_discord_markdown_in_minetest`: Optional boolean. Whether Discord Markdown should be stripped from Minetest chat.
Example :
```json
{
"discord": {
@ -80,22 +93,33 @@ Example :
```
#### `irc`
Table with fields (all required) :
* `network` : IRC network, for example `irc.freenode.net`
* `port` : Port, on [Freenode](https://freenode.net/kb/answer/chat) it would be `7000` if SSL is used, or else `6667`. Just google "connecting to network" for your IRC network of choice to get detailed information.
* `ssl` : Whether to use encryption (SSL) to communicate with the IRC network. Setting this to `true` is recommended.
* `nickname` : Bot nickname
* `channelname` : IRC channel name, for example `#minetest-server`
* `prefix`, `minetest_prefix` : Prefixes for IRC/Minetest commands, required
Example :
Table with fields. Required are:
* `network`: IRC network, for example `irc.freenode.net`
* `port`: Port, on [Freenode](https://freenode.net/kb/answer/chat) it would be `7000` if SSL is used, or else `6667`. Just google "connecting to network" for your IRC network of choice to get detailed information.
* `ssl`: Whether to use encryption (SSL) to communicate with the IRC network. Setting this to `true` is recommended.
* `nickname`: Bot nickname
* `channelname`: IRC channel name, for example `#minetest-server`
* `prefix`, `minetest_prefix`: Prefixes for IRC bot/Minetest chatcommands, required
Optional fields are:
* `bridge`: Type of process bridge to use can be forced here. Choices are `"file"` and `"socket"`. Sockets are recommended but require `luasocket`.
* `convert_minetest_colors`: How colors from Minetest chat messages should be converted to IRC. Possible values are `"disabled"`, `"safest"`, `"safe"` and `"hex_safe"` and `"hex"`
* `handle_internal_markdown`: How Markdown sent from internal MT should be converted to IRC text styles. Possible values are `"disabled"`, `"strip"` and `"convert"`
* `handle_minetest_markdown`: How Markdown sent from Minetest should be converted to IRC text styles. Possible values are `"disabled"`, `"strip"` and `"convert"`
* `handle_discord_markdown`: How Markdown sent from Discord should be converted to IRC text styles. Possible values are `"disabled"`, `"strip"` and `"convert"`
Example:
```json
{
"irc": {
"prefix": "?",
"minetest_prefix": "!",
"channelname": "#minetest-server",
"nickname": "SERVERNAME_Chat",
"channelname": "#minetest-server",
"nickname": "SERVERNAME_Chat",
"port": 7000,
"ssl": true,
"network": "irc.freenode.net"
@ -103,21 +127,29 @@ Example :
}
```
#### `chatcommand_whitelist`/`chatcommand_blacklist`
Whitelist/blacklist of chatcommands which are not available from Discord or IRC. If both or none are set, all chatcommands are blacklisted.
## Recommendations
### Consistency
It is recommended to **keep consistency**. To do so, channel & chat bot names could be similar across Discord and IRC. The same goes for prefixes.
### Prefixes
You should try to keep prefixes similar and memorable, while ensuring that there are no collisions. I recommend the combination of `?` for Discord/IRC commands and `!` for Minetest commands.
Other neat combinations I have thought of are `+` and `-`, or `;` and `:`. Keep in mind that prefixes should be easy to type as well, and that others might have a different keyboard layout.
### Discord Avatar
Pixel-art Minetest skin heads always work well as avatars. For an example look you could look at my [Robby-Head](https://github.com/appgurueu/artwork/blob/master/robbyhead.png).
There are tons of skins out there and it's fairly easy to extract the faces (but make sure you don't violate the licenses when using the images).
A good starting point is [Addis Open MT-Skin Database](http://minetest.fensta.bplaced.net/). You can, however, of course also design it yourself. Just grab your favorite pixel-art program and draw a 8x8 head.
You should also make sure to scale the small image up (to at least 256x256), because else Discord scales it up "for you" which makes it lose it's sharp edges.
### Security
Only two basic hints : Always enable SSL, and don't give your bot token to anyone.
And of course make sure your server isn't hacked. Messages are sent as plain text over the file bridges.
Only two basic hints : Always enable SSL, and don't give your bot token to anyone.
And of course make sure your server isn't hacked. Messages are sent as plain text over the sockets or file bridges.

View File

@ -1,6 +1,6 @@
{
"scheme" : {
"minetest" : {"mention_prefix":"#FFFF00@", "mention_delim":"#FFFF00, ", "delim":"#FFFF00 : "},
"schemes" : {
"minetest" : {"message_prefix": "", "mention_prefix": "#FFFF00@", "mention_delim": "#FFFF00, ", "content_prefix": "#FFFF00: #FFFFFF"},
"other" : null
},

View File

@ -17,20 +17,29 @@ function delete_discord_role(linecontent)
end
end
file_ext.process_bridge_build("discord")
local bridge
if bridges.discord.bridge == "files" then
bridge = build_file_bridge("discord")
else
bridge = build_bridge("discord")
end
file_ext.process_bridge_listen("discord", function(line)
discord_bridge = bridge
bridge.listen(function(line)
local linecontent=line:sub(6)
if string_ext.starts_with(line, "[MSG]") then
local parts=string_ext.split(linecontent, " ", 2)
local src=parts[1].."[discord]"
send_to_all(src, src..scheme.other.delim..parts[2], minetest.get_color_escape_sequence(get_color(src))..src..scheme.minetest.delim..parts[2], "discord")
local adv_msg=message.new(chatters[src], nil, parts[2])
adv_msg.sent_to="discord"
send_to_all(adv_msg)
elseif string_ext.starts_with(line, "[GMS]") or string_ext.starts_with(line, "[CGM]") then -- GMS = group message or CGM = channel group message
local parts=string_ext.split(linecontent, " ",3)
local source=parts[1]
local targets=string_ext.split_without_limit(parts[2], ",")
local msg=parts[3]
local sent_to="nobody"
local sent_to
if string_ext.starts_with(line, "[CGM]") then
sent_to="discord"
end
@ -38,39 +47,29 @@ file_ext.process_bridge_listen("discord", function(line)
for _, target in ipairs(targets) do
targetset[target]=true
end
local invalid_targets, msg, mt_msg=build_message(source.."[discord]", targets, msg)
send_to_targets(source.."[discord]", table_ext.set(targets), msg, mt_msg, sent_to)
if (#invalid_targets) == 1 then
file_ext.process_bridge_write("discord", "[PMS]"..source.." The target "..invalid_targets[1].." is inexistant.")
elseif (#invalid_targets) > 1 then
file_ext.process_bridge_write("discord", "[PMS]"..source.." The targets "..table.concat(invalid_targets, ", ").." are inexistant.")
local adv_msg=message.new(chatters[source.."[discord]"], targets, msg)
adv_msg.sent_to=sent_to
message.mentionpart(adv_msg) --force check mentions
send_to_targets(adv_msg)
if (#adv_msg.invalid_mentions) == 1 then
discord_bridge.write("[PMS]#FFFFFF "..source.." The target "..adv_msg.invalid_mentions[1].." is inexistant.")
elseif (#adv_msg.invalid_mentions) > 1 then
discord_bridge.write("[PMS]#FFFFFF "..source.." The targets "..table.concat(adv_msg.invalid_mentions, ", ").." are inexistant.")
end
elseif string_ext.starts_with(line, "[CMD]") then
local parts=string_ext.split(linecontent, " ", 3)
local parts=string_ext.split(linecontent, " ", 2)
local source=parts[1]
local commandname=parts[2]
local params=parts[3]
local command=minetest.registered_chatcommands[commandname]
if command then
if not table_ext.is_empty(command.privs) then
file_ext.process_bridge_write("discord", "[ERR]"..source.." Command requires privs.")
else
local success, retval = command.func(source.."[discord]", params or "")
if success then
file_ext.process_bridge_write("discord", "[SUC]"..source.." "..(retval or "No return value."))
else
file_ext.process_bridge_write("discord", "[ERR]"..source.." "..(retval or "No return value."))
end
end
else
file_ext.process_bridge_write("discord", "[ERR]"..source.."`"+commandname+"` : No such command.")
end
local call=parts[2]
local success, retval = call_chatcommand(source.."[discord]", call)
local prefix = "[PMS]#FFFFFF "
if success then prefix = "[SUC]" elseif success == false then prefix = "[ERR]" end
discord_bridge.write(prefix..source.." "..(retval or "No return value."))
elseif string_ext.starts_with(line, "[JOI]") or string_ext.starts_with(line, "[LIS]") then
local parts=string_ext.split(linecontent, " ", 2) --nick & roles
local chatter=parts[1].."[discord]"
join(chatter, {color=parts[2], roles={}, discord=true})
if string_ext.starts_with(line, "[JOI]") then
send_to_all("", get_color(chatter)..chatter..minetest.get_color_escape_sequence("#FFFFFF").." joined.")
minetest.chat_send_all(get_color(chatter)..chatter..minetest.get_color_escape_sequence("#FFFFFF").." joined.")
end
elseif string_ext.starts_with(line, "[EXT]") then
chatters[linecontent.."[discord]"]=nil
@ -134,24 +133,24 @@ end)
-- Pinging
mt_ext.register_globalstep(1, function()
file_ext.process_bridge_write("discord", "[PIN]")
bridge.write("[PIN]")
end)
-- Killing on_shutdown
minetest.register_on_shutdown(function()
file_ext.process_bridge_write("discord", "[KIL]")
bridge.write("[KIL]")
end)
file_ext.process_bridge_serve("discord")
bridge.serve()
-- Start AFTER mods are loaded, so that the player sees chat messages
minetest.register_on_mods_loaded(function()
local java="java"
local classpath=minetest.get_modpath("adv_chat").."/minetest-chat-bridge-bot/out/production/classes:/home/lars/.gradle/caches/modules-2/files-2.1/net.dv8tion/JDA/3.7.1_388/f534ab5132d8df986e603a404120492d4cdf815e/JDA-3.7.1_388.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/23.5-jre/e9ce4989adf6092a3dab6152860e93d989e8cf88/guava-23.5-jre.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar:/home/lars/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.25/da76ca59f6a57ee3102f8f9bd9cee742973efa8a/slf4j-api-1.7.25.jar:/home/lars/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-collections4/4.1/a4cf4688fe1c7e3a63aa636cc96d013af537768e/commons-collections4-4.1.jar:/home/lars/.gradle/caches/modules-2/files-2.1/org.json/json/20160810/aca5eb39e2a12fddd6c472b240afe9ebea3a6733/json-20160810.jar:/home/lars/.gradle/caches/modules-2/files-2.1/net.sf.trove4j/trove4j/3.0.3/42ccaf4761f0dfdfa805c9e340d99a755907e2dd/trove4j-3.0.3.jar:/home/lars/.gradle/caches/modules-2/files-2.1/club.minnced/opus-java/1.0.2/c2e69f8d9aab5eab7476df8f5558e001657009bd/opus-java-1.0.2.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.neovisionaries/nv-websocket-client/2.4/da95dda351dba317468b08f8e5575216c05102/nv-websocket-client-2.4.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/3.8.1/4d060ca3190df0eda4dc13415532a12e15ca5f11/okhttp-3.8.1.jar:/home/lars/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/2.0.0/518929596ee3249127502a8573b2e008e2d51ed3/checker-qual-2.0.0.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.0.18/5f65affce1684999e2f4024983835efc3504012e/error_prone_annotations-2.0.18.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/ed28ded51a8b1c6b112568def5f4b455e6809019/j2objc-annotations-1.1.jar:/home/lars/.gradle/caches/modules-2/files-2.1/org.codehaus.mojo/animal-sniffer-annotations/1.14/775b7e22fb10026eed3f86e8dc556dfafe35f2d5/animal-sniffer-annotations-1.14.jar:/home/lars/.gradle/caches/modules-2/files-2.1/club.minnced/opus-java-api/1.0.2/e6e5afd72b5305356ef6d3aa95e84790cd340828/opus-java-api-1.0.2.jar:/home/lars/.gradle/caches/modules-2/files-2.1/club.minnced/opus-java-natives/1.0.2/b62c0be7a49c9bf0933d003cc0418e90518db728/opus-java-natives-1.0.2.jar:/home/lars/.gradle/caches/modules-2/files-2.1/com.squareup.okio/okio/1.13.0/a9283170b7305c8d92d25aff02a6ab7e45d06cbe/okio-1.13.0.jar:/home/lars/.gradle/caches/modules-2/files-2.1/net.java.dev.jna/jna/4.4.0/cb208278274bf12ebdb56c61bd7407e6f774d65a/jna-4.4.0.jar"
local jarpath=minetest.get_modpath("adv_chat").."/MinetestChatBridgeBot/build/libs/MinetestChatBridgeBot-all.jar"
local token=bridges.discord.token or "NTc4MjM0NjM5NTc2MDcyMjEx.XPgWKA.ilzmvz-I7XTIML6Emj1jBx4ejLw"
local text_channel=bridges.discord.channelname
local prefixes='"'..bridges.discord.minetest_prefix..'" "'..bridges.discord.prefix..'"'
local guild_id=bridges.discord.guild_id.." " or ""
file_ext.process_bridge_start("discord", java..' -Dfile.encoding=UTF-8 -classpath "'..classpath..'" appguru.Main "'..token..'" "'..text_channel..'" "%s" "%s" "%s" '..prefixes.." "..guild_id.."&")
bridge.start(java..' -jar "'..jarpath..'" "'..token..'" "'..text_channel..'" "%s" "%s" "%s" '..prefixes.." "..guild_id.." &")
end)

View File

@ -8,41 +8,38 @@ local bridge_ifndefs={
irc=adv_chat.bridges.irc
}
if not bridge_ifndefs.bridge then
error("OOF")
end
extend_mod_string("adv_chat", string_ext.handle_ifndefs(file_ext.read(get_resource("adv_chat", "colorize_message.lua")), bridge_ifndefs))
if bridge_ifndefs.bridge then
adv_chat.scheme.other=adv_chat.scheme.other or {}
for k, v in pairs(adv_chat.scheme.minetest) do
local mt_msg, msg=adv_chat.colorize_message(v)
adv_chat.scheme.minetest[k]=mt_msg
if not adv_chat.scheme.other[k] then
adv_chat.scheme.other[k]=msg
end
end
else
for k, v in pairs(adv_chat.scheme.minetest) do
local mt_msg=adv_chat.colorize_message(v)
adv_chat.scheme.minetest[k]=mt_msg
end
end
extend_mod_string("adv_chat", string_ext.handle_ifndefs(file_ext.read(get_resource("adv_chat", "main.lua")), bridge_ifndefs))
-- Basic API stuff
extend_mod("adv_chat", "unicode")
extend_mod("adv_chat", "closest_color")
extend_mod("adv_chat", "trie")
extend_mod("adv_chat", "text_styles")
extend_mod("adv_chat", "message")
extend_mod("adv_chat", "hud_channels")
-- Chat bridges
if adv_chat.bridges.irc then
extend_mod("adv_chat", "irc")
end
if bridge_ifndefs.bridge then
extend_mod("adv_chat", "chatcommands")
extend_mod("adv_chat", "process_bridges")
local env = minetest.request_insecure_environment() or error("Error: adv_chat needs to be added to the trusted mods for chat bridges to work. See the Readme for more info.")
adv_chat.set_os_execute(env.os.execute)
adv_chat.set_socket(env.require("socket"))
if adv_chat.bridges.discord then
extend_mod("adv_chat", "discord")
if adv_chat.bridges.irc then
extend_mod("adv_chat", "irc")
end
if adv_chat.bridges.discord then
extend_mod("adv_chat", "discord")
end
adv_chat.build_socket_bridge = nil
adv_chat.build_file_bridge = nil
adv_chat.build_bridge = nil
end
-- Tests - don't uncomment unless you actually want to test something

94
irc.lua
View File

@ -1,102 +1,112 @@
register_role("irc",{color="#FFFF66"})
file_ext.process_bridge_build("irc")
local bridge
if bridges.discord.bridge == "files" then
bridge = build_file_bridge("irc")
else
bridge = build_bridge("irc")
end
file_ext.process_bridge_listen("irc", function(line)
irc_bridge = bridge
local color = function(chattername) return minetest.get_color_escape_sequence((chatters[chattername] and chatters[chattername].color) or "#FFFFFF") end
bridge.listen(function(line)
local linecontent=line:sub(6)
if string_ext.starts_with(line, "[MSG]") then
local parts=string_ext.split(linecontent, " ", 2)
local src=parts[1].."[irc]"
send_to_all(src, src..scheme.other.delim..parts[2], minetest.get_color_escape_sequence(get_color(src))..src..scheme.minetest.delim..parts[2], "irc")
local adv_msg=message.new(chatters[src], nil, parts[2])
adv_msg.sent_to="irc"
send_to_all(adv_msg)
--send_to_all(src, src..scheme.other.delim..parts[2], minetest.get_color_escape_sequence(get_color(src))..src..scheme.minetest.delim..parts[2], "irc")
elseif string_ext.starts_with(line, "[PMS]") then
local parts=string_ext.split(linecontent, " ", 1)
local source=parts[1]
local target=parts[2]
local msg=parts[3]
if string_ext.ends_with(target, "[discord]") then
file_ext.process_bridge_write("discord", "[PMS]"..source.." "..target.."@you : "..msg)
discord_bridge.write("[PMS]"..source.." "..target.."@you : "..msg)
else
if minetest.get_player_by_name(target_and_msg[1]) then
minetest.chat_send_player(target_and_msg[2])
end
end
elseif string_ext.starts_with(line, "[CMD]") then
local parts=string_ext.split(linecontent, " ", 3)
local parts=string_ext.split(linecontent, " ", 2)
local source=parts[1]
local commandname=parts[2]
local params=parts[3]
local command=minetest.registered_chatcommands[commandname]
if command then
if not table_ext.is_empty(command.privs) then
file_ext.process_bridge_write("irc", "[PMS]"..source.." ".."Error: Command requires privs.")
else
local success, retval = command.func(source, params)
local prefix="Unknown"
if success then prefix="Success" elseif success ~= nil then prefix="Error" end
file_ext.process_bridge_write("irc", "[PMS]"..source.." "..prefix.." : "..(retval or "No return value."))
end
else
file_ext.process_bridge_write("irc", "[PMS]"..source.." ".."Error: No such command.")
end
local call=parts[2]
local success, retval = call_chatcommand(source.."[irc]", call)
local prefix="Unknown"
if success then prefix="Success" elseif success ~= nil then prefix="Error" end
irc_bridge.write("[PMS]"..source.." "..prefix.." : "..(retval or "No return value."))
elseif string_ext.starts_with(line, "[GMS]") or string_ext.starts_with(line, "[CGM]") then -- GMS = group message or CGM = channel group message
local parts=string_ext.split(linecontent, " ",3)
local source=parts[1]
local targets=string_ext.split_without_limit(parts[2], ",")
local msg=parts[3]
local sent_to="nobody"
local sent_to
if string_ext.starts_with(line, "[CGM]") then
sent_to="irc"
end
targetset={}
for _, target in ipairs(targets) do
targetset[target]=true
end
local invalid_targets, msg, mt_msg=build_message(source.."[irc]", targets, msg)
send_to_targets(source.."[irc]", table_ext.set(targets), msg, mt_msg, sent_to)
if (#invalid_targets) == 1 then
file_ext.process_bridge_write("irc", "[PMS]"..source.." The target "..invalid_targets[1].." is inexistant.")
elseif (#invalid_targets) > 1 then
file_ext.process_bridge_write("irc", "[PMS]"..source.." The targets "..table.concat(invalid_targets, ", ").." are inexistant.")
--targetset={}
--for _, target in ipairs(targets) do
-- targetset[target]=true
--end
--local invalid_targets, msg, mt_msg=build_message(source.."[irc]", targets, msg)
--send_to_targets(source.."[irc]", table_ext.set(targets), msg, mt_msg, sent_to)
local adv_msg=message.new(chatters[source.."[discord]"], targets, msg)
adv_msg.sent_to=sent_to
message.mentionpart(adv_msg) --force check mentions
send_to_targets(adv_msg)
if (#adv_msg.invalid_mentions) == 1 then
irc_bridge.write("[PMS]"..source.." The target "..adv_msg.invalid_mentions[1].." is inexistant.")
elseif (#adv_msg.invalid_mentions) > 1 then
irc_bridge.write("[PMS]"..source.." The targets "..table.concat(adv_msg.invalid_mentions, ", ").." are inexistant.")
end
elseif string_ext.starts_with(line, "[JOI]") then
local parts=string_ext.split(linecontent, " ", 3) --nick & color & channel
join(parts[1].."[irc]", {color=parts[2], roles={}, irc=true})
send_to_all("", parts[1].."[irc]".." joined.", minetest.get_color_escape_sequence(parts[2])..
parts[1].."[irc]"..
minetest.get_color_escape_sequence("#FFFFFF").." joined.")
local chattername=parts[1].."[irc]"
minetest.chat_send_all(color(chattername)..
chattername..minetest.get_color_escape_sequence("#FFFFFF").." joined.",
minetest.get_color_escape_sequence(parts[2])..parts[1].."[irc]"..
minetest.get_color_escape_sequence("#FFFFFF").." joined.")
--parts[3])
elseif string_ext.starts_with(line, "[EXT]") then
local parts=string_ext.split(linecontent, " ", 2) --nick & reason
local chattername=parts[1].."[irc]"
send_to_all("", chattername.." quitted ("..parts[2]..").", minetest.get_color_escape_sequence(get_color(chattername))..
chattername..minetest.get_color_escape_sequence("#FFFFFF").." quitted ("..parts[2]..").")
minetest.chat_send_all(color(chattername)..
chattername..minetest.get_color_escape_sequence("#FFFFFF").." quitted ("..parts[2]..").", minetest.get_color_escape_sequence(get_color(chattername))..
chattername..minetest.get_color_escape_sequence("#FFFFFF").." quitted ("..parts[2]..").")
chatters[chattername]=nil
elseif string_ext.starts_with(line, "[BYE]") then
local parts=string_ext.split(linecontent, " ", 2) --nick & reason
local chattername=parts[1].."[irc]"
send_to_all("", chattername.." left ("..parts[2]..").", minetest.get_color_escape_sequence(get_color(chattername))..
minetest.chat_send_all(color(chattername)..chattername..minetest.get_color_escape_sequence("#FFFFFF").." left ("..parts[2]..").", minetest.get_color_escape_sequence(get_color(chattername))..
chattername..minetest.get_color_escape_sequence("#FFFFFF").." left ("..parts[2]..").")
chatters[chattername]=nil
elseif string_ext.starts_with(line, "[NCK]") then
local parts=string_ext.split(linecontent, " ", 2) --nick & newnick
irc_users[parts[1]]=nil
irc_users[parts[2]]=true
minetest.chat_send_all(parts[1].."[irc] is now known as "..parts[2].."[irc]")
local chattername=parts[1].."[irc]"
minetest.chat_send_all(color(chattername)..chattername..minetest.get_color_escape_sequence("#FFFFFF").." is now known as "..parts[2].."[irc]")
end
end)
-- Pinging
mt_ext.register_globalstep(1, function()
file_ext.process_bridge_write("irc", "[PIN]")
bridge.write("[PIN]")
end)
file_ext.process_bridge_serve("irc")
bridge.serve()
--"/usr/lib/jvm/jdk-11.0.1/bin/java -classpath /home/lars/IdeaProjects/minetest-chat-bridge-irc-bot/out/production/minetest-chat-bridge-irc-bot Main 7000 irc.freenode.net true MT_Chat_Bridge #mtchatbridgetest /home/lars/.minetest/worlds/world/bridges/irc/output.txt /home/lars/.minetest/worlds/world/bridges/irc/input.txt"
-- Start AFTER mods are loaded, so that the player sees chat messages
minetest.register_on_mods_loaded(function()
local java="java"
local classpath=minetest.get_modpath("adv_chat").."/minetest-chat-bridge-irc-bot/out/production/minetest-chat-bridge-irc-bot"
local classpath=minetest.get_modpath("adv_chat").."/MinetestChatBridgeIRCBot/build/classes/java/main"
local port=bridges.irc.port
local network=bridges.irc.network
local ssl=tostring(bridges.irc.ssl)
@ -104,5 +114,5 @@ minetest.register_on_mods_loaded(function()
local textchannel=bridges.irc.channelname
local prefixes='"'..bridges.discord.minetest_prefix..'" "'..bridges.discord.prefix..'"'
file_ext.process_bridge_start("irc", java..' -Dfile.encoding=UTF-8 -classpath "'..classpath..'" appguru.Main '..port..' "'..network..'" '..ssl..' "'..nick..'" "'..textchannel..'" "%s" "%s" "%s" '..prefixes..' &')
bridge.start(java..' -Dfile.encoding=UTF-8 -classpath "'..classpath..'" appguru.Main '..port..' "'..network..'" '..ssl..' "'..nick..'" "'..textchannel..'" "%s" "%s" "%s" '..prefixes..' &')
end)

159
main.lua
View File

@ -1,8 +1,3 @@
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by lars.
---
--- THIS FILE USES CUSTOM STUFF (IFNDEFS) IMPLEMENTED USING MODLIB - DON'T CHANGE THE WAY IT IS EXECUTED IN init.lua
-- TODO (planned features)
@ -17,7 +12,6 @@ player_ext.set_property_default("adv_chat.blocked",{chatters={}, roles={}})
channels={} --channelname -> definition : {hud_pos, mode, autoremove, max_messages, max_lines, wrap_chars, smartwrap}
roles={} -- Role -> players -> true
chatters={} -- Chatter -> stuff
--blocked={} -- Receiver -> what he blocks : roles and playernames, true
to_be_sent={} --Receiver -> { {sender, message, date, time} }
scheme={minetest={mention_prefix=minetest.get_color_escape_sequence("#FFFF66").."@", mention_delim=minetest.get_color_escape_sequence("#FFFF66")..", ",
delim=minetest.get_color_escape_sequence("#FFFF66").." : "..minetest.get_color_escape_sequence("#FFFFFF")},
@ -55,36 +49,36 @@ function send_to_chatter(sendername, chattername, message)
else
--IFNDEF discord
if chatters[chattername].discord then
file_ext.process_bridge_write("discord", "[PMS]"..get_color(chattername).." "..chattername.." "..message)
discord_bridge.write("[PMS]"..get_color(chattername).." "..chattername.." "..message)
end
--ENDIF
--IFNDEF irc
if chatters[chattername].irc then
file_ext.process_bridge_write("irc", "[PMS]"..chattername.." "..message)
irc_bridge.write("[PMS]"..chattername.." "..message)
end
--ENDIF
end
end
function send_to_targets(sendername, targets, message, mt_message, sent_to)
function send_to_targets(msg)
message.mentionpart(msg)
--IFNDEF bridge
local discord_mentioned, irc_mentioned=targets.discord, targets.irc
local discord_mentioned, irc_mentioned=msg.targets.discord, msg.targets.irc
--ENDIF
for target, _ in pairs(targets) do
for target, _ in pairs(msg.targets) do
if not chatters[target] then
if roles[target] then
table_ext.add_all(targets, roles[target].affected)
table_ext.add_all(msg.targets, roles[target].affected)
end
targets[target]=nil
msg.targets[target]=nil
end
end
local mt_message=mt_message or message
local discord_chatters={}
local irc_chatters={}
for chatter, _ in pairs(targets) do
for chatter, _ in pairs(msg.targets) do
if not is_blocked(chatter, sendername) then
if chatters[chatter].minetest then
minetest.chat_send_player(chatter, mt_message)
minetest.chat_send_player(chatter, message.build(msg, "minetest"))
else
--IFNDEF discord
if chatters[chatter].discord then
@ -101,25 +95,21 @@ function send_to_targets(sendername, targets, message, mt_message, sent_to)
end
--IFNDEF discord
if sent_to ~= "discord" then
if msg.sent_to ~= "discord" then
if discord_mentioned then
file_ext.process_bridge_write("discord", "[MSG]"..get_color(sendername).." "..message)
else
if #discord_chatters > 0 then
file_ext.process_bridge_write("discord", "[PMS]"..get_color(sendername).." "..table.concat(discord_chatters, ",").." "..message)
end
discord_bridge.write("[MSG]"..(msg.chatter.color).." "..message.build(msg, "discord"))
elseif #discord_chatters > 0 then
discord_bridge.write("[PMS]"..(msg.chatter.color).." "..table.concat(discord_chatters, ",").." "..message.build(msg, "discord"))
end
end
--ENDIF
--IFNDEF irc
if sent_to ~= "irc" then
if msg.sent_to ~= "irc" then
if irc_mentioned then
file_ext.process_bridge_write("irc", "[MSG]"..message)
else
if #irc_chatters > 0 then
file_ext.process_bridge_write("irc", "[PMS]"..table.concat(irc_chatters, ",").." "..message)
end
irc_bridge.write("[MSG]"..message.build(msg, "irc"))
elseif #irc_chatters > 0 then
irc_bridge.write("[PMS]"..table.concat(irc_chatters, ",").." "..message.build(msg, "irc"))
end
end
--ENDIF
@ -129,6 +119,10 @@ function join(name, def)
if not def.roles then
def.roles={}
end
if not def.name then
def.name=name
end
def.service = ((def.minetest and "minetest") or (def.irc and "irc")) or "discord"
chatters[name]=def
local to_be_received=to_be_sent[name]
if to_be_received then
@ -172,7 +166,33 @@ end
--IFNDEF bridge
minetest.original_chat_send_all=minetest.chat_send_all
minetest.chat_send_all=function(msg)
send_to_all("", minetest.strip_colors(msg), msg)
local adv_message=message.new(nil, nil, msg)
adv_message.internal=true
send_to_all(adv_message)
end
minetest.original_chat_send_player=minetest.chat_send_player
minetest.chat_send_player=function(name, msg)
local chatter=chatters[name]
if not chatter then
return
end
if chatter.minetest then
return minetest.original_chat_send_player(name, msg)
end
local adv_message=message.new(nil, nil, msg)
adv_message.internal=true
local to_be_sent=message.build(adv_message, chatter.service)
--IFNDEF irc
if chatter.irc then
irc_bridge.write("[PMS]"..chatter.name.." "..to_be_sent)
end
--ENDIF
--IFNDEF discord
if chatter.discord then
discord_bridge.write("[PMS]#FFFFFF "..chatter.name.." "..to_be_sent)
end
--ENDIF
end
--ENDIF
@ -201,6 +221,7 @@ function remove_role(player, role, expected_value)
end
end
-- deprecated, minetest-only
function get_color(chatter)
if chatters[chatter] then
return chatters[chatter].color or "#FFFFFF"
@ -208,22 +229,23 @@ function get_color(chatter)
return "#FFFFFF"
end
function send_to_all(sender, msg, mt_msg, sent_to)
function send_to_all(msg)
--IFNDEF irc
if sent_to ~= "irc" then
file_ext.process_bridge_write("irc", "[MSG]"..msg)
if msg.sent_to ~= "irc" then
irc_bridge.write("[MSG]"..message.build(msg, "irc"))
end
--ENDIF
--IFNDEF discord
if sent_to ~= "discord" then
file_ext.process_bridge_write("discord", "[MSG]"..get_color(sender).." "..msg)
if msg.sent_to ~= "discord" then
discord_bridge.write("[MSG]"..((msg.chatter and msg.chatter.color) or "#FFFFFF").." "..message.build(msg, "discord"))
end
--ENDIF
if sent_to ~= "minetest" then
local mt_msg=mt_msg or msg
if msg.sent_to ~= "minetest" then
local mt_msg
for _,player in pairs(minetest.get_connected_players()) do
local playername=player:get_player_name()
if not is_blocked(playername, sender) then
if not msg.chatter or not is_blocked(playername, msg.chatter) then
mt_msg=mt_msg or message.build(msg, "minetest")
minetest.chat_send_player(playername, mt_msg)
end
end
@ -327,13 +349,14 @@ on_chat_message=function(sender, msg)
for _, part in pairs(parts) do
table.insert(mentions, string_ext.trim(part, " "))
end
local invalid_targets, msg, mt_msg=build_message(sender, mentions, msg_content)
local adv_msg=message.new(chatters[sender], mentions, msg_content)
message.mentionpart(adv_msg)
table.insert(mentions, sender)
send_to_targets(sender, table_ext.set(mentions), msg, mt_msg, "nobody")
if (#invalid_targets) == 1 then
minetest.chat_send_player(sender, "The target "..invalid_targets[1].." is inexistant.")
elseif (#invalid_targets) > 1 then
minetest.chat_send_player(sender, "The targets "..table.concat(invalid_targets, ", ").." are inexistant.")
send_to_targets(adv_msg)--sender, table_ext.set(mentions), msg, mt_msg, "nobody")
if #adv_msg.invalid_mentions == 1 then
minetest.chat_send_player(sender, "The target "..adv_msg.invalid_mentions[1].." is inexistant.")
elseif #adv_msg.invalid_mentions > 1 then
minetest.chat_send_player(sender, "The targets "..table.concat(adv_msg.invalid_mentions, ", ").." are inexistant.")
end
else
local sender_color=get_color(sender)
@ -341,16 +364,16 @@ on_chat_message=function(sender, msg)
for _,player in pairs(minetest.get_connected_players()) do
players[player:get_player_name()]=true
end
local msg, mt_msg=parse_message(msg)
mt_msg=minetest.get_color_escape_sequence(sender_color)..sender..scheme.minetest.delim..msg
msg=sender..scheme.other.delim..msg
send_to_all(sender, msg, mt_msg)
local adv_msg=message.new(chatters[sender], mentions, msg_content)
send_to_all(adv_msg)
end
return true
end
minetest.register_on_chat_message(on_chat_message)
minetest.register_chatcommand("msg",{
local prefix = (cmd_ext and "chat ") or "chat_"
minetest.register_chatcommand(prefix.."msg",{
params = "<name> <message>",
description = "Send a message to a chatter as soon as they join",
privs={},
@ -380,7 +403,7 @@ button_exit[7,0;2,0.75;send;Send]
no_prepend[]
]]
minetest.register_chatcommand("say", {
minetest.register_chatcommand(prefix.."say", {
params="",
description="Send chat message using entry field.",
privs={discord_user=false, irc_user=false},
@ -395,7 +418,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
end)
minetest.register_chatcommand("block", {
minetest.register_chatcommand(prefix.."block", {
params = "<name> | <role>",
description = "Block messages from chatter or role",
privs={},
@ -419,7 +442,7 @@ minetest.register_chatcommand("block", {
end
})
minetest.register_chatcommand("unblock", {
minetest.register_chatcommand(prefix.."unblock", {
params = "<name> | <role>",
description = "Unblock messages from chatter or role",
privs={},
@ -441,4 +464,38 @@ minetest.register_chatcommand("unblock", {
blocked[param]=nil
return true, type..param.." was unblocked"
end
})
minetest.register_chatcommand(prefix.."login", {
params = "<name> <password>",
description = "Log in as (fake) player to execute chatcommands as them",
privs = {chatter=true},
func = function(sendername, param)
param=string_ext.trim(param)
if param:len() == 0 then
return false, "No arguments given - missing name and password."
end
local name, password = unpack(string_ext.split(param, " ", 2))
password = password or ""
local auth = minetest.get_auth_handler().get_auth(name)
if auth and minetest.check_password_entry(name, auth.password, password) then
chatters[sendername].login = name
return true, 'Logged in as "'..name..'"'
end
return false, "Wrong playername/password. : "..name..", "..password.."!="..auth.password
end
})
minetest.register_chatcommand(prefix.."logout", {
params = "",
description = "Log out from your (fake) player account",
privs = {chatter=true},
func = function(sendername, param)
if not chatters[sendername].login then
return false, "Not logged in."
end
local login = chatters[sendername].login
chatters[sendername].login = nil
return true, 'Logged out from "'..login..'"'
end
})

227
message.lua Normal file
View File

@ -0,0 +1,227 @@
-- TODO handle <https://example.com> and mentions like @<ID> or @<!ID> (modification of Discord bot needed)
message={}
function message.new(chatter, mentions, content)
return {chatter=chatter, mentions=mentions, content=content}
end
local function unicode(message)
message.unicode_content = message.unicode_content or parse_unicode(message.content)
return message.unicode_content
end
local function colorized(message)
if not message.colorized_content then
message.colorized_content, message.uncolorized_content=colorize_message(unicode(message))
end
return message.colorized_content
end
local function uncolorized(message)
if not message.uncolorized_content then
message.colorized_content, message.uncolorized_content=colorize_message(unicode(message))
end
message.uncolorized_content=minetest.strip_colors(message.uncolorized_content)
return message.uncolorized_content
end
local to = {
minetest = {
from={
internal=function(message)
return message.content
end,
minetest = colorized,
irc = function(message)
message.colorized_content = irc_to_minetest(colorized(message))
return message.colorized_content
end,
discord = uncolorized
}
},
irc = {
from={
internal=function(message)
return message.content
end,
minetest = function(message)
return colorized(message)
end,
irc = function(message)
return colorized(message)
end,
discord = function(message)
return minetest_to_irc(colorized(message))
end
}
},
discord = {
from={
internal=function(message)
return minetest.strip_colors(message.content)
end,
minetest = uncolorized,
irc = function(message)
return uncolorized(message)
end,
discord = uncolorized
}
}
}
local builders = to
builders.minetest.scheme = schemes.minetest
builders.irc.scheme = schemes.irc
builders.discord.scheme = schemes.discord
function message.mentionpart(msg)
if not msg.mentionpart then
msg.invalid_mentions={}
msg.targets={}
msg.mentionpart={}
for _, mention in ipairs(msg.mentions or {}) do
if not msg.targets[mention] then
msg.targets[mention]=true
if roles[mention] then
table.insert(msg.mentionpart,roles[mention].color)
table.insert(msg.mentionpart, mention)
elseif chatters[mention] then
table.insert(msg.mentionpart, chatters[mention].color)
table.insert(msg.mentionpart, mention)
else
table.insert(msg.invalid_mentions,mention)
end
end
end
end
end
local mentionpart_builders = {
irc=nil,
discord=function(msg)
if not msg.uncolorized_mentionpart then
msg.uncolorized_mentionpart={}
for i=2, #msg.mentionpart, 2 do
table.insert(msg.uncolorized_mentionpart,msg.mentionpart[i])
end
end
return "uncolorized_mentionpart"
end,
minetest=function(msg)
if not msg.mt_mentionpart then
msg.mt_mentionpart={}
for index, item in ipairs(msg.mentionpart) do
table.insert(msg.mt_mentionpart, ((index % 2 == 0) and item) or minetest.get_color_escape_sequence(item))
end
end
return "mt_mentionpart"
end
}
local function wrap_builder(source, goal, wrapper)
local old_builder = builders[source].from[goal]
builders[source].from[goal] = function(msg) return wrapper(old_builder(msg)) end
end
if bridges.discord then
if not bridges.discord.convert_internal_markdown then
wrap_builder("discord", "internal", escape_markdown)
end
if not bridges.discord.convert_minetest_markdown then
wrap_builder("discord", "minetest", escape_markdown)
end
if bridges.discord.handle_irc_styles == "escape_markdown" then
wrap_builder("discord", "irc", escape_markdown)
elseif bridges.discord.handle_irc_styles ~= "disabled" then
wrap_builder("discord", "irc", irc_to_markdown)
end
end
if bridges.irc then
if bridges.irc.handle_discord_markdown == "strip" then
wrap_builder("irc", "discord", strip_markdown)
elseif bridges.irc.handle_discord_markdown ~= "disabled" then
wrap_builder("irc", "discord", markdown_to_irc)
end
if bridges.irc.handle_minetest_markdown == "strip" then
wrap_builder("irc", "minetest", strip_markdown)
elseif bridges.irc.handle_discord_markdown ~= "disabled" then
wrap_builder("irc", "minetest", markdown_to_irc)
end
if bridges.irc.handle_internal_markdown == "strip" then
wrap_builder("irc", "internal", strip_markdown)
elseif bridges.irc.handle_discord_markdown ~= "disabled" then
wrap_builder("irc", "internal", markdown_to_irc)
end
if bridges.irc.convert_minetest_colors=="disabled" then
mentionpart_builders.irc=mentionpart_builders.discord
else
local old_from_minetest = builders.irc.from.minetest
builders.irc.from.minetest=function(msg) return minetest_to_irc(old_from_minetest(msg)) end
local old_from_internal = builders.irc.from.internal
builders.irc.from.internal=function(msg) return minetest_to_irc(old_from_internal(msg)) end
mentionpart_builders.irc=function(msg)
if not msg.irc_mentionpart then
msg.irc_mentionpart={}
for index, item in ipairs(msg.mentionpart) do
if index % 2 == 0 then
table.insert(msg.irc_mentionpart, item)
elseif item ~= "#FFFFFF" then
table.insert(msg.irc_mentionpart, convert_color_to_irc(item:sub(2)))
end
table.insert(msg.irc_mentionpart, ((index % 2 == 0) and item) or (item ~= "#FFFFFF" and convert_color_to_irc(item:sub(2))))
end
end
return "irc_mentionpart"
end
end
end
function message.mentionpart_target(msg, target)
local builder=mentionpart_builders[target]
message.mentionpart(msg)
local name=builder(msg)
local text = name.."_text"
if not msg[text] then
msg[text]=table.concat(msg[name], builders[target].mention_delim)
end
return msg[text]
end
function message.build(msg, target)
local build=target.."_build"
if not msg[build] then
local builder = builders[target]
if msg.internal then
msg[build]=builder.from.internal(msg)
return msg[build]
end
local conversion = builder.from[msg.chatter.service]
local content = conversion(msg)
local source = (msg.chatter.name and msg.chatter.name)
if source and msg.chatter.color then
if target=="minetest" then
source=minetest.get_color_escape_sequence(msg.chatter.color)..source
elseif target=="irc" and bridges.irc.style_conversion.color~="disabled" then
local to_escape, color=convert_color_to_irc(msg.chatter.color:sub(2))
if source:sub(1,1)==to_escape then
source=string.char(0x02)..string.char(0x02)..source
end
source=color..source
end
end
local mentions = (msg.mentions and next(msg.mentions) and builder.scheme.mention_prefix..message.mentionpart_target(msg, target)..builder.scheme.content_prefix)
if not mentions and source then source=source..builder.scheme.content_prefix end
msg[build]=builder.scheme.message_prefix..(source or "")..(mentions or "")..content..builder.scheme.message_suffix
end
return msg[build]
end

Some files were not shown because too many files have changed in this diff Show More