FINISHED mcchat!
#12 #13 Vanilla commands are supported as well Now it'll send the command output to the player as well, if ran on Discord Minecraft chat sending to Discord made async Started using CraftBukkit Updated Travis config according to that
This commit is contained in:
parent
a501d9d457
commit
fd14bf1954
9 changed files with 230 additions and 57 deletions
24
.travis.yml
24
.travis.yml
|
@ -1,4 +1,26 @@
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.m2/repository/org/bukkit/craftbukkit
|
||||||
|
before_install: | # Wget BuildTools and run if cached folder not found
|
||||||
|
if [ ! -d "$HOME/.m2/repository/org/bukkit/craftbukkit/1.12-R0.1-SNAPSHOT" ]; then
|
||||||
|
wget -O BuildTools.jar https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar
|
||||||
|
# grep so that download counts don't appear in log files
|
||||||
|
- java -jar BuildTools.jar --rev 1.12 | grep -vE "[^/ ]*/[^/ ]*\s*KB\s*$" | grep -v "^\s*$"
|
||||||
|
fi
|
||||||
language: java
|
language: java
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
|
sudo: true
|
||||||
|
deploy:
|
||||||
|
# deploy develop to the staging environment
|
||||||
|
- provider: script
|
||||||
|
script: chmod +x deploy.sh && sh deploy.sh staging
|
||||||
|
on:
|
||||||
|
branch: dev
|
||||||
|
skip_cleanup: true
|
||||||
|
# deploy master to production
|
||||||
|
- provider: script
|
||||||
|
script: chmod +x deploy.sh && sh deploy.sh production
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
skip_cleanup: true
|
||||||
|
|
10
deploy.sh
Normal file
10
deploy.sh
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
FILENAME=$(find target/ -maxdepth 1 ! -name '*original*' -name '*.jar')
|
||||||
|
echo Found file: $FILENAME
|
||||||
|
|
||||||
|
if [ $1 = 'production' ]; then
|
||||||
|
echo Production mode
|
||||||
|
echo $UPLOAD_KEY > upload_key
|
||||||
|
chmod 400 upload_key
|
||||||
|
yes | scp -B -i upload_key -o StrictHostKeyChecking=no $FILENAME travis@server.figytuna.com:/minecraft/main/plugins
|
||||||
|
fi
|
6
pom.xml
6
pom.xml
|
@ -150,6 +150,12 @@
|
||||||
<version>1.12-R0.1-SNAPSHOT</version>
|
<version>1.12-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot</artifactId>
|
||||||
|
<version>1.12-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.austinv11</groupId>
|
<groupId>com.github.austinv11</groupId>
|
||||||
<artifactId>Discord4j</artifactId>
|
<artifactId>Discord4j</artifactId>
|
||||||
|
|
|
@ -3,14 +3,18 @@ package buttondevteam.discordplugin;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer;
|
import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer;
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
|
||||||
|
import lombok.Getter;
|
||||||
import sx.blah.discord.handle.obj.IChannel;
|
import sx.blah.discord.handle.obj.IChannel;
|
||||||
import sx.blah.discord.handle.obj.IUser;
|
import sx.blah.discord.handle.obj.IUser;
|
||||||
|
|
||||||
public class DiscordConnectedPlayer extends DiscordFakePlayer {
|
public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer<DiscordConnectedPlayer> {
|
||||||
private static int nextEntityId = 0;
|
private static int nextEntityId = 10000;
|
||||||
|
private @Getter VanillaCommandListener<DiscordConnectedPlayer> vanillaCmdListener;
|
||||||
|
|
||||||
public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid) {
|
public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid) {
|
||||||
super(user, channel, nextEntityId++, uuid);
|
super(user, channel, nextEntityId++, uuid);
|
||||||
|
vanillaCmdListener = new VanillaCommandListener<>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,20 +31,33 @@ import org.bukkit.potion.PotionEffectType;
|
||||||
import org.bukkit.scoreboard.Scoreboard;
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
|
||||||
|
import lombok.Getter;
|
||||||
import sx.blah.discord.handle.obj.IChannel;
|
import sx.blah.discord.handle.obj.IChannel;
|
||||||
import sx.blah.discord.handle.obj.IUser;
|
import sx.blah.discord.handle.obj.IUser;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class DiscordPlayerSender extends DiscordSenderBase implements Player {
|
public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer<DiscordPlayerSender> {
|
||||||
|
|
||||||
protected Player player;
|
protected Player player;
|
||||||
// protected @Delegate(excludes = ProjectileSource.class) Player player;
|
private @Getter VanillaCommandListener<DiscordPlayerSender> vanillaCmdListener;
|
||||||
// protected @Delegate(excludes = { ProjectileSource.class, Permissible.class }) Player player;
|
|
||||||
// protected @Delegate(excludes = { ProjectileSource.class, CommandSender.class }) Player player;
|
|
||||||
|
|
||||||
public DiscordPlayerSender(IUser user, IChannel channel, Player player) {
|
public DiscordPlayerSender(IUser user, IChannel channel, Player player) {
|
||||||
super(user, channel);
|
super(user, channel);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
vanillaCmdListener = new VanillaCommandListener<DiscordPlayerSender>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String message) {
|
||||||
|
player.sendMessage(message);
|
||||||
|
super.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String[] messages) {
|
||||||
|
player.sendMessage(messages);
|
||||||
|
super.sendMessage(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
9
src/main/java/buttondevteam/discordplugin/IMCPlayer.java
Normal file
9
src/main/java/buttondevteam/discordplugin/IMCPlayer.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package buttondevteam.discordplugin;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
|
||||||
|
|
||||||
|
public interface IMCPlayer<T extends DiscordSenderBase & IMCPlayer<T>> extends Player {
|
||||||
|
VanillaCommandListener<T> getVanillaCmdListener();
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
import buttondevteam.discordplugin.*;
|
import buttondevteam.discordplugin.*;
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
|
||||||
import buttondevteam.lib.*;
|
import buttondevteam.lib.*;
|
||||||
import buttondevteam.lib.chat.Channel;
|
import buttondevteam.lib.chat.Channel;
|
||||||
import buttondevteam.lib.chat.TBMCChatAPI;
|
import buttondevteam.lib.chat.TBMCChatAPI;
|
||||||
|
@ -36,20 +37,22 @@ public class MCChatListener implements Listener, IListener<MessageReceivedEvent>
|
||||||
return;
|
return;
|
||||||
if (e.getSender() instanceof DiscordSender || e.getSender() instanceof DiscordPlayerSender)
|
if (e.getSender() instanceof DiscordSender || e.getSender() instanceof DiscordPlayerSender)
|
||||||
return;
|
return;
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
final String authorPlayer = DiscordPlugin.sanitizeString(e.getSender() instanceof Player //
|
final String authorPlayer = DiscordPlugin.sanitizeString(e.getSender() instanceof Player //
|
||||||
? ((Player) e.getSender()).getDisplayName() //
|
? ((Player) e.getSender()).getDisplayName() //
|
||||||
: e.getSender().getName());
|
: e.getSender().getName());
|
||||||
final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer).withDescription(e.getMessage())
|
final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer)
|
||||||
.withColor(new Color(e.getChannel().color.getRed(), e.getChannel().color.getGreen(),
|
.withDescription(e.getMessage()).withColor(new Color(e.getChannel().color.getRed(),
|
||||||
e.getChannel().color.getBlue()));
|
e.getChannel().color.getGreen(), e.getChannel().color.getBlue()));
|
||||||
if (e.getSender() instanceof Player)
|
if (e.getSender() instanceof Player)
|
||||||
embed.withAuthorIcon("https://minotar.net/avatar/" + ((Player) e.getSender()).getName() + "/32.png");
|
embed.withAuthorIcon(
|
||||||
|
"https://minotar.net/avatar/" + ((Player) e.getSender()).getName() + "/32.png");
|
||||||
final long nanoTime = System.nanoTime();
|
final long nanoTime = System.nanoTime();
|
||||||
Consumer<LastMsgData> doit = lastmsgdata -> {
|
Consumer<LastMsgData> doit = lastmsgdata -> {
|
||||||
final EmbedObject embedObject = embed.build();
|
final EmbedObject embedObject = embed.build();
|
||||||
String msg = lastmsgdata.channel.isPrivate() ? DiscordPlugin.sanitizeString(e.getChannel().DisplayName)
|
String msg = lastmsgdata.channel.isPrivate()
|
||||||
: "";
|
? DiscordPlugin.sanitizeString(e.getChannel().DisplayName) : "";
|
||||||
if (lastmsgdata.message == null || lastmsgdata.message.isDeleted()
|
if (lastmsgdata.message == null || lastmsgdata.message.isDeleted()
|
||||||
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName())
|
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName())
|
||||||
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|
||||||
|
@ -69,8 +72,8 @@ public class MCChatListener implements Listener, IListener<MessageReceivedEvent>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (e.getChannel().equals(Channel.GlobalChat))
|
if (e.getChannel().equals(Channel.GlobalChat))
|
||||||
doit.accept(
|
doit.accept(lastmsgdata == null ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel)
|
||||||
lastmsgdata == null ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel) : lastmsgdata);
|
: lastmsgdata);
|
||||||
|
|
||||||
for (LastMsgData data : lastmsgPerUser) {
|
for (LastMsgData data : lastmsgPerUser) {
|
||||||
final IUser iUser = data.channel.getUsersHere().stream()
|
final IUser iUser = data.channel.getUsersHere().stream()
|
||||||
|
@ -79,7 +82,8 @@ public class MCChatListener implements Listener, IListener<MessageReceivedEvent>
|
||||||
if (user.isMinecraftChatEnabled() && e.shouldSendTo(getSender(data.channel, iUser, user)))
|
if (user.isMinecraftChatEnabled() && e.shouldSendTo(getSender(data.channel, iUser, user)))
|
||||||
doit.accept(data);
|
doit.accept(data);
|
||||||
}
|
}
|
||||||
} // TODO: Author URL
|
}
|
||||||
|
}); // TODO: Author URL
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LastMsgData {
|
private static class LastMsgData {
|
||||||
|
@ -235,7 +239,7 @@ public class MCChatListener implements Listener, IListener<MessageReceivedEvent>
|
||||||
dsender.sendMessage("Stop it. You know the answer.");
|
dsender.sendMessage("Stop it. You know the answer.");
|
||||||
lastlist = 0;
|
lastlist = 0;
|
||||||
} else
|
} else
|
||||||
Bukkit.dispatchCommand(dsender, cmd);
|
VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd);
|
||||||
lastlistp = (short) Bukkit.getOnlinePlayers().size();
|
lastlistp = (short) Bukkit.getOnlinePlayers().size();
|
||||||
} else {
|
} else {
|
||||||
if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0)
|
if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0)
|
||||||
|
@ -273,7 +277,7 @@ public class MCChatListener implements Listener, IListener<MessageReceivedEvent>
|
||||||
val key = (channel.isPrivate() ? "" : "P") + author.getStringID();
|
val key = (channel.isPrivate() ? "" : "P") + author.getStringID();
|
||||||
return Stream.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239
|
return Stream.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239
|
||||||
() -> Optional.ofNullable(OnlineSenders.get(key)), // Find first non-null
|
() -> Optional.ofNullable(OnlineSenders.get(key)), // Find first non-null
|
||||||
() -> Optional.ofNullable(ConnectedSenders.get(author.getStringID())), // This doesn't support it
|
() -> Optional.ofNullable(ConnectedSenders.get(key)), // This doesn't support the public chat, but it'll always return null for it
|
||||||
() -> Optional.ofNullable(UnconnectedSenders.get(key)), () -> {
|
() -> Optional.ofNullable(UnconnectedSenders.get(key)), () -> {
|
||||||
val dsender = new DiscordSender(author, channel);
|
val dsender = new DiscordSender(author, channel);
|
||||||
UnconnectedSenders.put(key, dsender);
|
UnconnectedSenders.put(key, dsender);
|
||||||
|
|
|
@ -11,13 +11,10 @@ import org.bukkit.entity.*;
|
||||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||||
import org.bukkit.map.MapView;
|
import org.bukkit.map.MapView;
|
||||||
import org.bukkit.permissions.PermissibleBase;
|
import org.bukkit.permissions.PermissibleBase;
|
||||||
import org.bukkit.permissions.ServerOperator;
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.scoreboard.Scoreboard;
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
import buttondevteam.discordplugin.DiscordPlugin;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.Delegate;
|
import lombok.experimental.Delegate;
|
||||||
import sx.blah.discord.handle.obj.IChannel;
|
import sx.blah.discord.handle.obj.IChannel;
|
||||||
import sx.blah.discord.handle.obj.IUser;
|
import sx.blah.discord.handle.obj.IUser;
|
||||||
|
@ -25,9 +22,7 @@ import sx.blah.discord.handle.obj.IUser;
|
||||||
public class DiscordFakePlayer extends DiscordHumanEntity implements Player {
|
public class DiscordFakePlayer extends DiscordHumanEntity implements Player {
|
||||||
protected DiscordFakePlayer(IUser user, IChannel channel, int entityId, UUID uuid) {
|
protected DiscordFakePlayer(IUser user, IChannel channel, int entityId, UUID uuid) {
|
||||||
super(user, channel, entityId, uuid);
|
super(user, channel, entityId, uuid);
|
||||||
perm = new PermissibleBase(new ServerOperator() {
|
perm = new PermissibleBase(Bukkit.getOfflinePlayer(uuid));
|
||||||
private @Getter @Setter boolean op;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delegate
|
@Delegate
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.CraftServer;
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.CraftWorld;
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper;
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordSenderBase;
|
||||||
|
import buttondevteam.discordplugin.IMCPlayer;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.val;
|
||||||
|
import net.minecraft.server.v1_12_R1.ChatMessage;
|
||||||
|
import net.minecraft.server.v1_12_R1.CommandException;
|
||||||
|
import net.minecraft.server.v1_12_R1.EnumChatFormat;
|
||||||
|
import net.minecraft.server.v1_12_R1.IChatBaseComponent;
|
||||||
|
import net.minecraft.server.v1_12_R1.ICommandListener;
|
||||||
|
import net.minecraft.server.v1_12_R1.MinecraftServer;
|
||||||
|
import net.minecraft.server.v1_12_R1.World;
|
||||||
|
|
||||||
|
public class VanillaCommandListener<T extends DiscordSenderBase & IMCPlayer<T>> implements ICommandListener {
|
||||||
|
private @Getter T player;
|
||||||
|
private Player bukkitplayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will only send raw vanilla messages to the sender in plain text.
|
||||||
|
*
|
||||||
|
* @param player
|
||||||
|
* The Discord sender player (the wrapper)
|
||||||
|
*/
|
||||||
|
public VanillaCommandListener(T player) {
|
||||||
|
this.player = player;
|
||||||
|
this.bukkitplayer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
||||||
|
*
|
||||||
|
* @param player
|
||||||
|
* The Discord sender player (the wrapper)
|
||||||
|
* @param bukkitplayer
|
||||||
|
* The Bukkit player to send the raw message to
|
||||||
|
*/
|
||||||
|
public VanillaCommandListener(T player, Player bukkitplayer) {
|
||||||
|
this.player = player;
|
||||||
|
this.bukkitplayer = bukkitplayer;
|
||||||
|
if (!(bukkitplayer instanceof CraftPlayer))
|
||||||
|
throw new ClassCastException("bukkitplayer must be a Bukkit player!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinecraftServer C_() {
|
||||||
|
return ((CraftServer) Bukkit.getServer()).getServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean a(int oplevel, String cmd) {
|
||||||
|
// return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands
|
||||||
|
return oplevel == 0 || player.isOp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return player.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public World getWorld() {
|
||||||
|
return ((CraftWorld) player.getWorld()).getHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(IChatBaseComponent arg0) {
|
||||||
|
player.sendMessage(arg0.toPlainText());
|
||||||
|
if (bukkitplayer != null)
|
||||||
|
((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) {
|
||||||
|
val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase());
|
||||||
|
if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper))
|
||||||
|
return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds
|
||||||
|
|
||||||
|
if (!(dsender instanceof IMCPlayer))
|
||||||
|
throw new ClassCastException(
|
||||||
|
"dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.");
|
||||||
|
|
||||||
|
IMCPlayer<?> sender = (IMCPlayer<?>) dsender; // Don't use val on recursive interfaces :P
|
||||||
|
|
||||||
|
val vcmd = (VanillaCommandWrapper) cmd;
|
||||||
|
if (!vcmd.testPermission(sender))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ICommandListener icommandlistener = sender.getVanillaCmdListener();
|
||||||
|
String[] args = cmdstr.split(" ");
|
||||||
|
args = Arrays.copyOfRange(args, 1, args.length);
|
||||||
|
try {
|
||||||
|
vcmd.dispatchVanillaCommand(sender, icommandlistener, args);
|
||||||
|
} catch (CommandException commandexception) {
|
||||||
|
// Taken from CommandHandler
|
||||||
|
ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.getArgs());
|
||||||
|
chatmessage.getChatModifier().setColor(EnumChatFormat.RED);
|
||||||
|
icommandlistener.sendMessage(chatmessage);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue