.
This commit is contained in:
parent
f1e9ac5ea9
commit
179f94085f
15 changed files with 167 additions and 67 deletions
|
|
@ -1,3 +1,7 @@
|
||||||
|
TODO
|
||||||
|
- separate error handling from processing
|
||||||
|
- tab completion
|
||||||
|
|
||||||
environment
|
environment
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package de.kentoj.scrow.bukkit;
|
||||||
import com.mongodb.reactivestreams.client.MongoDatabase;
|
import com.mongodb.reactivestreams.client.MongoDatabase;
|
||||||
import de.kentoj.scrow.bukkit.services.EconomyService;
|
import de.kentoj.scrow.bukkit.services.EconomyService;
|
||||||
import de.kentoj.scrow.bukkit.services.command.CommandManager;
|
import de.kentoj.scrow.bukkit.services.command.CommandManager;
|
||||||
|
import de.kentoj.scrow.bukkit.services.friends.FriendsService;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import reactor.core.scheduler.Scheduler;
|
import reactor.core.scheduler.Scheduler;
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class ScrowAPI {
|
public class ScrowAPI {
|
||||||
|
|
@ -18,11 +17,13 @@ public class ScrowAPI {
|
||||||
@Getter
|
@Getter
|
||||||
private static @Nullable MongoDatabase database;
|
private static @Nullable MongoDatabase database;
|
||||||
@Getter
|
@Getter
|
||||||
private static EconomyService economyService;
|
|
||||||
@Getter
|
|
||||||
private static CommandManager commandManager;
|
private static CommandManager commandManager;
|
||||||
@Getter
|
@Getter
|
||||||
private static Scheduler minecraftScheduler;
|
private static Scheduler minecraftScheduler;
|
||||||
|
@Getter
|
||||||
|
private static EconomyService economyService;
|
||||||
|
@Getter
|
||||||
|
private static FriendsService friendsService;
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public static void setDatabase(@Nullable MongoDatabase database) {
|
public static void setDatabase(@Nullable MongoDatabase database) {
|
||||||
|
|
@ -43,5 +44,10 @@ public class ScrowAPI {
|
||||||
public static void setMinecraftScheduler(Scheduler minecraftScheduler) {
|
public static void setMinecraftScheduler(Scheduler minecraftScheduler) {
|
||||||
ScrowAPI.minecraftScheduler = minecraftScheduler;
|
ScrowAPI.minecraftScheduler = minecraftScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public static void setFriendsService(FriendsService friendsService) {
|
||||||
|
ScrowAPI.friendsService = friendsService;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package de.kentoj.scrow.bukkit.services.command.execution;
|
package de.kentoj.scrow.bukkit.services.command.execution;
|
||||||
|
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
|
||||||
|
|
||||||
public interface CommandExecutor {
|
public interface CommandExecutor {
|
||||||
|
|
||||||
void execute(CommandContext ctx);
|
void execute(CommandContext ctx) throws CommandInvocationException;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ public class PlayerArgumentType {
|
||||||
try {
|
try {
|
||||||
player = Bukkit.getOfflinePlayer(UUID.fromString(rawInput));
|
player = Bukkit.getOfflinePlayer(UUID.fromString(rawInput));
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
throw new CommandInvocationException("Not a UUID or name of an online player");
|
throw new CommandInvocationException("Not a UUID or name of an online player -- " + rawInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return player;
|
return player;
|
||||||
|
|
@ -75,6 +75,7 @@ public class PlayerArgumentType {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkValue(CommandContext ctx, OfflinePlayer value, String rawInput) throws CommandInvocationException {
|
public void checkValue(CommandContext ctx, OfflinePlayer value, String rawInput) throws CommandInvocationException {
|
||||||
|
if (!value.hasPlayedBefore()) throw new CommandInvocationException("Player has never played before -- " + rawInput);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package de.kentoj.scrow.bukkit.services.friends;
|
||||||
|
|
||||||
|
public class FriendRequestUsageException extends RuntimeException {
|
||||||
|
|
||||||
|
public FriendRequestUsageException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FriendRequestUsageException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package de.kentoj.scrow.bukkit.services.friends;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface FriendsService {
|
||||||
|
|
||||||
|
Flux<@NotNull Friendship> getFriendships(UUID playerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param initiator id of player who wished to end the friendship
|
||||||
|
* @param other id of the player the initiator wants to end the friendship with
|
||||||
|
* @throws FriendRequestUsageException
|
||||||
|
*/
|
||||||
|
Mono<@NotNull Boolean> removeFriend(UUID initiator, UUID other);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param playerId
|
||||||
|
*/
|
||||||
|
Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId);
|
||||||
|
|
||||||
|
Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param from id of player sending the friend request
|
||||||
|
* @param to id of the player the initiator wants to befriend
|
||||||
|
* @throws FriendRequestUsageException
|
||||||
|
*/
|
||||||
|
Mono<@NotNull Friendship> sendFriendRequest(UUID from, UUID to);
|
||||||
|
}
|
||||||
|
|
@ -8,4 +8,9 @@ import java.util.UUID;
|
||||||
public interface Friendship {
|
public interface Friendship {
|
||||||
Tuple2<UUID, UUID> getPlayerIds();
|
Tuple2<UUID, UUID> getPlayerIds();
|
||||||
Instant getCreatedAt();
|
Instant getCreatedAt();
|
||||||
|
|
||||||
|
default UUID getOther(UUID self) {
|
||||||
|
final var ids = getPlayerIds();
|
||||||
|
return ids.getFirst() != self ? ids.getFirst() : ids.getSecond();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package de.kentoj.scrow.bukkit;
|
package de.kentoj.scrow.bukkit;
|
||||||
|
|
||||||
import de.kentoj.scrow.bukkit.economy.command.CoinsCommand;
|
import de.kentoj.scrow.bukkit.economy.CoinsCommand;
|
||||||
|
import de.kentoj.scrow.bukkit.friends.command.FriendCommand;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ public class CoreImplPlugin extends JavaPlugin {
|
||||||
|
|
||||||
{
|
{
|
||||||
ScrowAPI.getCommandManager().register(new CoinsCommand().create(), this);
|
ScrowAPI.getCommandManager().register(new CoinsCommand().create(), this);
|
||||||
|
ScrowAPI.getCommandManager().register(new FriendCommand().create(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -33,9 +35,4 @@ public class CoreImplPlugin extends JavaPlugin {
|
||||||
}
|
}
|
||||||
ScrowAPISurface.destroyScrowAPI();
|
ScrowAPISurface.destroyScrowAPI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class ScrowAPISurface {
|
||||||
ScrowAPI.getDatabase() != null ?
|
ScrowAPI.getDatabase() != null ?
|
||||||
new MongoEconomyService(ScrowAPI.getDatabase()) : new InMemoryEconomyService()
|
new MongoEconomyService(ScrowAPI.getDatabase()) : new InMemoryEconomyService()
|
||||||
);
|
);
|
||||||
|
// TODO friends service
|
||||||
Commands.setFactory(new CommandFactoryImpl());
|
Commands.setFactory(new CommandFactoryImpl());
|
||||||
ScrowAPI.setCommandManager(new BukkitCommandManager());
|
ScrowAPI.setCommandManager(new BukkitCommandManager());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class ArgumentReaderImpl implements ArgumentReader {
|
||||||
final var str = new StringBuilder();
|
final var str = new StringBuilder();
|
||||||
while (true) {
|
while (true) {
|
||||||
if (buf.position() >= buf.limit()) break;
|
if (buf.position() >= buf.limit()) break;
|
||||||
char c = buf.get();
|
final char c = buf.get();
|
||||||
if (c == ' ') break;
|
if (c == ' ') break;
|
||||||
str.append(c);
|
str.append(c);
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ public class ArgumentReaderImpl implements ArgumentReader {
|
||||||
var quoting = false;
|
var quoting = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (buf.position() >= buf.limit()) break;
|
if (buf.position() >= buf.limit()) break;
|
||||||
char c = buf.get();
|
final char c = buf.get();
|
||||||
if (c == '"') quoting = !quoting;
|
if (c == '"') quoting = !quoting;
|
||||||
if (c == ' ' && !quoting) break;
|
if (c == ' ' && !quoting) break;
|
||||||
str.append(c);
|
str.append(c);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
package de.kentoj.scrow.bukkit.cmds2.parsing;
|
package de.kentoj.scrow.bukkit.cmds2.parsing;
|
||||||
|
|
||||||
import de.kentoj.scrow.bukkit.cmds2.CommandContextImpl;
|
import de.kentoj.scrow.bukkit.cmds2.CommandContextImpl;
|
||||||
import de.kentoj.scrow.bukkit.services.command.literal.ArgumentReader;
|
import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
|
||||||
import de.kentoj.scrow.bukkit.services.command.execution.ArgumentData;
|
import de.kentoj.scrow.bukkit.services.command.execution.ArgumentData;
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.literal.ArgumentReader;
|
||||||
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;
|
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;
|
||||||
import de.kentoj.scrow.bukkit.services.command.literal.Literal;
|
import de.kentoj.scrow.bukkit.services.command.literal.Literal;
|
||||||
import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral;
|
import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral;
|
||||||
import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CommandProcessor {
|
public class CommandProcessor {
|
||||||
|
|
@ -22,28 +23,35 @@ public class CommandProcessor {
|
||||||
private final ArgumentReader argumentReader;
|
private final ArgumentReader argumentReader;
|
||||||
|
|
||||||
public void execute(RootLiteral rootLiteral) {
|
public void execute(RootLiteral rootLiteral) {
|
||||||
Literal lastLiteral = rootLiteral;
|
Literal literal = rootLiteral;
|
||||||
do {
|
do {
|
||||||
processArguments(lastLiteral);
|
processArguments(literal);
|
||||||
|
|
||||||
if (argumentReader.isEOF()) {
|
if (argumentReader.isEOF()) {
|
||||||
if (lastLiteral.getExecutor() == null) throw new CommandInvocationException("Incomplete command -- literal expected");
|
if (literal.getExecutor() == null) {
|
||||||
lastLiteral.getExecutor().execute(new CommandContextImpl(sender, arguments));
|
throw new CommandInvocationException("Incomplete command -- literal expected");
|
||||||
|
}
|
||||||
|
literal.getExecutor().execute(new CommandContextImpl(sender, arguments));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final var literalName = argumentReader.readWord();
|
final var literalName = argumentReader.readWord();
|
||||||
lastLiteral = lastLiteral.getLiteral(literalName);
|
final var nextLiteral = literal.getLiteral(literalName);
|
||||||
if (lastLiteral == null) throw new CommandInvocationException("Unknown literal -- " + literalName);
|
if (nextLiteral == null) {
|
||||||
|
final String validLiterals = literal.getLiterals().stream().map(Literal::getName).collect(Collectors.joining(", "));
|
||||||
|
throw new CommandInvocationException("Unknown literal -- " + literalName +
|
||||||
|
"\nValid literals: " + validLiterals);
|
||||||
|
}
|
||||||
|
literal = nextLiteral;
|
||||||
|
|
||||||
final boolean hasSenderPermission = lastLiteral.getRequiredPermission() == null || sender.hasPermission(lastLiteral.getRequiredPermission());
|
final boolean hasSenderPermission = literal.getRequiredPermission() == null || sender.hasPermission(literal.getRequiredPermission());
|
||||||
if (!hasSenderPermission) throw new CommandInvocationException("No permission");
|
if (!hasSenderPermission) throw new CommandInvocationException("No permission");
|
||||||
} while (lastLiteral.hasLiteral());
|
} while (literal.hasLiteral());
|
||||||
|
|
||||||
processArguments(lastLiteral);
|
processArguments(literal);
|
||||||
|
|
||||||
if (lastLiteral.getExecutor() == null) throw new CommandInvocationException("Incomplete command.");
|
if (literal.getExecutor() == null) throw new CommandInvocationException("Incomplete command.");
|
||||||
lastLiteral.getExecutor().execute(new CommandContextImpl(sender, arguments));
|
literal.getExecutor().execute(new CommandContextImpl(sender, arguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processArguments(Literal node) {
|
private void processArguments(Literal node) {
|
||||||
|
|
@ -55,13 +63,13 @@ public class CommandProcessor {
|
||||||
|
|
||||||
private <T> ArgumentData<T> parseArgument(CommandArgument<T> argument) {
|
private <T> ArgumentData<T> parseArgument(CommandArgument<T> argument) {
|
||||||
if (argumentReader.isEOF()) {
|
if (argumentReader.isEOF()) {
|
||||||
var defaultValue = argument.getDefaultValue(new CommandContextImpl(sender, arguments));
|
final var defaultValue = argument.getDefaultValue(new CommandContextImpl(sender, arguments));
|
||||||
if (defaultValue == null) throw new CommandInvocationException("Missing argument -- " + argument.getName());
|
if (defaultValue == null) throw new CommandInvocationException("Missing argument -- " + argument.getName());
|
||||||
return new ArgumentDataImpl<>(defaultValue, argument, null);
|
return new ArgumentDataImpl<>(defaultValue, argument, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
final var rawInput = argument.getType().readRawInput(argumentReader);
|
final var rawInput = argument.getType().readRawInput(argumentReader);
|
||||||
T value = argument.getType().parseInput(rawInput);
|
final T value = argument.getType().parseInput(rawInput);
|
||||||
argument.getType().checkValue(new CommandContextImpl(sender, arguments), value, rawInput);
|
argument.getType().checkValue(new CommandContextImpl(sender, arguments), value, rawInput);
|
||||||
return new ArgumentDataImpl<>(value, argument, rawInput);
|
return new ArgumentDataImpl<>(value, argument, rawInput);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package de.kentoj.scrow.bukkit.economy.command;
|
package de.kentoj.scrow.bukkit.economy;
|
||||||
|
|
||||||
import de.kentoj.scrow.bukkit.ScrowAPI;
|
import de.kentoj.scrow.bukkit.ScrowAPI;
|
||||||
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;
|
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package de.kentoj.scrow.bukkit.friends;
|
|
||||||
|
|
||||||
import de.kentoj.scrow.bukkit.services.friends.FriendRequest;
|
|
||||||
import de.kentoj.scrow.bukkit.services.friends.Friendship;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface FriendshipService {
|
|
||||||
|
|
||||||
Flux<@NotNull Friendship> getFriendships(UUID playerId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param initiator id of player who wished to end the friendship
|
|
||||||
* @param other id of the player the initiator wants to end the friendship with
|
|
||||||
* @return true on success, false if no such friendship existed.
|
|
||||||
*/
|
|
||||||
Mono<@NotNull Boolean> endFriendship(UUID initiator, UUID other);
|
|
||||||
|
|
||||||
Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId);
|
|
||||||
|
|
||||||
Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param initiator id of player sending the friend request
|
|
||||||
* @param to id of the player the initiator wants to befriend
|
|
||||||
* @return true on success, false if already requested
|
|
||||||
*/
|
|
||||||
Mono<@NotNull Boolean> sendFriendRequest(UUID initiator, UUID to);
|
|
||||||
|
|
||||||
Mono<@NotNull Friendship> acceptFriendRequest(UUID from, UUID to);
|
|
||||||
|
|
||||||
Mono<@NotNull Friendship> denyFriendRequest(UUID from, UUID to);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package de.kentoj.scrow.bukkit.friends.command;
|
||||||
|
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.Commands;
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral;
|
||||||
|
|
||||||
|
public class FriendCommand {
|
||||||
|
|
||||||
|
private final RootLiteral rootLiteral;
|
||||||
|
|
||||||
|
public FriendCommand() {
|
||||||
|
rootLiteral = Commands.root("friend");
|
||||||
|
rootLiteral.addAliases("f", "friends");
|
||||||
|
|
||||||
|
rootLiteral.addLiteral(new FriendListLiteral().create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootLiteral create() {
|
||||||
|
return rootLiteral;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package de.kentoj.scrow.bukkit.friends.command;
|
||||||
|
|
||||||
|
import de.kentoj.scrow.bukkit.ScrowAPI;
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.Commands;
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.execution.CommandContext;
|
||||||
|
import de.kentoj.scrow.bukkit.services.command.literal.Literal;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class FriendListLiteral {
|
||||||
|
|
||||||
|
private final Literal literal;
|
||||||
|
|
||||||
|
public FriendListLiteral() {
|
||||||
|
literal = Commands.literal("list");
|
||||||
|
literal.setExecutor(this::listFriends);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Literal create() {
|
||||||
|
return literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listFriends(CommandContext ctx) {
|
||||||
|
ScrowAPI.getFriendsService().getFriendships(ctx.getSenderPlayer().getUniqueId())
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.collectList()
|
||||||
|
.publishOn(ScrowAPI.getMinecraftScheduler())
|
||||||
|
.subscribe(friendships -> {
|
||||||
|
if (friendships.isEmpty()) {
|
||||||
|
ctx.getSender().sendMessage("You have no friends.");
|
||||||
|
} else {
|
||||||
|
ctx.getSender().sendMessage("You have " + friendships.size() + " friends:");
|
||||||
|
friendships.forEach(friendship -> {
|
||||||
|
final UUID friendId = friendship.getOther(ctx.getSenderPlayer().getUniqueId());
|
||||||
|
// TODO get name even if never online on the network
|
||||||
|
final OfflinePlayer friend = Bukkit.getOfflinePlayer(friendId);
|
||||||
|
ctx.getSender().sendMessage("- " + friend.getName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, error -> {
|
||||||
|
ctx.getSender().sendMessage("Error fetching friends");
|
||||||
|
// TODO improve handling
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue