package util.settings; import gui.BotnakTrayIcon; import gui.forms.GUIMain; import irc.Subscriber; import irc.account.OAuth; import irc.message.Message; import irc.message.MessageQueue; import lib.JSON.JSONArray; import lib.JSON.JSONObject; import lib.pircbot.User; import sound.SoundEngine; import util.Utils; import java.net.URL; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Optional; import java.util.concurrent.CopyOnWriteArraySet; /** * Created by Nick on 11/28/2014. * <p> * This class keeps track of subscribers of your own channel, along * with providing the date of which Botnak recognized them to have subbed. * <p> * Every time their sub status is called, this manager will tell Botnak * how long they have been subbed for, so when one month after they subbed * comes around, Botnak will know that it needs to up the donation by $2.50. */ public class SubscriberManager { private CopyOnWriteArraySet<Subscriber> subscribers; private Subscriber lastSubscriber = null; public void setLastSubscriber(Subscriber lastSubscriber) { this.lastSubscriber = lastSubscriber; } public Subscriber getLastSubscriber() { return lastSubscriber; } public Subscriber[] getLastSubscribers(int count) { return subscribers.stream().sorted().limit(count).toArray(Subscriber[]::new); } public CopyOnWriteArraySet<Subscriber> getSubscribers() { return subscribers; } public SubscriberManager() { subscribers = new CopyOnWriteArraySet<>(); } public Optional<Subscriber> getSubscriber(String name) { for (Subscriber s : subscribers) { if (s.getName().equalsIgnoreCase(name)) { return Optional.of(s); } } return Optional.empty(); } /** * How's our little friend doing? * <p> * Called from the Channel class of Pircbot, this method * updates the sub's status (donation-wise) based on how * many months it's been since they first subbed, and checks * to see if the person subscribed while offline (new or not). * * @param u The user object of the potential subscriber. * @param channel Your channel name, for the messages. * @param currentlyActive Boolean used to determine current sub status of the user. */ public void updateSubscriber(User u, String channel, boolean currentlyActive) { if (u.getNick().equalsIgnoreCase(Settings.accountManager.getUserAccount().getName())) return; //you will always be your own sub, silly Optional<Subscriber> s = getSubscriber(u.getNick()); if (s.isPresent()) { if (s.get().isActive()) { int streak = s.get().getStreak(); int monthsSince = (int) (s.get().getStarted().until(LocalDateTime.now(), ChronoUnit.DAYS) / 32); if (monthsSince > streak) { if (currentlyActive) { String content = s.get().getName() + " has continued their subscription for over " + (monthsSince) + ((monthsSince) > 1 ? " months!" : " month!"); MessageQueue.addMessage(new Message().setChannel(channel).setType(Message.MessageType.SUB_NOTIFY).setContent(content)); s.get().incrementStreak(monthsSince - streak);//this will most likely be 1 playSubscriberSound(); } else {//we're offering a month to re-sub s.get().setActive(false); s.get().resetStreak(); } } } else { if (currentlyActive) { // this has the potential to be an offline re-sub: // botnak will know that the sub is currently alive if it catches it live, (see the other use of SUB_NOTIFY) // however if the person subscribes offline, botnak has no way of telling, and // the next time they talk is the only time Botnak (and perhaps you as well) knows for sure that they did // so, we need to update the date the user subbed to now, ensure their streak is reset, and // make botnak send a "thanks for subbing offline" message //or twitchnotify could have been a douchenozzle and did not send the message String content = s.get().getName() + " has RE-subscribed offline!"; if (Settings.botAnnounceSubscribers.getValue()) { Settings.accountManager.getBot().sendMessage(channel, ".me " + u.getNick() + " has just RE-subscribed!"); } MessageQueue.addMessage(new Message().setContent(content).setType(Message.MessageType.SUB_NOTIFY).setChannel(channel)); s.get().resetStreak(); s.get().setStarted(LocalDateTime.now()); s.get().setActive(true); playSubscriberSound(); setLastSubscriber(s.get()); notifyTrayIcon(content, false); } } } else { if (currentlyActive) { // this is a new, offline sub. Botnak is going to throw a new sub message just // as if they had subbed the instant they sent the message //or twitchnotify could have been a douchenozzle and did not send the message if (Settings.botAnnounceSubscribers.getValue()) { Settings.accountManager.getBot().sendMessage(channel, ".me " + u.getNick() + " has just subscribed!"); } String content = u.getNick().toLowerCase() + " has subscribed offline!"; MessageQueue.addMessage(new Message().setContent(content).setType(Message.MessageType.SUB_NOTIFY).setChannel(channel)); addSub(new Subscriber(u.getNick().toLowerCase(), LocalDateTime.now(), true, 0)); playSubscriberSound(); notifyTrayIcon(content, false); } } } public void fillSubscribers(HashSet<Subscriber> set) { subscribers.addAll(set); } public void addSub(Subscriber s) { if (subscribers.add(s)) { setLastSubscriber(s); } } public boolean addNewSubscriber(String name, String channel) { Optional<Subscriber> subscriber = getSubscriber(name); if (!subscriber.isPresent()) {//brand spanking new sub, live as botnak caught it addSub(new Subscriber(name, LocalDateTime.now(), true, 0)); notifyTrayIcon(name + " has just subscribed!", false); playSubscriberSound(); //we're going to return false (end of method) so that Botnak generates the message //like it did before this manager was created and implemented (and because less of the same code is better eh?) } else if (subscriber.get().isActive()) { //this may have been twitchnotify telling us twice, discard without messing up sounds return true; } else { //re-sub! //if we got the message, this means a month has passed, and they cancelled //question: should the streak still matter, if they're quick enough to resub? //answer: Botnak automatically acknowledges them for their continued support anyways, and // if they decide to cancel just to get the notification again, they deserve their streak to be reset String content = name + " has just RE-subscribed!"; MessageQueue.addMessage(new Message().setContent(content).setChannel(channel).setType(Message.MessageType.SUB_NOTIFY)); notifyTrayIcon(content, false); playSubscriberSound(); subscriber.get().resetStreak(); subscriber.get().setStarted(LocalDateTime.now()); subscriber.get().setActive(true); setLastSubscriber(subscriber.get()); return true; } return false; } private void playSubscriberSound() { if (Settings.loadedSubSounds) SoundEngine.getEngine().playSpecialSound(true); } public void notifyTrayIcon(String content, boolean continuation) { if (BotnakTrayIcon.shouldDisplayNewSubscribers()) { GUIMain.getSystemTrayIcon().displaySubscriber(content, continuation); } } public void scanInitialSubscribers(String channel, OAuth key, int passesCompleted, HashSet<Subscriber> set) { String oauth = key.getKey().split(":")[1]; String urlString = "https://api.twitch.tv/kraken/channels/" + channel + "/subscriptions?oauth_token=" + oauth + "&limit=100"; String offset = "&offset=" + String.valueOf(100 * passesCompleted); urlString += offset; try { String line = Utils.createAndParseBufferedReader(new URL(urlString).openStream()); if (!line.isEmpty()) { JSONObject entire = new JSONObject(line); if (entire.has("error")) { GUIMain.log("Error scanning for initial subs, does your OAuth key allow for this?"); } else { int total = entire.getInt("_total"); int passes = (total > 100 ? (int) Math.ceil((double) total / 100.0) : 1); if (passes == passesCompleted) { fillSubscribers(set); GUIMain.log("Successfully scanned " + set.size() + " subscriber(s)!"); Settings.scannedInitialSubscribers.setValue(true); } else { JSONArray subs = entire.getJSONArray("subscriptions"); for (int subIndex = 0; subIndex < subs.length(); subIndex++) { JSONObject outer = subs.getJSONObject(subIndex); JSONObject user = outer.getJSONObject("user"); String name = user.getString("name"); if (name.equalsIgnoreCase(channel)) continue;//don't want to add yourself LocalDateTime started = LocalDateTime.parse(outer.getString("created_at"), DateTimeFormatter.ISO_DATE_TIME); int streak = (int) started.until(LocalDateTime.now(), ChronoUnit.MONTHS); Subscriber s = new Subscriber(name, started, true, streak); set.add(s); } scanInitialSubscribers(channel, key, passesCompleted + 1, set); } } } } catch (Exception e) { if (e.getMessage().contains("422")) { //the user does not have a sub button key.setCanReadSubscribers(false); GUIMain.log("Failed to parse subscribers; your channel is not partnered!"); } else GUIMain.log(e); } } }