All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.twitch4j.chat.TwitchChatConnectionPool Maven / Gradle / Ivy

package com.github.twitch4j.chat;

import com.github.philippheuer.credentialmanager.domain.OAuth2Credential;
import com.github.twitch4j.chat.enums.MirroredMessagePolicy;
import com.github.twitch4j.chat.enums.NoticeTag;
import com.github.twitch4j.chat.events.channel.ChannelJoinFailureEvent;
import com.github.twitch4j.chat.events.channel.ChannelNoticeEvent;
import com.github.twitch4j.chat.events.channel.ChannelStateEvent;
import com.github.twitch4j.chat.events.channel.IRCMessageEvent;
import com.github.twitch4j.chat.util.TwitchChatLimitHelper;
import com.github.twitch4j.common.annotation.Unofficial;
import com.github.twitch4j.common.pool.TwitchModuleConnectionPool;
import com.github.twitch4j.common.util.BucketUtils;
import com.github.twitch4j.common.util.ChatReply;
import com.github.twitch4j.common.util.CryptoUtils;
import com.github.twitch4j.util.IBackoffStrategy;
import io.github.bucket4j.Bandwidth;
import io.github.xanthic.cache.api.Cache;
import io.github.xanthic.cache.api.domain.ExpiryType;
import io.github.xanthic.cache.core.CacheApi;
import lombok.Builder;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import org.jetbrains.annotations.Nullable;

import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * A pool for {@link TwitchChat} connections.
 * 

* This pool is easiest to use for: *

    *
  • Reading from many channels without an account
  • *
  • Reading from many channels with an account
  • *
  • Reading from many channels using valid proxies
  • *
  • Reading and sending messages/whispers with only one account
  • *
* Other applications are possible, but harder to configure. See below for a list of warnings. *

* Warning: If a custom {@link java.util.concurrent.ScheduledThreadPoolExecutor} is specified, * its corePoolSize must be large enough for the threads required by connections made by this class. *

* Warning: If a chatAccount is to be shared across multiple connections and used to send messages, * one should use advancedConfiguration to ensure the two are using a shared {@link io.github.bucket4j.Bucket}. *

* Note: If whispers are to be sent using this pool, one must manually join the channel to send the whisper from first. * If chatAccount's are dynamically supplied such that no two connections are using the same account, one can set * twitchChatBuilder.withAutoJoinOwnChannel(true) via advancedConfiguration to avoid the manual join. */ @SuperBuilder public class TwitchChatConnectionPool extends TwitchModuleConnectionPool implements ITwitchChat { private final String threadPrefix = "twitch4j-pool-" + CryptoUtils.generateNonce(4) + "-chat-"; /** * Provides a chat account to be used when constructing a new {@link TwitchChat} instance. * By default, this yields null, which corresponds to an anonymous connection. */ @NonNull @Builder.Default protected final Supplier chatAccount = () -> null; /** * Whether chat connections should automatically part from channels they have been banned from. * This is useful for reclaiming subscription headroom so a minimal number of chat instances are running. * By default false so that a chat instance can (eventually) reconnect if a unban occurs. * * @deprecated use removeChannelOnJoinFailure via advancedConfiguration instead. */ @Deprecated @Builder.Default protected final boolean automaticallyPartOnBan = false; /** * Custom RateLimit for ChatMessages */ @Builder.Default protected Bandwidth chatRateLimit = TwitchChatLimitHelper.USER_MESSAGE_LIMIT; /** * Custom RateLimit for Whispers */ @Builder.Default protected Bandwidth[] whisperRateLimit = TwitchChatLimitHelper.USER_WHISPER_LIMIT.toArray(new Bandwidth[2]); /** * Custom RateLimit for JOIN/PART */ @Builder.Default protected Bandwidth joinRateLimit = TwitchChatLimitHelper.USER_JOIN_LIMIT; /** * Custom RateLimit for AUTH */ @Builder.Default protected Bandwidth authRateLimit = TwitchChatLimitHelper.USER_AUTH_LIMIT; /** * Custom RateLimit for Messages per Channel *

