Skip to content

Commit

Permalink
Merge pull request #13 from GospelBG/feature/multiple-streams
Browse files Browse the repository at this point in the history
Support multiple streams
  • Loading branch information
GospelBG authored Sep 22, 2024
2 parents ea34dca + aad6208 commit 0c2c7a3
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 176 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ These are the only official download mirrors. Any downloads besides of these lin
> [!TIP]
> If needed, you can copy-paste the names of the Channel Points Rewards into a text document for later use.
3. Set your [config.yml](core/src/main/resources/config.yml) up. Adjust the settings and add and setup the actions for rewards, donations... There are two ways to link your Twitch account to the plugin:
3. Set your [config.yml](core/src/main/resources/config.yml) up. Adjust the settings and setup events for rewards, donations... You will need to link a Twitch account in order to connect use the Twitch API (the linked account **needs to own/moderate all Twitch channels** set in config.yml) There are two ways to link your Twitch account to the plugin:

- **Using a key-based authentication** *(recommended)*:
You will need a Client ID and Access token. You can get one mannually or through a website as [Twitch Token Generator](https://twitchtokengenerator.com). Make sure to add all the [needed scopes](#twitch-scopes).
Expand All @@ -30,7 +30,7 @@ These are the only official download mirrors. Any downloads besides of these lin
- **Log in through a browser**:
You won't need any extra modification in your config.yml file. You will just need to run `/twitch link` in-game and open the provided link. You may need to log in your Twitch account and authorise the app. Once you finish the log in process you can close the browser and your account will be linked.
> [!WARNING]
> Currently due to technical limitations **it's only possible to use the browser method if the login link is opened with the same machine the server is being ran on**. You also have to repeat this process each time you start the server or reload the plugin.
> Currently due to API limitations **it's only possible to use the browser method if the login link is opened with the same machine the server is being ran on**. You also have to repeat this process each time you start the server or reload the plugin.
4. Set up permissions for:
- linking/reloading (`chatpointsttv.manage`).
Expand All @@ -45,7 +45,7 @@ These are the only official download mirrors. Any downloads besides of these lin
## **config.yml docs**
To reset the original configuration, delete `config.yml` and reload the plugin. The file will regenerate automatically.
*Sections with a (\*) are required to be changed in order to the plugin to be used.*
* **Channel Username***: The channel that will be listened for rewards, bits and subs.
* **Channel Username***: The channel(s) that will be listened for rewards, bits and subs. (In case of multiple channels, they must be added as a list: `["channel_1", "channel_2", "..."]`)
* **Custom Client ID**: Client ID used for key-based authorization. Leave commented if it's not being used.
* **Custom Access Token**: Access token used for key-based authorization. Leave commented if it's not being used.
* **Show Chat**: If enabled, your stream chat will be shown in-game to all players in the server.
Expand Down Expand Up @@ -110,17 +110,32 @@ Currently, there are 2 types of actions:
> Argument names surrounded by <> means that it is a required argument.
> Arguments surrounded by [] are optional.
You should set up your events in your config file with this format:
You should set up your events in your config file following this format:
```
TYPE_REWARDS:
- KEY:
- Action 1
- Action 2
- ...
```
or
```
TYPE_REWARDS:
- KEY:
- STREAMER:
- Action 1
- Action 2
- ...
- default:
- Action
```
Whereas `TYPE_REWARDS` is replaces with the appropiate config key that is already on the file, `KEY` with the channel points reward name, subscription tier or minimal amount of bits/subs.
> [!IMPORTANT]
> **For follow events this line should be ommited.** See the placeholders on the default [config.yml](core/src/main/resources/config.yml).
> **For follow events you shouldn't add reward keys.** See placeholders on the default [config.yml](core/src/main/resources/config.yml).
> [!TIP]
> You can now target multiple channels. If you do so, you can target some events to a specific channel following the second format. You can still follow the first example if you don't aim to target specific channels.
## Twitch Scopes
The latest version of the plugin needs the following scopes to function propertly:
Expand All @@ -129,8 +144,8 @@ The latest version of the plugin needs the following scopes to function propertl
* `moderator:read:followers`: Needed to be able to listen for follows.
* `bits:read`: Needed to listen for cheers.
* `channel:read:subscriptions`: Needed to listen for subscriptions and gifts.
* `user:read:chat`: Needed to use Twitch EventSub API.
* `chat:read`: Needed to show your stream chat in-game.
* `chat:read` and `user:read:chat`: Needed to show your stream chat in-game and use EventSub API.
* `user:bot` and `channel:bot`: Joins a stream chat to listen for subs.

## Permissions
- TARGET
Expand Down
144 changes: 90 additions & 54 deletions core/src/main/java/me/gosdev/chatpointsttv/ChatPointsTTV.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.github.twitch4j.TwitchClientBuilder;
import com.github.twitch4j.auth.providers.TwitchIdentityProvider;
import com.github.twitch4j.chat.events.channel.ChannelMessageEvent;
import com.github.twitch4j.eventsub.domain.chat.NoticeType;
import com.github.twitch4j.eventsub.events.ChannelChatMessageEvent;
import com.github.twitch4j.eventsub.events.ChannelChatNotificationEvent;
import com.github.twitch4j.eventsub.events.ChannelFollowEvent;
Expand Down Expand Up @@ -79,7 +80,6 @@ public class ChatPointsTTV extends JavaPlugin {
public Thread linkThread;

private String user_id;
private String channel_id;

public Logger log = getLogger();
public FileConfiguration config;
Expand All @@ -89,12 +89,15 @@ public class ChatPointsTTV extends JavaPlugin {
private final static String ClientID = "1peexftcqommf5tf5pt74g7b3gyki3";
public final String scopes = Scopes.join(
Scopes.CHANNEL_READ_REDEMPTIONS,
Scopes.CHANNEL_READ_SUBSCRIPTIONS,
Scopes.USER_READ_MODERATED_CHANNELS,
Scopes.MODERATOR_READ_FOLLOWERS,
Scopes.BITS_READ,
Scopes.CHANNEL_READ_SUBSCRIPTIONS,
Scopes.USER_READ_CHAT,
Scopes.CHAT_READ
Scopes.CHAT_READ,
Scopes.USER_BOT,
Scopes.CHANNEL_BOT
).replace(":", "%3A"); // Format colon character for browser

private OAuth2Credential oauth;
Expand All @@ -116,9 +119,19 @@ public static ChatPointsTTV getPlugin() {
}

public String getUserId(String username) {
UserList resultList = getTwitchClient().getHelix().getUsers(null, null, Arrays.asList(username)).execute();
UserList resultList = getTwitchClient().getHelix().getUsers(oauth.getAccessToken(), null, Arrays.asList(username)).execute();
if (resultList.getUsers().isEmpty()) {
throw new NullPointerException("Couldn't fetch user: " + username);
}
return resultList.getUsers().get(0).getId();
}
public String getUsername(String userId) {
UserList resultList = client.getHelix().getUsers(oauth.getAccessToken(), Arrays.asList(userId), null).execute();
if (resultList.getUsers().isEmpty()) {
throw new NullPointerException("Couldn't fetch user ID: " + userId);
}
return resultList.getUsers().get(0).getDisplayName();
}

public static ITwitchClient getTwitchClient() {
return client;
Expand Down Expand Up @@ -148,13 +161,11 @@ public static Map<String, String> getRedemptionStrings() {
public String getConnectedUsername() {
return accountConnected ? user.getLogin() : "Not Linked";
}
public String getListenedChannel() {
if (plugin != null) {
return client.getChat().getChannels().iterator().next(); // UNTESTED
} else {
if (plugin.config.getString("TWITCH_CHANNEL_USERNAME") == null | plugin.config.getString("TWITCH_CHANNEL_USERNAME").startsWith("MemorySection[path=")) return null; // Invalid string (probably left default "{YOUR CHANNEL}")) return null;
return plugin.config.getString("TWITCH_CHANNEL_USERNAME");
}
public List<String> getListenedChannels() {
List<String> channels = new ArrayList<>();
if (plugin.config.getStringList("CHANNEL_USERNAME") != null) channels = plugin.config.getStringList("CHANNEL_USERNAME");
else channels.add(plugin.config.getString("CHANNEL_USERNAME"));
return channels;
}

private static Utils utils;
Expand Down Expand Up @@ -317,51 +328,39 @@ public void linkToTwitch(CommandSender p, String token) {
}

utils.sendMessage(Bukkit.getConsoleSender(), "Logged in as: "+ user.getDisplayName());
// Join the twitch chat of this channel and enable stream/follow events
String channel = config.getString("CHANNEL_USERNAME");
channel_id = getUserId(channel);

eventHandler = new TwitchEventHandler();

// Linked account UserID
user_id = new TwitchIdentityProvider(null, null, null).getAdditionalCredentialInformation(oauth).map(OAuth2Credential::getUserId).orElse(null);
utils.sendMessage(Bukkit.getConsoleSender(), "Listening to " + channel + "'s events...");
client.getChat().joinChannel(channel);

// Subscribe to events

eventSocket = client.getEventSocket();
eventManager = client.getEventManager();

CountDownLatch latch = new CountDownLatch(3);
eventManager.onEvent(EventSocketSubscriptionSuccessEvent.class, e -> latch.countDown());
eventManager.onEvent(EventSocketSubscriptionFailureEvent.class, e -> latch.countDown());
int channels = getListenedChannels().size();
int subs = 0;

if (Rewards.getRewards(Rewards.rewardType.CHANNEL_POINTS) != null) {
client.getPubSub().listenForChannelPointsRedemptionEvents(null, channel_id);
eventManager.onEvent(RewardRedeemedEvent.class, new Consumer<RewardRedeemedEvent>() {
@Override
public void accept(RewardRedeemedEvent e) {
eventHandler.onChannelPointsRedemption(e);
}
});
utils.sendMessage(Bukkit.getConsoleSender(), "Listening for channel point rewards...");
}
if (Rewards.getRewards(Rewards.rewardType.FOLLOW) != null) {
if (TwitchUtils.getModeratedChannelIDs(oauth.getAccessToken(), user_id).contains(channel_id) || user_id.equals(channel_id)) { // If account is the streamer or a mod (need to have mod permissions on the channel)
eventSocket.register(SubscriptionTypes.CHANNEL_FOLLOW_V2.prepareSubscription(b -> b.moderatorUserId(user_id).broadcasterUserId(channel_id).build(), null));
eventManager.onEvent(ChannelFollowEvent.class, new Consumer<ChannelFollowEvent>() {
@Override
public void accept(ChannelFollowEvent e) {
try { // May get NullPointerException if event is triggered while still subscribing
eventHandler.onFollow(e);
} catch (NullPointerException ex) {}
}
});
utils.sendMessage(Bukkit.getConsoleSender(), "Listening for follows...");
} else {
log.warning("Follow events cannot be listened to on unauthorised channels.");
}
} else latch.countDown();

subs++;
eventManager.onEvent(ChannelFollowEvent.class, new Consumer<ChannelFollowEvent>() {
@Override
public void accept(ChannelFollowEvent e) {
try { // May get NullPointerException if event is triggered while still subscribing
eventHandler.onFollow(e);
} catch (NullPointerException ex) {}
}
});
}
if (Rewards.getRewards(Rewards.rewardType.CHEER) != null) {
eventSocket.register(SubscriptionTypes.CHANNEL_CHAT_MESSAGE.prepareSubscription(b -> b.broadcasterUserId(channel_id).userId(user_id).build(), null));
subs++;
eventManager.onEvent(ChannelChatMessageEvent.class, new Consumer<ChannelChatMessageEvent>() {
@Override
public void accept(ChannelChatMessageEvent e) {
Expand All @@ -370,22 +369,19 @@ public void accept(ChannelChatMessageEvent e) {
} catch (NullPointerException ex) {}
}
});
utils.sendMessage(Bukkit.getConsoleSender(), "Listening for Cheers...");
} else latch.countDown();

}
if (Rewards.getRewards(Rewards.rewardType.SUB) != null || Rewards.getRewards(Rewards.rewardType.GIFT) != null) {
eventSocket.register(SubscriptionTypes.CHANNEL_CHAT_NOTIFICATION.prepareSubscription(b -> b.broadcasterUserId(channel_id).userId(user_id).build(), null));
subs++;
eventManager.onEvent(ChannelChatNotificationEvent.class, new Consumer<ChannelChatNotificationEvent>(){
@Override
public void accept(ChannelChatNotificationEvent e) {
try { // May get NullPointerException if event is triggered while still subscribing
eventHandler.onEvent(e);
if (e.getNoticeType() == NoticeType.SUB || e.getNoticeType() == NoticeType.RESUB) eventHandler.onSub(e);
else if (e.getNoticeType() == NoticeType.COMMUNITY_SUB_GIFT) eventHandler.onSubGift(e);
} catch (NullPointerException ex) {}
}
});
utils.sendMessage(Bukkit.getConsoleSender(), "Listening for subscriptions and gifts...");
} else latch.countDown();

}
if (config.getBoolean("SHOW_CHAT")) {
eventManager.onEvent(ChannelMessageEvent.class, event -> {
if (!chatBlacklist.contains(event.getUser().getName())) {
Expand All @@ -407,16 +403,30 @@ public void accept(ChannelChatNotificationEvent e) {
}
});
}
eventHandler = new TwitchEventHandler();
client.getEventManager().getEventHandler(SimpleEventHandler.class).registerListener(eventHandler);
utils.sendMessage(p, "Twitch client was started successfully!");


CountDownLatch latch = new CountDownLatch(subs * channels);

eventManager.onEvent(EventSocketSubscriptionSuccessEvent.class, e -> latch.countDown());
eventManager.onEvent(EventSocketSubscriptionFailureEvent.class, e -> latch.countDown());

// Join the twitch chat of this channel(s) and enable stream/follow events
if (config.getList("CHANNEL_USERNAME") == null) { // If field is not a list (single channel)
subscribeToEvents(p, latch, config.getString("CHANNEL_USERNAME"));
} else {
for (String channel : config.getStringList("CHANNEL_USERNAME")) {
subscribeToEvents(p, latch, channel);
}
}

try {
client.getEventManager().getEventHandler(SimpleEventHandler.class).registerListener(eventHandler);
latch.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (InterruptedException e) {
log.warning("Failed to bind events.");
return;
}

utils.sendMessage(p, "Twitch client was started successfully!");
accountConnected = true;
});
linkThread.start();
Expand All @@ -431,7 +441,33 @@ public void uncaughtException(Thread t, Throwable e) {
}
});
}

