Skip to content

Commit

Permalink
feat: add secondary per-channel irc message limit (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy committed May 15, 2022
1 parent 7cb737c commit c4482d0
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 3 deletions.
33 changes: 31 additions & 2 deletions chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@
import com.github.twitch4j.common.util.CryptoUtils;
import com.github.twitch4j.common.util.EscapeUtils;
import com.github.twitch4j.util.IBackoffStrategy;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -135,6 +138,11 @@ public class TwitchChat implements ITwitchChat {
*/
protected final Bucket ircAuthBucket;

/**
* IRC Per-Channel Message Limit
*/
protected final Bandwidth perChannelRateLimit;

/**
* IRC Command Queue
*/
Expand Down Expand Up @@ -216,6 +224,11 @@ public class TwitchChat implements ITwitchChat {
*/
protected final Cache<String, Integer> joinAttemptsByChannelName;

/**
* Cache of per-channel message buckets
*/
protected final Cache<String, Bucket> bucketByChannelName;

/**
* Constructor
*
Expand All @@ -242,8 +255,9 @@ public class TwitchChat implements ITwitchChat {
* @param chatJoinTimeout Minimum milliseconds to wait after a join attempt
* @param wsPingPeriod WebSocket Ping Period
* @param connectionBackoffStrategy WebSocket Connection Backoff Strategy
* @param perChannelRateLimit Per channel message limit
*/
public TwitchChat(WebsocketConnection websocketConnection, EventManager eventManager, CredentialManager credentialManager, OAuth2Credential chatCredential, String baseUrl, boolean sendCredentialToThirdPartyHost, Collection<String> commandPrefixes, Integer chatQueueSize, Bucket ircMessageBucket, Bucket ircWhisperBucket, Bucket ircJoinBucket, Bucket ircAuthBucket, ScheduledThreadPoolExecutor taskExecutor, long chatQueueTimeout, ProxyConfig proxyConfig, boolean autoJoinOwnChannel, boolean enableMembershipEvents, Collection<String> botOwnerIds, boolean removeChannelOnJoinFailure, int maxJoinRetries, long chatJoinTimeout, int wsPingPeriod, IBackoffStrategy connectionBackoffStrategy) {
public TwitchChat(WebsocketConnection websocketConnection, EventManager eventManager, CredentialManager credentialManager, OAuth2Credential chatCredential, String baseUrl, boolean sendCredentialToThirdPartyHost, Collection<String> commandPrefixes, Integer chatQueueSize, Bucket ircMessageBucket, Bucket ircWhisperBucket, Bucket ircJoinBucket, Bucket ircAuthBucket, ScheduledThreadPoolExecutor taskExecutor, long chatQueueTimeout, ProxyConfig proxyConfig, boolean autoJoinOwnChannel, boolean enableMembershipEvents, Collection<String> botOwnerIds, boolean removeChannelOnJoinFailure, int maxJoinRetries, long chatJoinTimeout, int wsPingPeriod, IBackoffStrategy connectionBackoffStrategy, Bandwidth perChannelRateLimit) {
this.eventManager = eventManager;
this.credentialManager = credentialManager;
this.chatCredential = chatCredential;
Expand All @@ -262,6 +276,12 @@ public TwitchChat(WebsocketConnection websocketConnection, EventManager eventMan
this.removeChannelOnJoinFailure = removeChannelOnJoinFailure;
this.maxJoinRetries = maxJoinRetries;
this.chatJoinTimeout = chatJoinTimeout;
this.perChannelRateLimit = perChannelRateLimit;

// init per channel message buckets by channel name
this.bucketByChannelName = Caffeine.newBuilder()
.expireAfterAccess(Math.max(perChannelRateLimit.getRefillPeriodNanos(), Duration.ofSeconds(30L).toNanos()), TimeUnit.NANOSECONDS)
.build();

// init connection
if (websocketConnection == null) {
Expand Down Expand Up @@ -537,9 +557,12 @@ protected void sendCommand(String command, String... args) {

/**
* Send raw irc command
* <p>
* Note: perChannelRateLimit does not apply when directly using this method
*
* @param command raw irc command
*/
@SuppressWarnings("ConstantConditions")
public boolean sendRaw(String command) {
return BucketUtils.scheduleAgainstBucket(ircMessageBucket, taskExecutor, () -> queueCommand(command)) != null;
}
Expand Down Expand Up @@ -677,6 +700,7 @@ private boolean removeCurrentChannel(String channelName) {
}

@Override
@SuppressWarnings("ConstantConditions")
public boolean sendMessage(String channel, String message, Map<String, Object> tags) {
StringBuilder sb = new StringBuilder();
if (tags != null && !tags.isEmpty()) {
Expand All @@ -687,7 +711,7 @@ public boolean sendMessage(String channel, String message, Map<String, Object> t
sb.append("PRIVMSG #").append(channel.toLowerCase()).append(" :").append(message);

log.debug("Adding message for channel [{}] with content [{}] to the queue.", channel.toLowerCase(), message);
return sendRaw(sb.toString());
return BucketUtils.scheduleAgainstBucket(getChannelMessageBucket(channel), taskExecutor, () -> sendRaw(sb.toString())) != null;
}

/**
Expand Down Expand Up @@ -809,4 +833,9 @@ public TMIConnectionState getConnectionState() {
return TMIConnectionState.DISCONNECTED;
}
}

private Bucket getChannelMessageBucket(@NotNull String channelName) {
return bucketByChannelName.get(channelName.toLowerCase(), k -> BucketUtils.createBucket(perChannelRateLimit));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ public class TwitchChatBuilder {
@With
protected Bandwidth authRateLimit = TwitchChatLimitHelper.USER_AUTH_LIMIT;

/**
* Custom RateLimit for Messages per Channel
* <p>
* For example, this can restrict messages per channel at 100/30 (for a verified bot that has a global 7500/30 message limit).
*/
@With
protected Bandwidth perChannelRateLimit = TwitchChatLimitHelper.MOD_MESSAGE_LIMIT.withId("per-channel-limit");

/**
* Shared bucket for messages
*/
Expand Down Expand Up @@ -302,8 +310,11 @@ public TwitchChat build() {
if (ircAuthBucket == null)
ircAuthBucket = userId == null ? BucketUtils.createBucket(this.authRateLimit) : TwitchLimitRegistry.getInstance().getOrInitializeBucket(userId, TwitchLimitType.CHAT_AUTH_LIMIT, Collections.singletonList(authRateLimit));

if (perChannelRateLimit == null)
perChannelRateLimit = chatRateLimit;

log.debug("TwitchChat: Initializing Module ...");
return new TwitchChat(this.websocketConnection, this.eventManager, this.credentialManager, this.chatAccount, this.baseUrl, this.sendCredentialToThirdPartyHost, this.commandPrefixes, this.chatQueueSize, this.ircMessageBucket, this.ircWhisperBucket, this.ircJoinBucket, this.ircAuthBucket, this.scheduledThreadPoolExecutor, this.chatQueueTimeout, this.proxyConfig, this.autoJoinOwnChannel, this.enableMembershipEvents, this.botOwnerIds, this.removeChannelOnJoinFailure, this.maxJoinRetries, this.chatJoinTimeout, this.wsPingPeriod, this.connectionBackoffStrategy);
return new TwitchChat(this.websocketConnection, this.eventManager, this.credentialManager, this.chatAccount, this.baseUrl, this.sendCredentialToThirdPartyHost, this.commandPrefixes, this.chatQueueSize, this.ircMessageBucket, this.ircWhisperBucket, this.ircJoinBucket, this.ircAuthBucket, this.scheduledThreadPoolExecutor, this.chatQueueTimeout, this.proxyConfig, this.autoJoinOwnChannel, this.enableMembershipEvents, this.botOwnerIds, this.removeChannelOnJoinFailure, this.maxJoinRetries, this.chatJoinTimeout, this.wsPingPeriod, this.connectionBackoffStrategy, this.perChannelRateLimit);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ public class TwitchChatConnectionPool extends TwitchModuleConnectionPool<TwitchC
@Builder.Default
protected Bandwidth authRateLimit = TwitchChatLimitHelper.USER_AUTH_LIMIT;

/**
* Custom RateLimit for Messages per Channel
* <p>
* For example, this can restrict messages per channel at 100/30 (for a verified bot that has a global 7500/30 message limit).
*/
@Builder.Default
protected Bandwidth perChannelRateLimit = TwitchChatLimitHelper.MOD_MESSAGE_LIMIT.withId("per-channel-limit");

/**
* WebSocket Connection Backoff Strategy
*/
Expand Down Expand Up @@ -263,6 +271,7 @@ protected TwitchChat createConnection() {
.withWhisperRateLimit(whisperRateLimit)
.withJoinRateLimit(joinRateLimit)
.withAuthRateLimit(authRateLimit)
.withPerChannelRateLimit(perChannelRateLimit)
.withAutoJoinOwnChannel(false) // user will have to manually send a subscribe call to enable whispers. this avoids duplicating whisper events
.withConnectionBackoffStrategy(connectionBackoffStrategy)
).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ public class TwitchClientBuilder {
@With
protected Bandwidth chatAuthLimit = TwitchChatLimitHelper.USER_AUTH_LIMIT;

/**
* Custom RateLimit for Messages per Channel
* <p>
* For example, this can restrict messages per channel at 100/30 (for a verified bot that has a global 7500/30 message limit).
*/
@With
protected Bandwidth chatChannelMessageLimit = TwitchChatLimitHelper.MOD_MESSAGE_LIMIT.withId("per-channel-limit");

/**
* Wait time for taking items off chat queue in milliseconds. Default recommended
*/
Expand Down Expand Up @@ -423,6 +431,7 @@ public TwitchClient build() {
.withWhisperRateLimit(chatWhisperLimit)
.withJoinRateLimit(chatJoinLimit)
.withAuthRateLimit(chatAuthLimit)
.withPerChannelRateLimit(chatChannelMessageLimit)
.withScheduledThreadPoolExecutor(scheduledThreadPoolExecutor)
.withBaseUrl(chatServer)
.withChatQueueTimeout(chatQueueTimeout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ public class TwitchClientPoolBuilder {
@With
protected Bandwidth chatAuthLimit = TwitchChatLimitHelper.USER_AUTH_LIMIT;

/**
* Custom RateLimit for Messages per Channel
* <p>
* For example, this can restrict messages per channel at 100/30 (for a verified bot that has a global 7500/30 message limit).
*/
@With
protected Bandwidth chatChannelMessageLimit = TwitchChatLimitHelper.MOD_MESSAGE_LIMIT.withId("per-channel-limit");

/**
* Wait time for taking items off chat queue in milliseconds. Default recommended
*/
Expand Down Expand Up @@ -439,6 +447,7 @@ public TwitchClientPool build() {
.whisperRateLimit(chatWhisperLimit)
.joinRateLimit(chatJoinLimit)
.authRateLimit(chatAuthLimit)
.perChannelRateLimit(chatChannelMessageLimit)
.executor(() -> scheduledThreadPoolExecutor)
.proxyConfig(() -> proxyConfig)
.maxSubscriptionsPerConnection(maxChannelsPerChatInstance)
Expand All @@ -463,6 +472,7 @@ public TwitchClientPool build() {
.withWhisperRateLimit(chatWhisperLimit)
.withJoinRateLimit(chatJoinLimit)
.withAuthRateLimit(chatAuthLimit)
.withPerChannelRateLimit(chatChannelMessageLimit)
.withScheduledThreadPoolExecutor(scheduledThreadPoolExecutor)
.withBaseUrl(chatServer)
.withChatQueueTimeout(chatQueueTimeout)
Expand Down

0 comments on commit c4482d0

Please sign in to comment.