* 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 = BucketUtils.simple(100, Duration.ofSeconds(30), "per-channel-limit"); /** * WebSocket Connection Backoff Strategy */ @Builder.Default private IBackoffStrategy connectionBackoffStrategy = null; /** * Mirrored Message Policy */ @Builder.Default private MirroredMessagePolicy mirroredMessagePolicy = MirroredMessagePolicy.REJECT_IF_OBSERVED; /** * Observed message IDs for mirrored chat deduplication purposes */ private volatile Cache observedMessageIds; /** * Channel IDs that are currently joined for mirrored chat deduplication purposes */ private final Set joinedRoomIds = ConcurrentHashMap.newKeySet(); @Override public boolean sendMessage(String channel, String message, @Nullable Map tags) { return this.sendMessage(channel, channel, message, tags); } /** * Sends a message from the {@link TwitchChat} identified either to a channel or directly on the socket. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). * @param message the message to be sent. * @return whether a {@link TwitchChat} instance was found and used to send the message */ public boolean sendMessage(final String channelToIdentifyChatInstance, final String targetChannel, final String message) { return this.sendMessage(channelToIdentifyChatInstance, targetChannel, message, Collections.emptyMap()); } /** * Sends a message from the identified {@link TwitchChat} instance with an optional nonce or reply parent. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). * @param message the message to be sent. * @param nonce the cryptographic nonce (optional). * @param replyMsgId the msgId of the parent message being replied to (optional). * @return whether a {@link TwitchChat} instance was found and used to send the message */ public boolean sendMessage(final String channelToIdentifyChatInstance, final String targetChannel, final String message, @Unofficial final String nonce, final String replyMsgId) { final Map tags = new LinkedHashMap<>(); if (nonce != null) tags.put(IRCMessageEvent.NONCE_TAG_NAME, nonce); if (replyMsgId != null) tags.put(ChatReply.REPLY_MSG_ID_TAG_NAME, replyMsgId); return this.sendMessage(channelToIdentifyChatInstance, targetChannel, message, tags); } /** * Sends a message from the identified {@link TwitchChat} instance with the specified tags. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). * @param message the message to be sent. * @param tags the message tags. * @return whether a {@link TwitchChat} instance was found and used to send the message */ public boolean sendMessage(String channelToIdentifyChatInstance, String targetChannel, String message, @Nullable Map tags) { if (channelToIdentifyChatInstance == null) return false; final TwitchChat chat = subscriptions.get(channelToIdentifyChatInstance.toLowerCase()); if (chat == null) return false; if (targetChannel != null) { chat.sendMessage(targetChannel, message, tags); } else { chat.sendRaw(message); } return true; } /** * Sends a whisper. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. * @param toChannel the channel to send the whisper to. * @param message the message to send in the whisper. * @return whether a {@link TwitchChat} instance was identified to send the message from. * @throws NullPointerException if the identified {@link TwitchChat} does not have a valid chatCredential * @deprecated Twitch will decommission this method on February 18, 2023; migrate to TwitchHelix#updateChatSettings */ @Deprecated public boolean sendPrivateMessage(final String channelToIdentifyChatInstance, final String toChannel, final String message) { final TwitchChat chat; if (channelToIdentifyChatInstance == null || (chat = subscriptions.get(channelToIdentifyChatInstance.toLowerCase())) == null) return false; chat.sendPrivateMessage(toChannel, message); return true; } /** * Joins a channel. * * @param s the channel name * @return the channel name */ @Override public String subscribe(String s) { return s != null ? super.subscribe(s.toLowerCase()) : null; } @Override public void joinChannel(String channelName) { this.subscribe(channelName); } /** * Parts from a channel. * * @param s the channel name * @return a non-null class if able to part, null otherwise */ @Override public Boolean unsubscribe(String s) { String key = s != null ? s.toLowerCase() : null; if (mirroredMessagePolicy == MirroredMessagePolicy.REJECT_IF_OBSERVED) { ITwitchChat chat = subscriptions.get(key); if (chat != null) { String roomId = chat.getChannelNameToChannelId().get(key); if (roomId != null) { joinedRoomIds.remove(roomId); } } } return super.unsubscribe(key); } @Override public boolean leaveChannel(String channelName) { final Boolean b = this.unsubscribe(channelName); return b != null && b; } @Override public boolean isChannelJoined(String channelName) { return this.subscriptions.containsKey(channelName.toLowerCase()); } @Override public Set getChannels() { return Collections.unmodifiableSet(subscriptions.keySet()); } @Override protected String handleSubscription(TwitchChat twitchChat, String s) { if (twitchChat == null) return null; twitchChat.joinChannel(s); return s; } @Override protected String handleDuplicateSubscription(TwitchChat twitchChat, TwitchChat old, String s) { return twitchChat != null && twitchChat != old && twitchChat.leaveChannel(s) ? s : null; } @Override protected Boolean handleUnsubscription(TwitchChat twitchChat, String s) { return twitchChat != null ? twitchChat.leaveChannel(s) : null; } @Override protected String getRequestFromSubscription(String s) { return s; } @Override protected int getSubscriptionSize(String s) { return 1; } @Override protected TwitchChat createConnection() { if (closed.get()) throw new IllegalStateException("Chat socket cannot be created after pool was closed!"); if (mirroredMessagePolicy == MirroredMessagePolicy.REJECT_IF_OBSERVED && observedMessageIds == null) { synchronized (this) { if (observedMessageIds == null) { this.observedMessageIds = CacheApi.create(spec -> { spec.expiryTime(Duration.ofSeconds(10L)); spec.expiryType(ExpiryType.POST_WRITE); spec.maxSize(2048L); }); } } } // Instantiate with configuration TwitchChat chat = advancedConfiguration.apply( TwitchChatBuilder.builder() .withChatAccount(chatAccount.get()) .withEventManager(getConnectionEventManager()) .withScheduledThreadPoolExecutor(getExecutor(threadPrefix + CryptoUtils.generateNonce(4), TwitchChat.REQUIRED_THREAD_COUNT)) .withProxyConfig(proxyConfig.get()) .withChatRateLimit(chatRateLimit) .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) .withJoinedToRoomId(joinedRoomIds::contains) .withMirroredMessagePolicy(mirroredMessagePolicy) .withObservedMessageIds(observedMessageIds) ).build(); // Track joined channels for mirrored message deduplication if (mirroredMessagePolicy == MirroredMessagePolicy.REJECT_IF_OBSERVED) { chat.getEventManager().onEvent(threadPrefix + "room-tracker", ChannelStateEvent.class, e -> joinedRoomIds.add(e.getChannel().getId())); } // Reclaim channel headroom upon generic join failures chat.getEventManager().onEvent(threadPrefix + "join-fail-tracker", ChannelJoinFailureEvent.class, e -> unsubscribe(e.getChannelName())); // Reclaim channel headroom upon a ban chat.getEventManager().onEvent(threadPrefix + "ban-tracker", ChannelNoticeEvent.class, e -> { if (automaticallyPartOnBan && NoticeTag.MSG_BANNED.toString().equals(e.getMsgId())) { unsubscribe(e.getChannel().getName()); } }); // Return chat client return chat; } @Override protected void disposeConnection(TwitchChat connection) { connection.close(); } @Override public long getLatency() { long sum = 0; int count = 0; for (TwitchChat connection : getConnections()) { final long latency = connection.getLatency(); if (latency > 0) { sum += latency; count++; } } return count > 0 ? sum / count : -1L; } /** * Note: this map does not dynamically update unlike {@link TwitchChat#getChannelIdToChannelName()} *

* {@inheritDoc} */ @Override public Map getChannelIdToChannelName() { return collectMapsFromConnections(TwitchChat::getChannelIdToChannelName); } /** * Note: this map does not dynamically update unlike {@link TwitchChat#getChannelNameToChannelId()} *

* {@inheritDoc} */ @Override public Map getChannelNameToChannelId() { return collectMapsFromConnections(TwitchChat::getChannelNameToChannelId); } private Map collectMapsFromConnections(final Function> mapRetriever) { final Map aggregated = new HashMap<>(numConnections() * maxSubscriptionsPerConnection); final Consumer retrieve = chat -> aggregated.putAll(mapRetriever.apply(chat)); // Note: if connections are changing in saturation concurrently, this lock-free approach could skip over those instances saturatedConnections.forEach(retrieve); unsaturatedConnections.keySet().forEach(retrieve); return Collections.unmodifiableMap(aggregated); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy