package com.github.otbproject.otbproject.bot.beam;
import com.github.otbproject.otbproject.App;
import com.github.otbproject.otbproject.bot.AbstractBot;
import com.github.otbproject.otbproject.bot.BotInitException;
import com.github.otbproject.otbproject.bot.BotUtil;
import com.github.otbproject.otbproject.channel.ChannelInitException;
import com.github.otbproject.otbproject.channel.ChannelNotFoundException;
import com.github.otbproject.otbproject.channel.JoinCheck;
import com.github.otbproject.otbproject.config.Account;
import com.github.otbproject.otbproject.config.BotConfig;
import com.github.otbproject.otbproject.config.Configs;
import com.github.otbproject.otbproject.util.ThreadUtil;
import net.jodah.expiringmap.ExpiringMap;
import org.apache.logging.log4j.Level;
import pro.beam.api.BeamAPI;
import pro.beam.api.resource.BeamUser;
import pro.beam.api.resource.chat.methods.ChatSendMethod;
import pro.beam.api.services.impl.UsersService;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class BeamBot extends AbstractBot {
public final ExpiringMap<String, Boolean> sentMessageCache;
private static final int CACHE_TIME = 4;
final BeamAPI beam = new BeamAPI();
BeamUser beamUser;
final ConcurrentHashMap<String, BeamChatChannel> beamChannels = new ConcurrentHashMap<>();
public BeamBot() throws BotInitException {
super();
sentMessageCache = ExpiringMap.builder()
.expiration(CACHE_TIME, TimeUnit.SECONDS)
.expirationPolicy(ExpiringMap.ExpirationPolicy.CREATED)
.build();
try {
beamUser = beam.use(UsersService.class).login(Configs.getAccount().getExactly(Account::getName), Configs.getAccount().getExactly(Account::getPasskey)).get();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
ThreadUtil.interruptIfInterruptedException(e);
throw new BotInitException("Unable to connect bot to Beam", e);
}
}
@Override
public boolean isConnected(String channelName) {
return beamChannels.containsKey(channelName);
}
@Override
public boolean isConnected() {
return true;
}
@Override
public boolean isChannel(String channelName) {
try {
return beam.use(UsersService.class).search(channelName).get().stream()
.anyMatch(user -> user.username.equalsIgnoreCase(channelName));
} catch (InterruptedException | ExecutionException e) {
ThreadUtil.interruptIfInterruptedException(e);
App.logger.catching(e);
}
return false;
}
@Override
public synchronized void shutdown() {
beamChannels.values().forEach(beamChatChannel -> beamChatChannel.beamChatConnectable.disconnect());
beamChannels.clear();
super.shutdown();
}
@Override
public String getUserName() {
return beamUser.username.toLowerCase();
}
@Override
public boolean isUserMod(String channel, String user) {
BeamChatChannel beamChatChannel = beamChannels.get(channel);
if (beamChatChannel == null) {
return false;
}
List<BeamUser.Role> list = beamChatChannel.userRoles.get(user);
return (list != null) && list.contains(BeamUser.Role.MOD);
}
@Override
public boolean isUserSubscriber(String channel, String user) {
BeamChatChannel beamChatChannel = beamChannels.get(channel);
if (beamChatChannel == null) {
return false;
}
List<BeamUser.Role> list = beamChatChannel.userRoles.get(user);
return (list != null) && list.contains(BeamUser.Role.SUBSCRIBER);
}
@Override
public void sendMessage(String channel, String message) {
beamChannels.get(channel).beamChatConnectable.send(ChatSendMethod.of(message));
sentMessageCache.put(message, Boolean.TRUE);
App.logger.info("Sent: <" + channel + "> " + message);
}
@Override
public void startBot() {
channelManager().join(getUserName(), EnumSet.of(JoinCheck.WHITELIST, JoinCheck.BLACKLIST));
Configs.getBotConfig().get(BotConfig::getCurrentChannels).forEach(channel -> channelManager().join(channel, EnumSet.of(JoinCheck.WHITELIST, JoinCheck.BLACKLIST)));
while (!beamChannels.isEmpty()) {
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
App.logger.warn("Bot thread interrupted - shutting down bot");
Thread.currentThread().interrupt();
shutdown();
break;
}
}
}
@Override
public boolean join(String channelName) {
try {
beamChannels.put(channelName, BeamChatChannel.create(channelName));
} catch (ChannelInitException ignored) {
return false;
}
return true;
}
@Override
public boolean leave(String channelName) {
BeamChatChannel channel = beamChannels.remove(channelName);
if (channel == null) {
return false;
}
channel.beamChatConnectable.disconnect();
return true;
}
@Override
public boolean ban(String channelName, String user) {
return banOrUnBan(channelName, user, true);
}
@Override
public boolean unBan(String channelName, String user) {
return banOrUnBan(channelName, user, false);
}
@Override
public boolean timeout(String channelName, String user, int timeInSeconds) {
if (timeInSeconds <= 0) {
App.logger.warn("Cannot time out user for non-positive amount of time");
return false;
}
user = user.toLowerCase(); // Just in case
// Check if user has user level mod or higher
try {
if (BotUtil.isModOrHigher(channelName, user)) {
return false;
}
} catch (ChannelNotFoundException e) {
App.logger.error("Channel '" + channelName + "' did not exist in which to timeout user");
App.logger.catching(e);
}
BeamChatChannel beamChatChannel = beamChannels.get(channelName);
// More null checks!
if (beamChatChannel == null) {
App.logger.error("Failed to timeout user: BeamChatChannel for channel '" + channelName + "' is null.");
return false;
}
ExpiringMap<String, Boolean> timeoutSet = beamChatChannel.timeoutSet;
if (timeoutSet.containsKey(user)) {
if (TimeUnit.MILLISECONDS.toSeconds(timeoutSet.getExpectedExpiration(user)) > timeInSeconds) {
App.logger.info("Did not timeout user '" + user + "' because they were already timed out for longer than that.");
return false;
} else {
timeoutSet.setExpiration(timeInSeconds, TimeUnit.SECONDS);
}
} else {
timeoutSet.put(user, Boolean.TRUE, timeInSeconds, TimeUnit.SECONDS);
beamChatChannel.deleteMessages(user);
}
App.logger.info("Timed out '" + user + "' in channel '" + channelName + "' for " + timeInSeconds + " seconds");
return true;
}
@Override
public boolean removeTimeout(String channelName, String user) {
BeamChatChannel channel = beamChannels.get(channelName);
if (channel == null) {
App.logger.error("Failed to remove timeout for user: BeamChatChannel for channel '" + channelName + "' is null.");
return false;
}
channel.timeoutSet.remove(user);
return true;
}
public boolean clearChannelCache(String channel) {
BeamChatChannel beamChatChannel = beamChannels.get(channel);
if (beamChatChannel == null) {
return false;
}
beamChatChannel.clearCache();
return true;
}
private boolean banOrUnBan(String channelName, String user, boolean ban) {
String param = ban? "add" : "remove";
// Check if user has user level mod or higher
if (ban) {
try {
if (BotUtil.isModOrHigher(channelName, user)) {
return false;
}
} catch (ChannelNotFoundException e) {
App.logger.error("Unable to get channel '" + channelName + "' to ban user");
App.logger.catching(e);
}
}
// Get channel info
BeamChatChannel beamChatChannel = beamChannels.get(channelName);
if (beamChatChannel == null) {
return false;
}
String path = beam.basePath.resolve("channels/" + beamChatChannel.channel.id + "/users/" + user.toLowerCase()).toString();
HashMap<String, Object> map = new HashMap<>();
map.put(param, new String[]{"Banned"});
try {
Object result = beam.http.patch(path, Object.class, map).get(4, TimeUnit.SECONDS);
if ((result != null) && result.toString().contains("username")) {
return true;
}
} catch (TimeoutException ignored) {
App.logger.error("Request to set 'Banned' status of user '" + user + "' timed out");
} catch (InterruptedException e) {
App.logger.catching(e);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
App.logger.catching(Level.DEBUG, e);
}
return false;
}
}