diff --git a/.gitignore b/.gitignore index cd67a55..fa49c98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ \.gradle/ \.idea/ build/ -*.iml \ No newline at end of file +*.iml + +*.db \ No newline at end of file diff --git a/src/main/java/me/okx/twitchsync/Revoker.java b/src/main/java/me/okx/twitchsync/Revoker.java index 0df1604..02461b4 100644 --- a/src/main/java/me/okx/twitchsync/Revoker.java +++ b/src/main/java/me/okx/twitchsync/Revoker.java @@ -22,70 +22,14 @@ public Revoker(TwitchSync plugin) { @Override public void run() { - plugin.getLogger().info("Revoking now."); + plugin.getLogger().info("Syncing user data now."); long start = System.currentTimeMillis(); Map tokens = plugin.getSqlHelper().getTokens().get(); - checkTokens(tokens); + plugin.getValidator().sync(tokens); long time = System.currentTimeMillis() - start; - plugin.getLogger().info("Finished revoking in " + time + "ms."); + plugin.getLogger().info("Finished Sync in " + time + "ms."); } - private void checkTokens(Map tokens) { - for(Map.Entry entry : tokens.entrySet()) { - UUID uuid = entry.getKey(); - - Token token = entry.getValue(); - - AccessToken accessToken = refresh(token.getAccessToken()); - plugin.getSqlHelper().setToken(uuid, - token.getId(), - accessToken.getAccessToken(), - accessToken.getRefreshToken()); - - plugin.getSqlHelper().isFollowing(uuid).ifPresent(b -> { - if(b && check(plugin.getValidator().getFollowingState(token.getId(), accessToken))) { - revoke("follow", uuid); - plugin.getSqlHelper().setFollowing(uuid, false); - } - }); - plugin.getSqlHelper().isSubscribed(uuid).ifPresent(b -> { - // if subscribed and all states are not YES - if(b && check(plugin.getValidator().getSubscriptionState(token.getId(), accessToken))) { - revoke("subscribe", uuid); - plugin.getSqlHelper().setSubscribed(uuid, false); - } - }); - } - } - - private void revoke(String type, UUID uuid) { - ConfigurationSection section = plugin.getConfig().getConfigurationSection(type); - OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); - - Bukkit.getScheduler().runTask(plugin, () -> { - plugin.debug(type + " - " + uuid, "revoking"); - String group = section.getString("rank"); - if (!group.equalsIgnoreCase("none") && plugin.getPerms() != null) { - plugin.getPerms().playerRemoveGroup(null, player, group); - } - - for (String command : section.getStringList("revoke-commands")) { - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command - .replace("%name%", player.getName())); - } - }); - } - - /** - * Check if all states are not YES - */ - public boolean check(Stream states) { - return states.allMatch(stateWithId -> stateWithId.getState() != CheckState.YES); - } - - private AccessToken refresh(AccessToken accessToken) { - return plugin.getValidator().refreshToken(accessToken.getRefreshToken()); - } } diff --git a/src/main/java/me/okx/twitchsync/TwitchServer.java b/src/main/java/me/okx/twitchsync/TwitchServer.java index 9772464..499ad7e 100644 --- a/src/main/java/me/okx/twitchsync/TwitchServer.java +++ b/src/main/java/me/okx/twitchsync/TwitchServer.java @@ -1,10 +1,9 @@ package me.okx.twitchsync; import com.sun.net.httpserver.HttpServer; +import me.okx.twitchsync.data.Token; import me.okx.twitchsync.data.sync.SyncMessage; import me.okx.twitchsync.data.sync.SyncResponse; -import me.okx.twitchsync.data.sync.SyncResponseFailure; -import me.okx.twitchsync.data.sync.SyncResponseSuccess; import java.io.IOException; import java.io.OutputStream; @@ -30,7 +29,7 @@ public void start() throws IOException { try { plugin.debug("Handling request " + ex + " on " + Thread.currentThread().getName()); - SyncResponse response = new SyncResponseFailure(SyncMessage.INVALID_URL); + SyncResponse response = SyncResponse.of(SyncMessage.INVALID_URL); String query = ex.getRequestURI().getQuery(); if (query != null) { @@ -41,35 +40,16 @@ public void start() throws IOException { String code = parameters.get("code"); if(state != null && code != null) { - response = plugin.getValidator().sync(UUID.fromString(state), code); + UUID stateUUID = UUID.fromString(state); + UUID uuid = plugin.getValidator().getUUIDFromAuthState(stateUUID); + if (uuid != null) { + Token token = plugin.getValidator().store(uuid, code).get(); + response = plugin.getValidator().sync(uuid, token).get(); + } else response = SyncResponse.of(SyncMessage.INVALID_URL); } } - SyncMessage message; - if (response instanceof SyncResponseSuccess) { - SyncResponseSuccess success = (SyncResponseSuccess) response; - if (success.isFollowing() && success.isSubscribed()) { - message = SyncMessage.BOTH_SUCCESS; - } else if (success.isFollowing()) { - message = SyncMessage.FOLLOW_SUCCESS; - } else if (success.isSubscribed()) { - message = SyncMessage.SUBSCRIPTION_SUCCESS; - } else { - SyncMessage subscribe = success.getSubscribeMessage(); - SyncMessage follow = success.getFollowMessage(); - // make sure the already-done message shows up - if(subscribe == SyncMessage.ALREADY_DONE) { - message = subscribe; - } else { - message = follow; - } - } - } else if (response instanceof SyncResponseFailure) { - SyncResponseFailure failure = (SyncResponseFailure) response; - message = failure.getMessage(); - } else { - throw new IllegalArgumentException("Sync response must either be success or failure."); - } + SyncMessage message = response.getMessage(); byte[] bytes = message.getValue(plugin).getBytes("UTF-8"); diff --git a/src/main/java/me/okx/twitchsync/TwitchSync.java b/src/main/java/me/okx/twitchsync/TwitchSync.java index 16e09d2..3551ce7 100644 --- a/src/main/java/me/okx/twitchsync/TwitchSync.java +++ b/src/main/java/me/okx/twitchsync/TwitchSync.java @@ -1,8 +1,12 @@ package me.okx.twitchsync; +import me.okx.twitchsync.data.Channel; +import me.okx.twitchsync.data.Options; +import me.okx.twitchsync.data.Upgrade; import me.okx.twitchsync.events.PlayerListener; import me.okx.twitchsync.util.SqlHelper; import net.milkbowl.vault.permission.Permission; +import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; @@ -19,8 +23,16 @@ public Permission getPerms() { return this.perms; } + @Override + public void onLoad() { + ConfigurationSerialization.registerClass(Options.class); + ConfigurationSerialization.registerClass(Channel.class); + ConfigurationSerialization.registerClass(Upgrade.class); + } + @Override public void onEnable() { + debug("Logging works.", "Testing"); getConfig().options().copyDefaults(true); saveDefaultConfig(); initValidator(); @@ -32,7 +44,7 @@ public void onEnable() { getCommand("twitchsync").setExecutor(new TwitchSyncCommand(this)); getCommand("revoke").setExecutor(new RevokeCommand(this)); - long time = 20*86400*getConfig().getInt("revoke-interval-days"); + long time = 20*60*getConfig().getInt("revoke-interval-minutes"); new Revoker(this).runTaskTimerAsynchronously(this, time, time); new Metrics(this); diff --git a/src/main/java/me/okx/twitchsync/TwitchSyncCommand.java b/src/main/java/me/okx/twitchsync/TwitchSyncCommand.java index 365ad1e..d2ef885 100644 --- a/src/main/java/me/okx/twitchsync/TwitchSyncCommand.java +++ b/src/main/java/me/okx/twitchsync/TwitchSyncCommand.java @@ -1,5 +1,7 @@ package me.okx.twitchsync; +import me.okx.twitchsync.data.sync.SyncMessage; +import me.okx.twitchsync.data.sync.SyncResponse; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; @@ -9,6 +11,9 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + public class TwitchSyncCommand implements CommandExecutor { private TwitchSync plugin; @@ -24,18 +29,27 @@ public boolean onCommand(CommandSender cs, Command command, String s, String[] a } Player player = (Player) cs; - - String url = plugin.getValidator().createAuthenticationUrl(player.getUniqueId()); - if(url == null) { - player.sendMessage(ChatColor.RED + "An error occurred. Please try again."); - return true; - } - - player.spigot().sendMessage(new ComponentBuilder("Click this text to sync to Twitch") - .color(net.md_5.bungee.api.ChatColor.GREEN) - .event(new ClickEvent(ClickEvent.Action.OPEN_URL, url)) - .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("Click to sync to Twitch").create())) - .create()); + plugin.getValidator().sync(player.getUniqueId()).whenCompleteAsync((response, throwable) -> { + if (throwable != null) { + player.sendMessage(ChatColor.translateAlternateColorCodes('&', "&6An error occurred. Please try again.")); + plugin.getLogger().warning(throwable.toString()); + throwable.printStackTrace(); + } + if (response.getMessage() != SyncMessage.NO_TOKEN) { + player.sendMessage(ChatColor.GREEN + "Re-synced."); + } else { + String url = plugin.getValidator().createAuthenticationUrl(player.getUniqueId()); + if(url == null) { + player.sendMessage(ChatColor.RED + "An error occurred. Please try again."); + return; + } + player.spigot().sendMessage(new ComponentBuilder("Click this text to sync to Twitch") + .color(net.md_5.bungee.api.ChatColor.GREEN) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL, url)) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("Click to sync to Twitch").create())) + .create()); + } + }); return true; } diff --git a/src/main/java/me/okx/twitchsync/Validator.java b/src/main/java/me/okx/twitchsync/Validator.java index 4c6e2a7..a29b81e 100644 --- a/src/main/java/me/okx/twitchsync/Validator.java +++ b/src/main/java/me/okx/twitchsync/Validator.java @@ -13,9 +13,7 @@ import com.nimbusds.oauth2.sdk.id.Issuer; import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; -import me.okx.twitchsync.data.CheckState; -import me.okx.twitchsync.data.MessageWithId; -import me.okx.twitchsync.data.StateWithId; +import me.okx.twitchsync.data.*; import me.okx.twitchsync.data.json.AccessToken; import me.okx.twitchsync.data.json.ChannelObject; import me.okx.twitchsync.data.json.CheckError; @@ -23,32 +21,34 @@ import me.okx.twitchsync.data.json.Users; import me.okx.twitchsync.data.sync.SyncMessage; import me.okx.twitchsync.data.sync.SyncResponse; -import me.okx.twitchsync.data.sync.SyncResponseFailure; -import me.okx.twitchsync.data.sync.SyncResponseSuccess; import me.okx.twitchsync.events.PlayerFollowEvent; import me.okx.twitchsync.events.PlayerSubscriptionEvent; +import me.okx.twitchsync.events.SyncEvent; +import me.okx.twitchsync.util.SqlHelper; import me.okx.twitchsync.util.WebUtil; +import net.milkbowl.vault.permission.Permission; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.text.ParseException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; +import java.util.stream.Collectors; import java.util.stream.Stream; public class Validator { + public static final String SUBSCRIPTIONS = "subscriptions"; + public static final String FOLLOWS = "follows/channels"; private Gson gson = new Gson(); - private Map channels; + private Map channels; private TwitchSync plugin; private Cache userStates; @@ -58,34 +58,79 @@ public Validator(TwitchSync plugin) { .build(); this.plugin = plugin; CompletableFuture.runAsync(() -> { - this.channels = getChannelIds(plugin.getConfig().getStringList("channel-names")); + plugin.debug("Getting Channels.", "Testing"); + List channelList = (List)plugin.getConfig().getList("channels"); + plugin.debug(channelList, "Channels"); + if (channelList == null) channelList = new ArrayList<>(); + this.channels = getChannelMap(channelList); + plugin.debug("Checking channels: " + channelList.stream().map(Channel::getName).collect(Collectors.joining(",")), "Channels"); + plugin.debug("Got channels: " + this.channels.values().stream().map(Channel::getName).collect(Collectors.joining(",")), "Channels"); + if (channelList.size() == 0) { + plugin.debug("Got an empty channel list, writing an example to file.", "Channels"); + Channel example = new Channel(); + example.setName("example"); + Options exampleOptions = new Options(); + exampleOptions.setRank("none"); + exampleOptions.setEnabled(false); + exampleOptions.setCommands(Collections.emptyList()); + exampleOptions.setRevokeCommands(Collections.emptyList()); + example.setSubscribe(exampleOptions); + example.setFollow(exampleOptions); + channelList.add(example); + plugin.getConfig().set("channels", channelList); + plugin.saveConfig(); + } }); } - public String getChannelName(int channelId) { + private List getList(String path, Class clazz) { + List _list = plugin.getConfig().getList(path); + List list = new ArrayList<>(); + if (_list == null) return list; + plugin.debug(_list, "Channels"); + + for (Object t : _list) { + if (clazz.isInstance(t)) { + list.add(clazz.cast(t)); + } + } + return list; + } + + public Channel getChannel(int channelId) { return channels.get(channelId); } - private Map getChannelIds(List channelNames) { - Map headers = new HashMap<>(); + private Map getChannelMap(List channels) { + try { + Map headers = new HashMap<>(); - headers.put("Client-ID", plugin.getConfig().getString("client-id")); - headers.put("Accept", "application/vnd.twitchtv.v5+json"); + headers.put("Client-ID", plugin.getConfig().getString("client-id")); + headers.put("Accept", "application/vnd.twitchtv.v5+json"); - Reader reader = WebUtil.getURL("https://api.twitch.tv/kraken/users?login=" - + String.join(",", channelNames), headers); - Users users = plugin.debug(gson.fromJson(reader, Users.class), "Users"); + if (channels.size() == 0) { + plugin.debug("No channels found", "Error"); + return new HashMap<>(); + } - if (users.getTotal() == 0) { - plugin.getLogger().log(Level.SEVERE, "No channels found"); - return null; - } + Reader reader = WebUtil.getURL("https://api.twitch.tv/kraken/users?login=" + + channels.stream().map(Channel::getName).collect(Collectors.joining(",")), headers); + Users users = plugin.debug(gson.fromJson(reader, Users.class), "Users"); - Map channels = new HashMap<>(); - for (User user : users.getUsers()) { - channels.put(user.getId(), user.getName()); + if (users.getTotal() == 0) { + plugin.debug("No channels found", "Error"); + return new HashMap<>(); + } + + Map channelMap = new HashMap<>(); + for (User user : users.getUsers()) { + channelMap.put(user.getId(), channels.stream().filter(c -> c.getName().equals(user.getName())).findFirst().get()); + } + return channelMap; + } catch (Exception ex) { + plugin.debug(ex); + return new HashMap<>(); } - return channels; } public String createAuthenticationUrl(UUID user) { @@ -100,146 +145,7 @@ public String createAuthenticationUrl(UUID user) { "&state=" + uuid; } - public SyncResponse sync(UUID withState, String code) { - plugin.debug("States: " + userStates.asMap()); - - UUID uuid = userStates.getIfPresent(withState); - if (uuid == null) { - return new SyncResponseFailure(SyncMessage.STATE_NOT_FOUND); - } - userStates.invalidate(withState); - - // safe to run blocking sql as this is run on web server thread - Optional following = plugin.getSqlHelper().isFollowing(uuid); - Optional subscribed = plugin.getSqlHelper().isSubscribed(uuid); - - if (!subscribed.isPresent() || !following.isPresent()) { - return new SyncResponseFailure(SyncMessage.UNKNOWN_ERROR); - } - - Player user = Bukkit.getPlayer(uuid); - - if (user == null) { - return new SyncResponseFailure(SyncMessage.PLAYER_NOT_FOUND); - } - - AccessToken token; - try { - token = plugin.debug(getAccessToken(code)); - } catch (Exception ex) { - plugin.debug(ex); - return new SyncResponseFailure(SyncMessage.UNKNOWN_ERROR); - } - - - try { - String userId = plugin.debug(getUserId(token), "User ID"); - plugin.getSqlHelper().setToken(uuid, userId, token.getAccessToken(), token.getRefreshToken()); - - MessageWithId subscriptionMessage = getSubscriptionMessage(userId, token); - if (subscriptionMessage.getMessage() == SyncMessage.SUBSCRIPTION_SUCCESS) { - if (plugin.debug(subscribed.get(), "Subscribed")) { - subscriptionMessage.setMessage(SyncMessage.ALREADY_DONE); - } else { - Bukkit.getScheduler().runTask(plugin, () -> - Bukkit.getPluginManager().callEvent( - new PlayerSubscriptionEvent(user, subscriptionMessage.getChannelId().get()))); - plugin.getSqlHelper().setSubscribed(uuid, true); - } - } - - MessageWithId followingMessage = getFollowingMessage(userId, token); - if (followingMessage.getMessage() == SyncMessage.FOLLOW_SUCCESS) { - if (plugin.debug(following.get(), "Followed")) { - followingMessage.setMessage(SyncMessage.ALREADY_DONE); - } else { - Bukkit.getScheduler().runTask(plugin, () -> - Bukkit.getPluginManager().callEvent( - new PlayerFollowEvent(user, followingMessage.getChannelId().get()))); - plugin.getSqlHelper().setFollowing(uuid, true); - } - } - - return new SyncResponseSuccess( - followingMessage.getMessage(), subscriptionMessage.getMessage()); - } catch (Exception ex) { - // TODO: More information based on error type - plugin.debug(ex); - return null; - } - } - - private MessageWithId getSubscriptionMessage(String userId, AccessToken token) { - try { - StateWithId subscriptionState = plugin.debug( - getSubscriptionState(userId, token).sorted().findFirst().get(), - "Subscribe state"); - - return new MessageWithId( - mapState(subscriptionState.getState(), SyncMessage.SUBSCRIPTION_SUCCESS), - subscriptionState.getId()); - } catch (Exception ex) { - // TODO: More information based on error type - plugin.debug(ex); - } - return new MessageWithId(SyncMessage.UNKNOWN_ERROR); - } - - private MessageWithId getFollowingMessage(String userId, AccessToken token) { - try { - StateWithId followingState = plugin.debug( - getFollowingState(userId, token).sorted().findFirst().get(), - "Follow state"); - - return new MessageWithId( - mapState(followingState.getState(), SyncMessage.FOLLOW_SUCCESS), - followingState.getId()); - } catch (Exception ex) { - // TODO: More information based on error type - plugin.debug(ex); - } - return new MessageWithId(SyncMessage.UNKNOWN_ERROR); - } - - private SyncMessage mapState(CheckState state, SyncMessage success) { - switch (state) { - case YES: - return success; - case NO: - return SyncMessage.NOT_BOTH; - case UNPROCESSABLE: - return SyncMessage.NO_SUBSCRIPTION_PROGRAM; - default: - return null; - } - } - - private AccessToken getAccessToken(String code) { - Reader reader = WebUtil.getURL("https://id.twitch.tv/oauth2/token" + - "?client_id=" + plugin.getConfig().getString("client-id") + - "&client_secret=" + plugin.getConfig().getString("client-secret") + - "&code=" + code + - "&grant_type=authorization_code" + - "&redirect_uri=" + plugin.getConfig().getString("redirect-uri"), - new HashMap<>(), "POST"); - return gson.fromJson(reader, AccessToken.class); - } - private String getUserId(AccessToken token) throws IOException, ParseException, BadJOSEException, JOSEException { - /*AccessTokenVerifier verifier = JwtVerifiers.accessTokenVerifierBuilder() - .setIssuer("https://id.twitch.tv/oauth2/keys") - .setConnectionTimeout(Duration.ofSeconds(2)) // defaults to 1000ms - .setReadTimeout(Duration.ofSeconds(1)) // defaults to 1000ms - .build(); - JwtVerifier verifier = new JwtHelper() - .setIssuerUrl("https://id.twitch.tv/oauth2") - .setConnectionTimeout(2000) - .setReadTimeout(2000) - .setClientId(plugin.getConfig().getString("client-id")) - .build(); - Jwt jwt = verifier.decode(token.getIdToken()); - plugin.debug(jwt.getClaims(), "claims"); - return (String) jwt.getClaims().get("sub");*/ Issuer iss = new Issuer("https://id.twitch.tv/oauth2"); ClientID clientID = new ClientID(plugin.getConfig().getString("client-id")); @@ -255,45 +161,48 @@ private String getUserId(AccessToken token) throws IOException, ParseException, return claims.getSubject().getValue(); } - public Stream getSubscriptionState(String userId, AccessToken token) { - return getStates(userId, token, "subscriptions"); + private AccessToken getAccessToken(String code) { + Reader reader = WebUtil.getURL("https://id.twitch.tv/oauth2/token" + + "?client_id=" + plugin.getConfig().getString("client-id") + + "&client_secret=" + plugin.getConfig().getString("client-secret") + + "&code=" + code + + "&grant_type=authorization_code" + + "&redirect_uri=" + plugin.getConfig().getString("redirect-uri"), + new HashMap<>(), "POST"); + return gson.fromJson(reader, AccessToken.class); } - public Stream getFollowingState(String userId, AccessToken token) { - return getStates(userId, token, "follows/channels"); - } - private Stream getStates(String userId, AccessToken token, String type) { + private Stream getStates(Token token, Map channels, String type) { Map headers = new HashMap<>(); headers.put("Client-ID", plugin.getConfig().getString("client-id")); headers.put("Accept", "application/vnd.twitchtv.v5+json"); - headers.put("Authorization", "OAuth " + token.getAccessToken()); + headers.put("Authorization", "OAuth " + token.getAccessToken().getAccessToken()); - return channels.keySet().stream() - .map(channelId -> getIndividualState(headers, - "https://api.twitch.tv/kraken/users/" + userId + "/" + type + "/", channelId)) - .peek(s -> plugin.debug(s, "Peek")); + return channels.entrySet().stream() + .map(entry -> getIndividualState(entry.getValue(), headers, + "https://api.twitch.tv/kraken/users/" + token.getId() + "/" + type + "/", entry.getKey())); } - private StateWithId getIndividualState(Map headers, String url, int channelId) { - InputStreamReader reader = WebUtil.getURL(url + channelId, headers); + private StateWithId getIndividualState(Channel channel, Map headers, String url, int channelId) { + InputStreamReader reader = WebUtil.getURL(plugin.debug(url + channelId, "Sync"), headers); JsonElement json = gson.fromJson(reader, JsonElement.class); ChannelObject object = gson.fromJson(json, ChannelObject.class); if (object.isValid()) { - return new StateWithId(CheckState.YES, channelId); + return new StateWithId(CheckState.YES, channel, channelId); } // user is not subscribed CheckError error = gson.fromJson(json, CheckError.class); switch (error.getStatus()) { case 404: - return new StateWithId(CheckState.NO, channelId); + return new StateWithId(CheckState.NO, channel, channelId); case 422: - return new StateWithId(CheckState.UNPROCESSABLE, channelId); + return new StateWithId(CheckState.UNPROCESSABLE, channel, channelId); default: plugin.debug("Check state: " + error); - return new StateWithId(CheckState.ERROR, channelId); + return new StateWithId(CheckState.ERROR, channel, channelId); } } @@ -306,4 +215,145 @@ public AccessToken refreshToken(String refreshToken) { new HashMap<>(), "POST"); return gson.fromJson(reader, AccessToken.class); } + + public CompletableFuture store(UUID uuid, String code) { + try { + AccessToken token = getAccessToken(code); + return plugin.getSqlHelper().setToken(uuid, getUserId(token), token); + } catch (IOException | ParseException | BadJOSEException | JOSEException ex) { + return CompletableFuture.completedFuture(null); + } + } + + public CompletableFuture sync(Map users) { + return CompletableFuture.runAsync(() -> users.forEach(this::sync)); + } + + public CompletableFuture sync(UUID uuid) { + return CompletableFuture.supplyAsync(() -> + plugin.getSqlHelper().getToken(uuid).map(token -> + sync(plugin.debug(uuid), plugin.debug(token)).join()) + .orElse(SyncResponse.of(SyncMessage.NO_TOKEN))); + } + + public CompletableFuture sync(UUID uuid, Token token) { + return CompletableFuture.supplyAsync(() -> { + if (token == null) { + return SyncResponse.of(SyncMessage.NO_TOKEN); + } + + if (channels == null) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + player.sendMessage(ChatColor.DARK_RED + "No channels available, contact your administrator."); + } + plugin.getLogger().info("No channels found to check for subscriptions."); + return SyncResponse.of(SyncMessage.SUCCESS); + } + + Token _token = refreshAndSaveToken(uuid, token); + + if (_token == null) { + _token = plugin.getSqlHelper().getToken(uuid).get(); + } + + if (_token.getId() == null) { + try { + _token.setId(getUserId(_token.getAccessToken())); + } catch (Exception ex) { + plugin.debug(ex); + } + } + plugin.debug("ID: " + _token.getId(), "Sync"); + + Stream subscribeStates = getStates(_token, channels, SUBSCRIPTIONS); + List subscriptions = syncStates(uuid, subscribeStates, Channel::getSubscribe, SqlHelper::setSubscribed, PlayerSubscriptionEvent.class); + + Stream followStates = getStates(_token, channels, FOLLOWS); + List follows = syncStates(uuid, followStates, Channel::getFollow, SqlHelper::setFollowing, PlayerFollowEvent.class); + + Integer subscriptionCount = subscriptions.size(); + // get highest upgrade + // get upgrades + List upgrades = getList("upgrades", Upgrade.class); + upgrades.sort(Comparator.comparingInt(Upgrade::getThreshold)); + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + Permission perms = plugin.getPerms(); + + for (Upgrade u : upgrades) { + if (u.getThreshold() <= subscriptions.size()) { + if (!perms.playerInGroup(null, player, u.getRank())) { + perms.playerAddGroup(null, player, u.getRank()); + } + } else if (perms.playerInGroup(null, player, u.getRank())) { + perms.playerRemoveGroup(null, player, u.getRank()); + } + } + + // todo: check total subscription count for upgrades + return new SyncResponse(SyncMessage.SUCCESS, subscriptions, follows); + }); + } + + private List syncStates(UUID uuid, Stream states, OptionSupplier optionSupplier, Persistence persistence, Class eventClass) { + List subscriptions = new ArrayList<>(); + SqlHelper helper = plugin.getSqlHelper(); + Permission perms = plugin.getPerms(); + states.forEach(state -> { + Boolean active = null; + if (state.getState() == CheckState.YES) active = true; + else if (state.getState() == CheckState.NO) active = false; + Channel channel = state.getChannel(); + plugin.debug(uuid.toString() + " - " + channel.getName() + ": " + state.getState().toString(), "Sync"); + if (active != null) { + if (active) subscriptions.add(state.getChannel()); + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) { // player is online + Options options = optionSupplier.supply(channel); + Boolean wasActive = helper.isSubscribed(uuid, channel.getName()); + persistence.persist(helper, uuid, channel.getName(), active); + if (!wasActive && active) { + Bukkit.getScheduler().runTask(plugin, () -> + { + try { + SyncEvent event = eventClass.getConstructor(Player.class, Channel.class).newInstance(player, channel); + Bukkit.getPluginManager().callEvent(event); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { + e.printStackTrace(); + } + }); + } else if (wasActive && !active) { + // remove manually until event is implemented + if (perms.playerInGroup(null, player, options.getRank())) { + perms.playerRemoveGroup(null, player, options.getRank()); + } + + for (String command : options.getRevokeCommands()) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command + .replace("%player%", player.getName()) + .replace("%channel%", channel.getName())); + } + + } + } else { + plugin.debug("Got subscription or follow but player is offline, ignoring.", "Sync"); + } + } + }); + return subscriptions; + } + + private Token refreshAndSaveToken(UUID uuid, Token token) { + AccessToken newToken = refreshToken(token.getAccessToken().getRefreshToken()); + try { + return plugin.getSqlHelper().setToken(uuid, token.getId(), newToken).get(); + } catch (Exception ex) { + plugin.debug(ex); + return null; + } + } + + public UUID getUUIDFromAuthState(UUID stateUUID) { + return this.userStates.getIfPresent(stateUUID); + } } diff --git a/src/main/java/me/okx/twitchsync/data/Channel.java b/src/main/java/me/okx/twitchsync/data/Channel.java new file mode 100644 index 0000000..aae4994 --- /dev/null +++ b/src/main/java/me/okx/twitchsync/data/Channel.java @@ -0,0 +1,58 @@ +package me.okx.twitchsync.data; + +import com.google.common.eventbus.Subscribe; +import org.bukkit.ChatColor; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.SerializableAs; + +import java.util.HashMap; +import java.util.Map; + +@SerializableAs("channel") +public class Channel implements ConfigurationSerializable { + + private String name; + private Options subscribe; + private Options follow; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Options getSubscribe() { + return subscribe; + } + + public void setSubscribe(Options subscribe) { + this.subscribe = subscribe; + } + + public Options getFollow() { + return follow; + } + + public void setFollow(Options follow) { + this.follow = follow; + } + + @Override + public Map serialize() { + Map serialised = new HashMap<>(); + serialised.put("name", this.name); + serialised.put("subscribe", this.subscribe); + serialised.put("follow", this.follow); + return serialised; + } + + public static Channel deserialize(Map map) { + Channel channel = new Channel(); + channel.setName((String)map.get("name")); + channel.setSubscribe((Options)map.get("subscribe")); + channel.setFollow((Options)map.get("follow")); + return channel; + } +} diff --git a/src/main/java/me/okx/twitchsync/data/OptionSupplier.java b/src/main/java/me/okx/twitchsync/data/OptionSupplier.java new file mode 100644 index 0000000..ca36133 --- /dev/null +++ b/src/main/java/me/okx/twitchsync/data/OptionSupplier.java @@ -0,0 +1,7 @@ +package me.okx.twitchsync.data; + +public interface OptionSupplier { + + public Options supply(Channel channel); + +} diff --git a/src/main/java/me/okx/twitchsync/data/Options.java b/src/main/java/me/okx/twitchsync/data/Options.java new file mode 100644 index 0000000..e5cf80b --- /dev/null +++ b/src/main/java/me/okx/twitchsync/data/Options.java @@ -0,0 +1,68 @@ +package me.okx.twitchsync.data; + +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.SerializableAs; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SerializableAs("options") +public class Options implements ConfigurationSerializable { + + private Boolean enabled; + private String rank; + private List commands; + private List revokeCommands; + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getRank() { + return rank; + } + + public void setRank(String rank) { + this.rank = rank; + } + + public List getCommands() { + return commands; + } + + public void setCommands(List commands) { + this.commands = commands; + } + + public List getRevokeCommands() { + return revokeCommands; + } + + public void setRevokeCommands(List revokeCommands) { + this.revokeCommands = revokeCommands; + } + + @Override + public Map serialize() { + Map serialised = new HashMap<>(); + serialised.put("enabled", this.enabled); + serialised.put("rank", this.rank); + serialised.put("commands", this.commands); + serialised.put("revoke-commands", this.revokeCommands); + return serialised; + } + + public static Options deserialize(Map map) { + Options options = new Options(); + options.setEnabled((Boolean)map.get("enabled")); + options.setRank((String)map.get("rank")); + options.setCommands((List)map.get("commands")); + options.setRevokeCommands((List)map.get("revoke-commands")); + return options; + } +} diff --git a/src/main/java/me/okx/twitchsync/data/Persistence.java b/src/main/java/me/okx/twitchsync/data/Persistence.java new file mode 100644 index 0000000..71a9c89 --- /dev/null +++ b/src/main/java/me/okx/twitchsync/data/Persistence.java @@ -0,0 +1,11 @@ +package me.okx.twitchsync.data; + +import me.okx.twitchsync.util.SqlHelper; + +import java.util.UUID; + +public interface Persistence { + + public void persist(SqlHelper helper, UUID uuid, String channel, Boolean state); + +} diff --git a/src/main/java/me/okx/twitchsync/data/StateWithId.java b/src/main/java/me/okx/twitchsync/data/StateWithId.java index ef4dd09..5576018 100644 --- a/src/main/java/me/okx/twitchsync/data/StateWithId.java +++ b/src/main/java/me/okx/twitchsync/data/StateWithId.java @@ -3,10 +3,16 @@ public class StateWithId implements Comparable { private CheckState state; private int id; + private Channel channel; - public StateWithId(CheckState state, int id) { + public StateWithId(CheckState state, Channel channel, int id) { this.state = state; this.id = id; + this.channel = channel; + } + + public Channel getChannel() { + return channel; } public int getId() { diff --git a/src/main/java/me/okx/twitchsync/data/Token.java b/src/main/java/me/okx/twitchsync/data/Token.java index 6b89a63..80153b2 100644 --- a/src/main/java/me/okx/twitchsync/data/Token.java +++ b/src/main/java/me/okx/twitchsync/data/Token.java @@ -18,4 +18,12 @@ public Token(String id, AccessToken accessToken) { this.id = id; this.accessToken = accessToken; } + + public void setAccessToken(AccessToken token) { + this.accessToken = token; + } + + public void setId(String userId) { + this.id = userId; + } } diff --git a/src/main/java/me/okx/twitchsync/data/Upgrade.java b/src/main/java/me/okx/twitchsync/data/Upgrade.java new file mode 100644 index 0000000..2b721c8 --- /dev/null +++ b/src/main/java/me/okx/twitchsync/data/Upgrade.java @@ -0,0 +1,47 @@ +package me.okx.twitchsync.data; + +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.SerializableAs; + +import java.util.HashMap; +import java.util.Map; + +@SerializableAs("upgrade") +public class Upgrade implements ConfigurationSerializable { + + private Integer threshold; + private String rank; + + public Upgrade(Integer threshold, String rank) { + this.threshold = threshold; + this.rank = rank; + } + + public Integer getThreshold() { + return threshold; + } + + public void setThreshold(Integer threshold) { + this.threshold = threshold; + } + + public String getRank() { + return rank; + } + + public void setRank(String rank) { + this.rank = rank; + } + + @Override + public Map serialize() { + Map serialised = new HashMap<>(); + serialised.put("rank", this.rank); + serialised.put("threshold", this.threshold); + return serialised; + } + + public static Upgrade deserialize(Map map) { + return new Upgrade((Integer)map.get("threshold"), (String)map.get("rank")); + } +} diff --git a/src/main/java/me/okx/twitchsync/data/sync/SyncMessage.java b/src/main/java/me/okx/twitchsync/data/sync/SyncMessage.java index 46fd59c..894d7a5 100644 --- a/src/main/java/me/okx/twitchsync/data/sync/SyncMessage.java +++ b/src/main/java/me/okx/twitchsync/data/sync/SyncMessage.java @@ -12,6 +12,8 @@ public enum SyncMessage { UNKNOWN_ERROR("unknown-error"), BOTH_SUCCESS("success-both"), NOT_BOTH("not-both"), + SUCCESS("success"), + NO_TOKEN("no-token"), ALREADY_DONE("already-done"); private String config; diff --git a/src/main/java/me/okx/twitchsync/data/sync/SyncResponse.java b/src/main/java/me/okx/twitchsync/data/sync/SyncResponse.java index 3109684..66011b0 100644 --- a/src/main/java/me/okx/twitchsync/data/sync/SyncResponse.java +++ b/src/main/java/me/okx/twitchsync/data/sync/SyncResponse.java @@ -1,5 +1,36 @@ package me.okx.twitchsync.data.sync; -public abstract class SyncResponse { +import me.okx.twitchsync.data.Channel; + +import java.util.Collections; +import java.util.List; + +public class SyncResponse { + + private SyncMessage message; + private List subscriptions; + private List follows; + + public SyncResponse(SyncMessage message, List subscriptions, List follows) { + this.message = message; + this.subscriptions = subscriptions; + this.follows = follows; + } + + public static SyncResponse of(SyncMessage message) { + return new SyncResponse(message, Collections.emptyList(), Collections.emptyList()); + } + + public List getSubscriptions() { + return subscriptions; + } + + public List getFollows() { + return follows; + } + + public SyncMessage getMessage() { + return message; + } } diff --git a/src/main/java/me/okx/twitchsync/data/sync/SyncResponseFailure.java b/src/main/java/me/okx/twitchsync/data/sync/SyncResponseFailure.java deleted file mode 100644 index 3a0fbf5..0000000 --- a/src/main/java/me/okx/twitchsync/data/sync/SyncResponseFailure.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.okx.twitchsync.data.sync; - -public class SyncResponseFailure extends SyncResponse { - private SyncMessage message; - - public SyncResponseFailure(SyncMessage message) { - this.message = message; - } - - public SyncMessage getMessage() { - return message; - } -} diff --git a/src/main/java/me/okx/twitchsync/data/sync/SyncResponseSuccess.java b/src/main/java/me/okx/twitchsync/data/sync/SyncResponseSuccess.java deleted file mode 100644 index 47e5eca..0000000 --- a/src/main/java/me/okx/twitchsync/data/sync/SyncResponseSuccess.java +++ /dev/null @@ -1,27 +0,0 @@ -package me.okx.twitchsync.data.sync; - -public class SyncResponseSuccess extends SyncResponse { - private SyncMessage follow; - private SyncMessage subscribe; - - public SyncResponseSuccess(SyncMessage follow, SyncMessage subscribe) { - this.follow = follow; - this.subscribe = subscribe; - } - - public boolean isFollowing() { - return follow == SyncMessage.FOLLOW_SUCCESS; - } - - public SyncMessage getFollowMessage() { - return follow; - } - - public boolean isSubscribed() { - return subscribe == SyncMessage.SUBSCRIPTION_SUCCESS; - } - - public SyncMessage getSubscribeMessage() { - return subscribe; - } -} diff --git a/src/main/java/me/okx/twitchsync/events/PlayerFollowEvent.java b/src/main/java/me/okx/twitchsync/events/PlayerFollowEvent.java index 0348b73..e408704 100644 --- a/src/main/java/me/okx/twitchsync/events/PlayerFollowEvent.java +++ b/src/main/java/me/okx/twitchsync/events/PlayerFollowEvent.java @@ -1,30 +1,21 @@ package me.okx.twitchsync.events; +import me.okx.twitchsync.data.Channel; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; -import org.bukkit.event.player.PlayerEvent; -public class PlayerFollowEvent extends PlayerEvent { +public class PlayerFollowEvent extends SyncEvent { private static final HandlerList handlers = new HandlerList(); - private int channelId; - /** * Fired when a player subscription event is triggered * * @param who The player who subscribed */ - public PlayerFollowEvent(Player who, int channelId) { - super(who); - this.channelId = channelId; + public PlayerFollowEvent(Player who, Channel channel) { + super(who, channel); } - /** - * @return The channel ID the user has subscribed to. - */ - public int getChannelId() { - return channelId; - } @Override public HandlerList getHandlers() { diff --git a/src/main/java/me/okx/twitchsync/events/PlayerListener.java b/src/main/java/me/okx/twitchsync/events/PlayerListener.java index e78014b..cd7a34d 100644 --- a/src/main/java/me/okx/twitchsync/events/PlayerListener.java +++ b/src/main/java/me/okx/twitchsync/events/PlayerListener.java @@ -1,8 +1,10 @@ package me.okx.twitchsync.events; import me.okx.twitchsync.TwitchSync; +import me.okx.twitchsync.data.Channel; +import me.okx.twitchsync.data.OptionSupplier; +import me.okx.twitchsync.data.Options; import org.bukkit.Bukkit; -import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -16,32 +18,33 @@ public PlayerListener(TwitchSync plugin) { @EventHandler public void on(PlayerSubscriptionEvent e) { - handle("subscribe", e.getPlayer(), e.getChannelId()); + + plugin.debug("got new subscribe event", "Event"); + handle(Channel::getSubscribe, e.getPlayer(), e.getChannel()); } @EventHandler public void on(PlayerFollowEvent e) { - handle("follow", e.getPlayer(), e.getChannelId()); + plugin.debug("got new follow event", "Event"); + handle(Channel::getFollow, e.getPlayer(), e.getChannel()); } - private void handle(String path, Player player, int channelId) { - ConfigurationSection config = plugin.getConfig().getConfigurationSection(path); - if(!config.getBoolean("enabled")) { + private void handle(OptionSupplier optionSupplier, Player player, Channel channel) { + Options options = optionSupplier.supply(channel); + if(!options.getEnabled()) { + plugin.debug("Event not processed, configuration section disabled.", "Event"); return; } - String channel = plugin.getValidator().getChannelName(channelId); - - String group = config.getString("rank"); + String group = options.getRank(); if (!group.equalsIgnoreCase("none") && plugin.getPerms() != null) { plugin.getPerms().playerAddGroup(null, player, group); } - for (String command : config.getStringList("commands")) { + for (String command : options.getCommands()) { Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command - .replace("%name%", player.getName()) - .replace("%channel%", channel) - .replace("%channelid%", channelId + "")); + .replace("%player%", player.getName()) + .replace("%channel%", channel.getName())); } } } diff --git a/src/main/java/me/okx/twitchsync/events/PlayerSubscriptionEvent.java b/src/main/java/me/okx/twitchsync/events/PlayerSubscriptionEvent.java index 8f602c5..b4d8c41 100644 --- a/src/main/java/me/okx/twitchsync/events/PlayerSubscriptionEvent.java +++ b/src/main/java/me/okx/twitchsync/events/PlayerSubscriptionEvent.java @@ -1,29 +1,19 @@ package me.okx.twitchsync.events; +import me.okx.twitchsync.data.Channel; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; -import org.bukkit.event.player.PlayerEvent; -public class PlayerSubscriptionEvent extends PlayerEvent { +public class PlayerSubscriptionEvent extends SyncEvent { private static final HandlerList handlers = new HandlerList(); - private int channelId; - /** * Fired when a player subscription event is triggered * * @param who The player who subscribed */ - public PlayerSubscriptionEvent(Player who, int channelId) { - super(who); - this.channelId = channelId; - } - - /** - * @return The channel ID the user has subscribed to. - */ - public int getChannelId() { - return channelId; + public PlayerSubscriptionEvent(Player who, Channel channel) { + super(who, channel); } @Override diff --git a/src/main/java/me/okx/twitchsync/events/SyncEvent.java b/src/main/java/me/okx/twitchsync/events/SyncEvent.java new file mode 100644 index 0000000..d555ca0 --- /dev/null +++ b/src/main/java/me/okx/twitchsync/events/SyncEvent.java @@ -0,0 +1,24 @@ +package me.okx.twitchsync.events; + +import me.okx.twitchsync.data.Channel; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; + +public abstract class SyncEvent extends PlayerEvent { + + protected Channel channel; + + public SyncEvent(Player who, Channel channel) { + super(who); + this.channel = channel; + } + + /** + * @return The channel ID the user has subscribed to. + */ + public Channel getChannel() { + return channel; + } + +} diff --git a/src/main/java/me/okx/twitchsync/util/SqlHelper.java b/src/main/java/me/okx/twitchsync/util/SqlHelper.java index 9c652b8..21b3ccb 100644 --- a/src/main/java/me/okx/twitchsync/util/SqlHelper.java +++ b/src/main/java/me/okx/twitchsync/util/SqlHelper.java @@ -5,12 +5,7 @@ import me.okx.twitchsync.data.json.AccessToken; import java.nio.file.Path; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -28,8 +23,8 @@ public SqlHelper(TwitchSync plugin) { connection = DriverManager.getConnection("jdbc:sqlite:" + db); Statement stmt = connection.createStatement(); - stmt.execute("CREATE TABLE IF NOT EXISTS subscribed (uuid VARCHAR(36) PRIMARY KEY)"); - stmt.execute("CREATE TABLE IF NOT EXISTS following (uuid VARCHAR(36) PRIMARY KEY)"); + stmt.execute("CREATE TABLE IF NOT EXISTS subscribed (uuid VARCHAR(36), channel VARCHAR(25), PRIMARY KEY (uuid, channel))"); + stmt.execute("CREATE TABLE IF NOT EXISTS following (uuid VARCHAR(36), channel VARCHAR(25), PRIMARY KEY (uuid, channel))"); stmt.execute("CREATE TABLE IF NOT EXISTS tokens " + "(uuid VARCHAR(36) PRIMARY KEY, " + "id VARCHAR(12), " + @@ -41,71 +36,51 @@ public SqlHelper(TwitchSync plugin) { }); } - public Optional getFollowing() { - return getCount("following"); + public Boolean isFollowing(UUID uuid, String channel) { + return isInTable("following", uuid, channel); } - public Optional getSubscribed() { - return getCount("subscribed"); + public Boolean isSubscribed(UUID uuid, String channel) { + return isInTable("subscribed", uuid, channel); } - private Optional getCount(String table) { - try(PreparedStatement stmt = connection.prepareStatement( - "SELECT * FROM " + table)) { - ResultSet rs = stmt.executeQuery(); - int count = 0; - while(rs.next()) { - count++; - } - return Optional.of(count); - } catch (SQLException e) { - e.printStackTrace(); - return Optional.empty(); - } - } - - public Optional isFollowing(UUID uuid) { - return isInTable(uuid, "following"); - } - - public Optional isSubscribed(UUID uuid) { - return isInTable(uuid, "subscribed"); - } - - private Optional isInTable(UUID uuid, String table) { - try(PreparedStatement stmt = connection.prepareStatement( - "SELECT * FROM " + table + " WHERE uuid=?")) { + private Boolean isInTable(String table, UUID uuid, String channel) { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT * FROM " + table + " WHERE uuid = ? AND channel = ?")) { stmt.setString(1, uuid.toString()); + stmt.setString(2, channel); stmt.execute(); - return Optional.of(stmt.getResultSet().next()); + return stmt.getResultSet().next(); } catch (SQLException e) { e.printStackTrace(); - return Optional.empty(); + return false; } } - public void setToken(UUID uuid, String id, String accessToken, String refeshToken) { - CompletableFuture.runAsync(() -> { - try(PreparedStatement stmt = connection.prepareStatement( + public CompletableFuture setToken(UUID uuid, String userId, AccessToken token) { + return CompletableFuture.supplyAsync(() -> { + try (PreparedStatement stmt = connection.prepareStatement( "REPLACE INTO tokens (uuid, id, access_token, refresh_token) VALUES (?, ?, ?, ?)")) { stmt.setString(1, uuid.toString()); - stmt.setString(2, id); - stmt.setString(3, accessToken); - stmt.setString(4, refeshToken); + stmt.setString(2, userId); + stmt.setString(3, token.getAccessToken()); + stmt.setString(4, token.getRefreshToken()); stmt.execute(); + return new Token(userId, token); } catch (SQLException e) { e.printStackTrace(); + return null; } }); } public Optional> getTokens() { - try(PreparedStatement stmt = connection.prepareStatement("SELECT * FROM tokens")) { + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM tokens")) { ResultSet rs = stmt.executeQuery(); Map tokens = new HashMap<>(); - while(rs.next()) { + while (rs.next()) { tokens.put(UUID.fromString(rs.getString("uuid")), new Token(rs.getString("id"), new AccessToken(rs.getString("access_token"), rs.getString("refresh_token")))); @@ -118,40 +93,42 @@ public Optional> getTokens() { } } - public void setFollowing(UUID uuid, boolean following) { + public void setFollowing(UUID uuid, String channel, boolean following) { CompletableFuture.runAsync(() -> { - if (following) { - addToTable(uuid, "following"); - } else { - deleteFromTable(uuid, "following"); + if (following && !isFollowing(uuid, channel)) { + addToTable("following", uuid, channel); + } else if (!following && isFollowing(uuid, channel)) { + deleteFromTable("following", uuid, channel); } }); } - public void setSubscribed(UUID uuid, boolean subscribed) { + public void setSubscribed(UUID uuid, String channel, boolean subscribed) { CompletableFuture.runAsync(() -> { - if (subscribed) { - addToTable(uuid, "subscribed"); - } else { - deleteFromTable(uuid, "subscribed"); + if (subscribed && !isSubscribed(uuid, channel)) { + addToTable("subscribed", uuid, channel); + } else if (!subscribed && isSubscribed(uuid, channel)) { + deleteFromTable("subscribed", uuid, channel); } }); } - private void addToTable(UUID uuid, String database) { - try(PreparedStatement stmt = connection.prepareStatement( - "INSERT INTO " + database + " (uuid) VALUES (?)")) { + private void addToTable(String table, UUID uuid, String channel) { + try (PreparedStatement stmt = connection.prepareStatement( + "INSERT INTO " + table + " (uuid, channel) VALUES (?, ?)")) { stmt.setString(1, uuid.toString()); + stmt.setString(2, channel); stmt.execute(); } catch (SQLException e) { e.printStackTrace(); } } - private void deleteFromTable(UUID uuid, String database) { - try(PreparedStatement stmt = connection.prepareStatement( - "DELETE FROM " + database + " WHERE uuid=?")) { + private void deleteFromTable(String table, UUID uuid, String channel) { + try (PreparedStatement stmt = connection.prepareStatement( + "DELETE FROM " + table + " WHERE uuid = ? AND channel = ?")) { stmt.setString(1, uuid.toString()); + stmt.setString(2, channel); stmt.execute(); } catch (SQLException e) { e.printStackTrace(); @@ -165,4 +142,19 @@ public void close() { e.printStackTrace(); } } + + public Optional getToken(UUID uuid) { + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM tokens WHERE uuid = ?")) { + stmt.setString(1, uuid.toString()); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + Token token = new Token(rs.getString("id"), + new AccessToken(rs.getString("access_token"), rs.getString("refresh_token"))); + return Optional.of(token); + } + return Optional.empty(); + } catch (SQLException ex) { + return Optional.empty(); + } + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3a9d9de..82eef07 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,12 +1,37 @@ # The channel name(s) to check for subscription/follows. -channel-names: -- required +channels: + - ==: channel + name: example + subscribe: + ==: options + enabled: true + rank: subscriber + # %player% for the player name, %channel% for the channel name (as seen above) + commands: + - 'broadcast %player% subscribed to %channel%' + revoke-commands: + - 'w %player% you are no longer subscribed to %channel%.' + follow: + ==: options + enabled: false + rank: none + commands: + - 'broadcast %player% followed to %channel%' + revoke-commands: + - 'w %player% you are no longer following %channel%.' + +upgrades: + - ==: upgrade + # rank to assign for this upgrade + rank: someupgrade + # amount of subscriptions needed for this upgrade + threshold: 2 # The client ID given by Twitch. -client-id: required +client-id: # The client secret given by Twitch. Remember not to tell anyone this! -client-secret: required +client-secret: # Port to host the HTTP server on server-port: 8080 @@ -19,9 +44,11 @@ expiry-time: 10 # Note that these messages are sent by the web server and use HTML, not chat formatting. messages: + success: "Successfully linked your account." + no-token: "No token for the current user." subscription: no-subscription-program: "The channel does not have a subscription program set up." - success: "You have successfully received the Twitch subscription rewards." + success: "Successfully set up Twitch Sync." follow: success: "You have successfully received the Twitch following rewards." success-both: "You have received both the following and subscription rewards." @@ -29,7 +56,7 @@ messages: # If you have subscription or following rewards disabled you might want to change this # to say "You are not following" or "You are not subscribed" to make it clear # that it's only possible to get rewards for one thing - not-both: "You are neither subscribed or are following the channel." + not-both: "You are not subscribed to any of the channels." already-done: "You have already redeemed subscription and/or follow rewards." state-not-found: "This link has either expired, or already been used." @@ -37,41 +64,12 @@ messages: invalid-url: "The URL is invalid" unknown-error: "An unknown error occurred. Please try again." -# revoke subscriber/follow rewards every X days +# revoke subscriber/follow rewards every X minutes # this will remove the rank and run revoke-commands -revoke-interval-days: 1 - -# What to do when somebody subscribes -subscribe: - enabled: true - - # Rank to give a player. Only used if Vault and a permissions plugin is installed. - # Set to 'none' to disable. - rank: 'Subscriber' - - # Commands to execute. - # You can use the following placeholders: - # %name% - name of the player who subscribed - # %channel% - channel their subscription is registered to - # %channelid% - channel id of %channel% - commands: - - 'broadcast %name% has subscribed!' - - 'give %name% diamond 1' - - # commands to run when a player is no longer subscribed - # this only supports the %name% placeholder - revoke-commands: - - 'give %name% dirt 1' -# What to do when somebody follows -follow: - enabled: true - rank: 'Follower' - commands: - - 'broadcast %name% has followed the channel!' +# defaults to twice a day +revoke-interval-minutes: 720 - revoke-commands: - - 'broadcast %name% has unfollowed :(' # Enable debugging information. # If you encounter an unexpected error please enable this # and send the results to the dev! -debug-mode: false +debug-mode: false \ No newline at end of file