public void subscribeToEvents(CommandSender p, CountDownLatch latch, String channel) {
String channel_id = getUserId(channel);

if (Rewards.getRewards(Rewards.rewardType.CHANNEL_POINTS) != null) {
client.getPubSub().listenForChannelPointsRedemptionEvents(null, channel_id);
}

if (Rewards.getRewards(Rewards.rewardType.FOLLOW) != null) {
if (TwitchUtils.getModeratedChannelIDs(oauth.getAccessToken(), user_id).contains(channel_id) || user_id.equals(channel_id)) { // If account is the streamer or a mod (need to have mod permissions on the channel)
eventSocket.register(SubscriptionTypes.CHANNEL_FOLLOW_V2.prepareSubscription(b -> b.moderatorUserId(user_id).broadcasterUserId(channel_id).build(), null));
} else {
log.warning(channel + ": Follow events cannot be listened to on unauthorised channels.");
latch.countDown();
}
}

if (Rewards.getRewards(Rewards.rewardType.CHEER) != null) {
eventSocket.register(SubscriptionTypes.CHANNEL_CHAT_MESSAGE.prepareSubscription(b -> b.broadcasterUserId(channel_id).userId(user_id).build(), null));
}

if (Rewards.getRewards(Rewards.rewardType.SUB) != null || Rewards.getRewards(Rewards.rewardType.GIFT) != null) {
eventSocket.register(SubscriptionTypes.CHANNEL_CHAT_NOTIFICATION.prepareSubscription(b -> b.broadcasterUserId(channel_id).userId(user_id).build(), null));
}
utils.sendMessage(Bukkit.getConsoleSender(), "Listening to " + channel + "'s events...");
client.getChat().joinChannel(channel);
}
public void unlink(CommandSender p) {
if (!accountConnected) {
p.sendMessage(ChatColor.RED + "There is no connected account.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,17 @@ private void help(CommandSender p) {
}

private void status(CommandSender p, ChatPointsTTV plugin) {
List<String> channels = plugin.getListenedChannels();
String strChannels = "";
for (int i = 0; i < channels.size(); i++) {
strChannels += channels.get(i);
if (i != channels.size() - 1) strChannels += ", ";
}
String msg = (
"---------- " + ChatColor.DARK_PURPLE + ChatColor.BOLD + "ChatPointsTTV status" + ChatColor.RESET + " ----------\n" +
ChatColor.LIGHT_PURPLE + "Plugin version: " + ChatColor.RESET + "v" +plugin.getDescription().getVersion() + "\n" +
ChatColor.LIGHT_PURPLE + "Connected account: " + ChatColor.RESET + plugin.getConnectedUsername() + "\n" +
ChatColor.LIGHT_PURPLE + "Listened channel: " + ChatColor.RESET + plugin.getListenedChannel() + "\n" +
ChatColor.LIGHT_PURPLE + "Listened channels: " + ChatColor.RESET + strChannels + "\n" +
"\n" +
ChatColor.LIGHT_PURPLE + "Connection status: " + (plugin.isAccountConnected() ? ChatColor.GREEN + "" + ChatColor.BOLD + "ACTIVE" : ChatColor.RED + "" + ChatColor.BOLD + "DISCONNECTED")
);
Expand Down
Loading

0 comments on commit 0c2c7a3

Please sign in to comment.