package irc;
import face.Face;
import face.FaceManager;
import gui.forms.GUIMain;
import irc.account.OAuth;
import irc.account.Task;
import irc.message.Message;
import irc.message.MessageHandler;
import irc.message.MessageQueue;
import lib.pircbot.PircBot;
import lib.pircbot.User;
import sound.Sound;
import sound.SoundEngine;
import thread.ThreadEngine;
import util.APIRequests;
import util.Response;
import util.StringArray;
import util.Utils;
import util.comm.Command;
import util.comm.ConsoleCommand;
import util.misc.Raffle;
import util.misc.Vote;
import util.settings.Settings;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
public class IRCBot extends MessageHandler {
public PircBot getBot() {
return Settings.accountManager.getBot();
}
public ArrayList<String> winners;
public ArrayList<Raffle> raffles;
private Vote poll;
private long lastAd;
public IRCBot() {
raffles = new ArrayList<>();
winners = new ArrayList<>();
poll = null;
lastAd = -1;
}
@Override
public void onConnect() {
Settings.channelManager.addUser(new User(getBot().getNick()));
GUIMain.channelSet.forEach(this::doConnect);
GUIMain.updateTitle(null);
}
public void doConnect(String channel) {
if (!channel.startsWith("#")) channel = "#" + channel;
Settings.accountManager.addTask(new Task(getBot(), Task.Type.JOIN_CHANNEL, channel));
}
/**
* Leaves a channel and if specified, removes the channel from the
* channel list.
*
* @param channel The channel name to leave (# not included).
*/
public void doLeave(String channel) {
if (!channel.startsWith("#")) channel = "#" + channel;
Settings.accountManager.addTask(new Task(getBot(), Task.Type.LEAVE_CHANNEL, channel));
}
/**
* Disconnects from all chats and disposes of the bot.
*
* @param forget True if you are logging out, false if shutting down.
*/
public void close(boolean forget) {
GUIMain.log("Logging out of bot: " + Settings.accountManager.getBotAccount().getName());
Settings.accountManager.addTask(new Task(getBot(), Task.Type.DISCONNECT, null));
if (forget) {
Settings.accountManager.setBotAccount(null);
}
GUIMain.bot = null;
}
public void onDisconnect() {
if (!GUIMain.shutDown && getBot() != null) {
GUIMain.logCurrent("Detected a disconnection for the account: " + getBot().getNick());
if (Settings.autoReconnectAccounts.getValue())
Settings.accountManager.createReconnectThread(getBot().getConnection());
else {
GUIMain.logCurrent("Auto-reconnects disabled, please check Preferences -> Auto-Reconnect!");
}
}
}
@Override
public void onJTVMessage(String channel, String line, String tags) {
if (tags != null) {
if (tags.contains("msg_banned") || tags.contains("msg_timedout")) {
MessageQueue.addMessage(new Message().setChannel(channel)
.setType(Message.MessageType.JTV_NOTIFY).setContent(getBot().getNick() + " is " + line.substring(8)));
}
}
}
@Override
public void onMessage(String channel, String sender, String message) {
if (message != null && channel != null && sender != null && Settings.accountManager.getViewer() != null) {
String botnakUserName = Settings.accountManager.getUserAccount().getName();
sender = sender.toLowerCase();
if (!channel.contains(botnakUserName.toLowerCase())) {//in other channels
int replyType = Settings.botReplyType.getValue();
if (replyType == 0) return;
//0 = reply to nobody (just spectate), 1 = reply to just the Botnak user, 2 = reply to everyone
if (replyType == 1 && !sender.equalsIgnoreCase(botnakUserName)) return;
}
boolean senderIsBot = sender.equalsIgnoreCase(getBot().getNick());
boolean userIsBot = botnakUserName.equalsIgnoreCase(Settings.accountManager.getBotAccount().getName());
//if the sender of the message is the bot, but
//the user account is NOT the bot, just return, we don't want the bot to trigger anything
if (senderIsBot && !userIsBot) return;
//raffles
User u = Settings.channelManager.getUser(sender, true);
if (!raffles.isEmpty()) {
if (!winners.contains(u.getNick().toLowerCase())) {
for (Raffle r : raffles) {
if (r.isDone()) {
continue;
}
String key = r.getKeyword();
if (message.contains(key)) {
r.addUser(u, channel); // Handles filtering permissions
}
}
}
ArrayList<Raffle> toRemove = new ArrayList<>();
raffles.stream().filter(Raffle::isDone).forEach(r -> {
winners.add(r.getWinner());
toRemove.add(r);
});
if (!toRemove.isEmpty()) {
raffles.removeAll(toRemove);
toRemove.clear();
}
}
OAuth key = Settings.accountManager.getUserAccount().getOAuth();
String[] split = message.split(" ");
//URL Checking
boolean ytVidDetail = Settings.botShowYTVideoDetails.getValue();
boolean twitchVOD = Settings.botShowTwitchVODDetails.getValue();
boolean unshortenURLs = Settings.botUnshortenURLs.getValue();
if (ytVidDetail || twitchVOD || unshortenURLs) {
ThreadEngine.submit(() -> {
int count = 0;
for (String part : split) {
if (count > 1) break;//only allowing 2 requests here; don't want spam
if (part.startsWith("http") || part.startsWith("www")) {
if (ytVidDetail && (part.contains("youtu.be") || part.contains("youtube.com/watch")
|| part.contains("youtube.com/v") || part.contains("youtube.com/embed/"))) {
getBot().sendMessage(channel, APIRequests.YouTube.getVideoData(part).getResponseText());
count++;
} else if (unshortenURLs && (part.contains("bit.ly") ||
part.contains("tinyurl") || part.contains("goo.gl"))) {
getBot().sendMessage(channel, APIRequests.UnshortenIt.getUnshortened(part).getResponseText());
count++;
} else if (twitchVOD && part.contains("twitch.tv/")) {
if (part.contains("/v/") || part.contains("/c/") || part.contains("/b/")) {
getBot().sendMessage(channel, APIRequests.Twitch.getTitleOfVOD(part).getResponseText());
count++;
}
}
}
}
});
}
String first = "";
if (split.length > 1) first = split[1];
//commands
if (message.startsWith("!")) {
String trigger = message.substring(1).split(" ")[0].toLowerCase();
String mess = message.substring(1);
//sound
if (SoundEngine.getEngine().soundTrigger(trigger, sender, channel)) {
SoundEngine.getEngine().playSound(new Sound(SoundEngine.getEngine().getSoundMap().get(trigger)));
}
ConsoleCommand consoleCommand = Utils.getConsoleCommand(trigger, channel, u);
if (consoleCommand != null) {
Response commandResponse = null;
switch (consoleCommand.getAction()) {
case ADD_FACE:
case CHANGE_FACE:
commandResponse = FaceManager.handleFace(mess);
if (commandResponse.isSuccessful()) Settings.FACES.save();
break;
case REMOVE_FACE:
commandResponse = FaceManager.removeFace(first);
if (commandResponse.isSuccessful()) Settings.FACES.save();
break;
case TOGGLE_FACE:
commandResponse = FaceManager.toggleFace(first);
break;
case ADD_SOUND:
case CHANGE_SOUND:
commandResponse = SoundEngine.getEngine().handleSound(mess, consoleCommand.getAction() == ConsoleCommand.Action.CHANGE_SOUND);
break;
case REMOVE_SOUND:
commandResponse = SoundEngine.getEngine().removeSound(first);
break;
case SET_SOUND_DELAY:
commandResponse = SoundEngine.getEngine().setSoundDelay(first);
break;
case TOGGLE_SOUND:
boolean individualSound = split.length > 1;
commandResponse = SoundEngine.getEngine().toggleSound(individualSound ? first : null, individualSound);
break;
case STOP_SOUND:
case STOP_ALL_SOUNDS:
commandResponse = SoundEngine.getEngine().stopSound(consoleCommand.getAction() == ConsoleCommand.Action.STOP_ALL_SOUNDS);
break;
case SEE_SOUND_STATE:
commandResponse = SoundEngine.getEngine().getSoundState(first);
break;
case ADD_KEYWORD:
case REMOVE_KEYWORD:
commandResponse = Utils.handleKeyword(mess);
if (commandResponse.isSuccessful()) Settings.KEYWORDS.save();
break;
case SET_USER_COL:
commandResponse = Utils.handleColor(sender, mess, u.getColor());
if (commandResponse.isSuccessful()) Settings.USER_COLORS.save();
break;
case SET_COMMAND_PERMISSION:
commandResponse = Utils.setCommandPermission(mess);
if (commandResponse.isSuccessful()) Settings.saveConCommands();
break;
case ADD_TEXT_COMMAND:
commandResponse = Utils.addCommands(mess);
if (commandResponse.isSuccessful()) Settings.COMMANDS.save();
break;
case REMOVE_TEXT_COMMAND:
commandResponse = Utils.removeCommands(first);
if (commandResponse.isSuccessful()) Settings.COMMANDS.save();
break;
case ADD_DONATION:
commandResponse = Settings.donationManager.parseDonation(split);
break;
case SET_SOUND_PERMISSION:
commandResponse = SoundEngine.getEngine().setSoundPermission(first);
break;
case SET_NAME_FACE:
if (first.startsWith("http")) {
commandResponse = FaceManager.downloadFace(first,
Settings.nameFaceDir.getAbsolutePath(),
Utils.setExtension(sender, ".png"), sender, FaceManager.FACE_TYPE.NAME_FACE);
}
break;
case REMOVE_NAME_FACE:
if (FaceManager.nameFaceMap.containsKey(sender)) {
try {
Face f = FaceManager.nameFaceMap.remove(sender);
if (f != null && new File(f.getFilePath()).delete())
getBot().sendMessage(channel, "Removed face for user: " + sender + " !");
} catch (Exception e) {
getBot().sendMessage(channel, "Name face for user " + sender +
" could not be removed due to an exception!");
}
} else {
getBot().sendMessage(channel, "The user " + sender + " has no name face!");
}
break;
case SET_STREAM_TITLE:
commandResponse = APIRequests.Twitch.setStreamStatus(key, channel, message, true);
if (commandResponse.isSuccessful()) {
if (GUIMain.statusGUI != null && GUIMain.statusGUI.isVisible()) {
GUIMain.statusGUI.updateStatusComponents();
}
}
break;
case SEE_STREAM_TITLE:
String title = APIRequests.Twitch.getTitleOfStream(channel);
if (!"".equals(title)) {
getBot().sendMessage(channel, "The title of the stream is: " + title);
} else {
getBot().sendMessage(channel, "The stream currently has no title!");
}
break;
case SEE_STREAM_GAME:
String game = APIRequests.Twitch.getGameOfStream(channel);
if ("".equals(game)) {
getBot().sendMessage(channel, "The streamer is currently not playing a game!");
} else {
getBot().sendMessage(channel, "The current game is: " + game);
}
break;
case SET_STREAM_GAME:
commandResponse = APIRequests.Twitch.setStreamStatus(key, channel, message, false);
if (commandResponse.isSuccessful()) {
if (GUIMain.statusGUI != null && GUIMain.statusGUI.isVisible()) {
GUIMain.statusGUI.updateStatusComponents();
}
}
break;
case PLAY_ADVERT:
if (key != null) {
commandResponse = playAdvert(key, first, channel);
}
break;
case START_RAFFLE:
if (split.length > 2) {
String timeString = split[2];
int time = Utils.getTime(timeString);
if (time < 1) {
getBot().sendMessage(channel, "Failed to start raffle, usage: !startraffle (key) (time) (permission?)");
break;
}
int perm = 0;//TODO select a parameter in Settings GUI that defines the default raffle
if (split.length == 4) {
//because right now it's just "Everyone" unless specified with the int param
try {
perm = Integer.parseInt(split[3]);
perm = Utils.capNumber(0, 3, perm);
} catch (Exception ignored) {//default to the specified value
}
}
Raffle r = new Raffle(getBot(), first, time, channel, perm);
startRaffle(r);
updateRaffleGUI(r, true);
} else {
getBot().sendMessage(channel, "Failed to start raffle, usage: !startraffle (key) (time) (permission?)");
}
break;
case ADD_RAFFLE_WINNER:
if (!winners.contains(first)) {
winners.add(first);
getBot().sendMessage(channel, "The user " + first + " has been added to the winners pool!");
} else {
getBot().sendMessage(channel, "The user " + first + " is already in the winners pool!");
}
break;
case STOP_RAFFLE:
Raffle toRemove = stopRaffle(first);
if (toRemove != null) {
sendStopRaffleMessage(toRemove);
updateRaffleGUI(toRemove, false);
raffles.remove(toRemove);
} else {
getBot().sendMessage(channel, "There is no such raffle \"" + first + "\" !");
}
break;
case REMOVE_RAFFLE_WINNER:
if (winners.contains(first)) {
if (winners.remove(first)) {
getBot().sendMessage(channel, "The user " + first + " was removed from the winners pool!");
}
} else {
getBot().sendMessage(channel, "The user " + first + " is not in the winners pool!");
}
break;
case SEE_WINNERS:
if (!winners.isEmpty()) {
StringBuilder stanSB = new StringBuilder();
stanSB.append("The current raffle winners are: ");
for (String name : winners) {
stanSB.append(name);
stanSB.append(", ");
}
getBot().sendMessage(channel, stanSB.toString().substring(0, stanSB.length() - 2) + " .");
} else {
getBot().sendMessage(channel, "There are no recorded winners!");
}
break;
case START_POLL:
if (poll != null) {
if (poll.isDone()) {
createPoll(channel, message);
} else {
getBot().sendMessage(channel, "Cannot start a poll with one currently running!");
}
} else {
createPoll(channel, message);
}
break;
case POLL_RESULT:
if (poll != null) {
if (poll.isDone())
poll.printResults();
else
getBot().sendMessage(channel, "The poll is still running! Cancel it before seeing the results!");
} else {
getBot().sendMessage(channel, "There never was a poll!");
}
break;
case CANCEL_POLL:
if (poll != null) {
if (poll.isDone()) {
getBot().sendMessage(channel, "The poll is already finished!");
} else {
stopPoll();
getBot().sendMessage(channel, "The poll has been stopped!");
}
} else {
getBot().sendMessage(channel, "There is no current poll!");
}
break;
case VOTE_POLL:
if (poll != null) {
if (!poll.isDone()) {
try {
int option = Integer.parseInt(first);
poll.addVote(sender, option);
} catch (Exception e)
{
GUIMain.log(e);
}
}
}
break;
case NOW_PLAYING:
commandResponse = APIRequests.LastFM.getCurrentlyPlaying();
break;
case SHOW_UPTIME:
commandResponse = APIRequests.Twitch.getUptimeString(channel.substring(1));
break;
case SEE_PREV_SOUND_DON:
if (Settings.botShowPreviousDonSound.getValue()) {
if (Settings.loadedDonationSounds)
commandResponse = SoundEngine.getEngine().getLastDonationSound();
}
break;
case SEE_PREV_SOUND_SUB:
if (Settings.botShowPreviousSubSound.getValue()) {
if (Settings.loadedSubSounds)
commandResponse = SoundEngine.getEngine().getLastSubSound();
}
break;
case SEE_OR_SET_REPLY_TYPE:
commandResponse = parseReplyType(first, botnakUserName);
break;
case SEE_OR_SET_VOLUME:
if ("".equals(first)) {
getBot().sendMessage(channel, "The current Sound volume is " + String.format("%.1f", Settings.soundVolumeGain.getValue()));
} else {
try {
Float volume = Float.parseFloat(first);
volume = Utils.capNumber(0F, 100F, volume);
Settings.soundVolumeGain.setValue(volume);
getBot().sendMessage(channel, "The Sound volume was successfully set to " + String.format("%.1f", Settings.soundVolumeGain.getValue()));
} catch (Exception e) {
getBot().sendMessage(channel, "Failed to change Sound volume! Usage: \"!volume (number)\"");
}
}
break;
default:
break;
}
if (commandResponse != null && !"".equals(commandResponse.getResponseText()))
getBot().sendMessage(channel, commandResponse.getResponseText());
}
//text command
Command c = Utils.getCommand(trigger);
//we check the senderIsBot here because we want to be able to call console commands,
//but we don't want the bot to trigger its own text commands, which
//could infinite loop (two commands calling each other over and over)
if (c != null && !senderIsBot && c.getMessage().data.length > 0 && !c.getDelayTimer().isRunning()) {
StringArray sa = c.getMessage();
if (c.hasArguments()) {
//build arguments if it has any
int argAmount = c.countArguments();
if ((split.length - 1) < argAmount) {
getBot().sendMessage(channel, "Missing command arguments! Command format: " + c.printCommand());
return;
}
String[] definedArguments = new String[argAmount];
System.arraycopy(split, 1, definedArguments, 0, argAmount);
sa = c.buildMessage(sa, definedArguments);
}
//send the message
for (String s : sa.data) {
getBot().sendMessage(channel, s);
}
c.getDelayTimer().reset();
}
}
}
}
//!startpoll time options
public void createPoll(String channel, String message)
{
if (message.contains("]")) {//because what's the point of a poll with one option?
int first = message.indexOf(" ") + 1;
int second = message.indexOf(" ", first) + 1;
String[] split = message.split(" ");
int time = Utils.getTime(split[1]);
if (time > 0) {
startPoll(new Vote(channel, time, message.substring(second).split("]")));
}
}
}
public boolean pollExists()
{
return poll != null;
}
public boolean isPollRunning()
{
return pollExists() && !poll.isDone() && poll.isAlive();
}
public Vote getPoll()
{
return poll;
}
public void startPoll(Vote v)
{
// Actually start the poll
poll = v;
poll.start();
// Update the GUI, if there is one
if (GUIMain.voteGUI != null)
GUIMain.voteGUI.addPoll(poll);
}
public void stopPoll()
{
if (poll == null) return;
poll.interrupt();
if (GUIMain.voteGUI != null)
GUIMain.voteGUI.pollEnded(poll);
}
private void updateRaffleGUI(Raffle r, boolean add)
{
if (GUIMain.raffleGUI != null && GUIMain.raffleGUI.isVisible())
{
if (add)
{
GUIMain.raffleGUI.addRaffle(r);
} else // removed
{
GUIMain.raffleGUI.removeRaffle(r);
}
}
}
public Raffle stopRaffle(String keyword)
{
for (Raffle r : raffles)
{
if (r.getKeyword().equalsIgnoreCase(keyword))
{
r.setDone(true);
r.interrupt();
return r;
}
}
return null;
}
public void sendStopRaffleMessage(Raffle r)
{
getBot().sendMessage(r.getChannel(), "The raffle with key \"" + r.getKeyword() + "\" has been stopped!");
}
public void startRaffle(Raffle toStart)
{
toStart.start();
raffles.add(toStart);
//print the blarb
getBot().sendMessage(toStart.getChannel(), toStart.getStartMessage());
getBot().sendMessage(toStart.getChannel(), "NOTE: This is a promotion from " + toStart.getChannel().substring(1) +
". Twitch does not sponsor or endorse broadcaster promotions and is not responsible for them.");
}
public Response playAdvert(OAuth key, String first, String channel)
{
Response r = new Response();
if (key.canPlayAd()) {
int length = Utils.getTime(first);
if (length == -1) length = 30;
if (APIRequests.Twitch.playAdvert(key.getKey(), channel, length)) {
r.wasSuccessful();
r.setResponseText("Playing an ad for " + length + " seconds!");
lastAd = System.currentTimeMillis();
} else {
r.setResponseText("Error playing an ad!");
long diff = System.currentTimeMillis() - lastAd;
if (lastAd > 0 && (diff < 480000)) {
SimpleDateFormat sdf = new SimpleDateFormat("m:ss");
Date d = new Date(diff);
Date toPlay = new Date(480000 - diff);
r.setResponseText("Error playing advertisement! Last ad was was only " + sdf.format(d)
+ " ago! You must wait " + sdf.format(toPlay) + " to play another ad!");
}
}
} else {
r.setResponseText("The current User OAuth key cannot play an advertisement!");
}
return r;
}
public Response parseReplyType(String first, String botnakUser) {
Response toReturn = new Response();
try {
if (!"".equals(first)) {
int perm = Integer.parseInt(first);
perm = Utils.capNumber(0, 2, perm);
Settings.botReplyType.setValue(perm);
toReturn.setResponseText("Successfully changed the bot reply type (for other channels) to: " + getReplyType(perm, botnakUser));
} else {
toReturn.setResponseText("Current bot reply type for other channels is: " +
getReplyType(Settings.botReplyType.getValue(), botnakUser));
}
} catch (Exception ignored) {
toReturn.setResponseText("Failed to set bot reply type due to an exception!");
}
return toReturn;
}
private String getReplyType(int perm, String botnakUser) {
if (perm > 1) {
return "Reply to everybody (" + perm + ")";
} else if (perm > 0) {
return "Reply to just " + botnakUser + " (" + perm + ")";
} else {
return "Reply to nobody (" + perm + ")";
}
}
}