From 08704c6529bab7d18112e4d107a9cb13bad18225 Mon Sep 17 00:00:00 2001 From: Bulat Kurbanov Date: Tue, 18 Mar 2025 17:51:59 +0100 Subject: [PATCH] Add reward redemption --- .../stream_notifications/messages_proc.py | 161 +++++++++++------- .../stream_notifications/reward_redemption.py | 39 +++++ src/modules/stream_notifications/tasks.py | 9 + .../stream_notifications/twitch/authorize.py | 2 + .../stream_notifications/twitch/webhook.py | 24 ++- 5 files changed, 169 insertions(+), 66 deletions(-) create mode 100644 src/modules/stream_notifications/reward_redemption.py diff --git a/src/modules/stream_notifications/messages_proc.py b/src/modules/stream_notifications/messages_proc.py index 74137dd..c5f11a5 100644 --- a/src/modules/stream_notifications/messages_proc.py +++ b/src/modules/stream_notifications/messages_proc.py @@ -6,7 +6,7 @@ from twitchAPI.object.eventsub import ChannelChatMessageEvent from httpx import AsyncClient from core.config import config -from .twitch.authorize import authorize +from .twitch.authorize import authorize, Twitch logger = logging.getLogger(__name__) @@ -139,9 +139,8 @@ async def get_completion(messages: list[dict]) -> str: class MessagesProc: - IGNORED_USER_LOGINS = [ + FULL_IGNORED_USER_LOGINS = [ "jeetbot", - "kurbezz", ] MESSAGE_LIMIT = 1000 @@ -172,9 +171,7 @@ class MessagesProc: return [m for m in cls.MESSAGE_HISTORY if m["id"] == message_id] @classmethod - async def on_message(cls, event: MessageEvent): - logging.info(f"Received message: {event}") - + async def _update_history(cls, event: MessageEvent): cls.update_message_history( id=event.message_id, text=event.message.text, @@ -182,11 +179,8 @@ class MessagesProc: thread_id=event.reply.thread_message_id if event.reply is not None else None ) - if event.chatter_user_name == "pahangor": - return - - twitch = await authorize() - + @classmethod + async def _goida(cls, twitch: Twitch, event: MessageEvent): if "гойда" in event.message.text.lower(): await twitch.send_chat_message( event.broadcaster_user_id, @@ -195,67 +189,80 @@ class MessagesProc: reply_parent_message_id=event.message_id ) - if "lasqexx" in event.chatter_user_login: - if "здароу" in event.message.text.lower(): + @classmethod + async def _lasqexx(cls, twitch: Twitch, event: MessageEvent): + if "lasqexx" not in event.chatter_user_login: + return + + if "здароу" in event.message.text.lower(): + await twitch.send_chat_message( + event.broadcaster_user_id, + config.TWITCH_ADMIN_USER_ID, + "Здароу, давай иди уже", + reply_parent_message_id=event.message_id + ) + return + + if "сосал?" in event.message.text.lower(): + await twitch.send_chat_message( + event.broadcaster_user_id, + config.TWITCH_ADMIN_USER_ID, + "А ты? Иди уже", + reply_parent_message_id=event.message_id + ) + return + + if "лан я пошёл" in event.message.text.lower(): + await twitch.send_chat_message( + event.broadcaster_user_id, + config.TWITCH_ADMIN_USER_ID, + "да да, иди уже", + reply_parent_message_id=event.message_id + ) + return + + @classmethod + async def _ask_ai(cls, twitch: Twitch, event: MessageEvent): + if not event.message.text.lower().startswith("!ai"): + return + + try: + messages = cls.get_message_history_with_thread( + event.message_id, + thread_id=event.reply.thread_message_id if event.reply is not None else None + ) + completion = await get_completion(messages) + + max_length = 255 + completion_parts = [completion[i:i + max_length] for i in range(0, len(completion), max_length)] + + for part in completion_parts: await twitch.send_chat_message( event.broadcaster_user_id, config.TWITCH_ADMIN_USER_ID, - "Здароу, давай иди уже", + part, reply_parent_message_id=event.message_id ) - if "сосал?" in event.message.text.lower(): - await twitch.send_chat_message( - event.broadcaster_user_id, - config.TWITCH_ADMIN_USER_ID, - "А ты? Иди уже", - reply_parent_message_id=event.message_id + cls.update_message_history( + id="ai", + text=part, + user="kurbezz", + thread_id=event.message_id ) + except Exception as e: + logger.error("Failed to get completion: {}", e, exc_info=True) - if "лан я пошёл" in event.message.text.lower(): - await twitch.send_chat_message( - event.broadcaster_user_id, - config.TWITCH_ADMIN_USER_ID, - "да да, иди уже", - reply_parent_message_id=event.message_id - ) + await twitch.send_chat_message( + event.broadcaster_user_id, + config.TWITCH_ADMIN_USER_ID, + "Ошибка!", + reply_parent_message_id=event.message_id + ) - if event.message.text.lower().startswith("!ai"): - try: - messages = cls.get_message_history_with_thread( - event.message_id, - thread_id=event.reply.thread_message_id if event.reply is not None else None - ) - completion = await get_completion(messages) - - max_length = 255 - completion_parts = [completion[i:i + max_length] for i in range(0, len(completion), max_length)] - - for part in completion_parts: - await twitch.send_chat_message( - event.broadcaster_user_id, - config.TWITCH_ADMIN_USER_ID, - part, - reply_parent_message_id=event.message_id - ) - - cls.update_message_history( - id="ai", - text=part, - user="kurbezz", - thread_id=event.message_id - ) - except Exception as e: - logger.error("Failed to get completion: {}", e, exc_info=True) - - await twitch.send_chat_message( - event.broadcaster_user_id, - config.TWITCH_ADMIN_USER_ID, - "Ошибка!", - reply_parent_message_id=event.message_id - ) - - if event.chatter_user_login in cls.IGNORED_USER_LOGINS: + @classmethod + async def _kurbezz(cls, twitch: Twitch, event: MessageEvent): + if event.chatter_user_login == "kurbezz": return if ("kurbezz" in event.message.text.lower() or \ @@ -295,3 +302,31 @@ class MessagesProc: "Пошел нахуй!", reply_parent_message_id=event.message_id ) + + @classmethod + async def _on_custom_reward(cls, twitch: Twitch, event: MessageEvent): + pass + # if event.channel_points_custom_reward_id: + # await twitch.send_chat_message( + # event.broadcaster_user_id, + # config.TWITCH_ADMIN_USER_ID, + # "Спасибо за поддержку!", + # reply_parent_message_id=event.message_id + # ) + + @classmethod + async def on_message(cls, event: MessageEvent): + if event.chatter_user_name in cls.FULL_IGNORED_USER_LOGINS: + return + + logging.info(f"Received message: {event}") + + await cls._update_history(event) + + twitch = await authorize() + + await cls._goida(twitch, event) + await cls._lasqexx(twitch, event) + await cls._ask_ai(twitch, event) + await cls._kurbezz(twitch, event) + await cls._on_custom_reward(twitch, event) diff --git a/src/modules/stream_notifications/reward_redemption.py b/src/modules/stream_notifications/reward_redemption.py new file mode 100644 index 0000000..2d314cb --- /dev/null +++ b/src/modules/stream_notifications/reward_redemption.py @@ -0,0 +1,39 @@ +import logging + +from pydantic import BaseModel + +from twitchAPI.object.eventsub import ChannelPointsCustomRewardRedemptionAddEvent + +from core.config import config +from .twitch.authorize import authorize + + +logger = logging.getLogger(__name__) + + +class RewardRedemption(BaseModel): + broadcaster_user_id: str + user_name: str + reward_title: str + reward_prompt: str + + @classmethod + def from_twitch_event(cls, event: ChannelPointsCustomRewardRedemptionAddEvent): + return cls( + broadcaster_user_id=event.event.broadcaster_user_id, + user_name=event.event.user_name, + reward_title=event.event.reward.title, + reward_prompt=event.event.reward.prompt or "", + ) + + +async def on_redemption_reward_add(reward: RewardRedemption): + logger.info(reward) + + twitch = await authorize() + + await twitch.send_chat_message( + reward.broadcaster_user_id, + config.TWITCH_ADMIN_USER_ID, + f"🎉 {reward.user_name} just redeemed {reward.reward_title}! 🎉" + ) diff --git a/src/modules/stream_notifications/tasks.py b/src/modules/stream_notifications/tasks.py index 8a8f4ae..c3d0753 100644 --- a/src/modules/stream_notifications/tasks.py +++ b/src/modules/stream_notifications/tasks.py @@ -9,6 +9,7 @@ from .state import State, UpdateEvent, EventType from .watcher import StateWatcher from .messages_proc import MessageEvent, MessagesProc from .twitch.authorize import authorize +from .reward_redemption import RewardRedemption, on_redemption_reward_add @broker.task( @@ -82,3 +83,11 @@ async def check_streams_states(): ) async def on_message(event: MessageEvent): await MessagesProc.on_message(event) + + +@broker.task( + "stream_notifications.on_redemption_reward_add", + retry_on_error=True +) +async def on_redemption_reward_add_task(event: RewardRedemption): + await on_redemption_reward_add(event) diff --git a/src/modules/stream_notifications/twitch/authorize.py b/src/modules/stream_notifications/twitch/authorize.py index f6fb7ce..791b831 100644 --- a/src/modules/stream_notifications/twitch/authorize.py +++ b/src/modules/stream_notifications/twitch/authorize.py @@ -14,6 +14,8 @@ SCOPES = [ AuthScope.USER_BOT, AuthScope.USER_READ_CHAT, AuthScope.USER_WRITE_CHAT, + + AuthScope.CHANNEL_READ_REDEMPTIONS, ] diff --git a/src/modules/stream_notifications/twitch/webhook.py b/src/modules/stream_notifications/twitch/webhook.py index e3f0f9f..b407860 100644 --- a/src/modules/stream_notifications/twitch/webhook.py +++ b/src/modules/stream_notifications/twitch/webhook.py @@ -4,14 +4,15 @@ from typing import NoReturn, Literal from twitchAPI.eventsub.webhook import EventSubWebhook from twitchAPI.twitch import Twitch -from twitchAPI.object.eventsub import StreamOnlineEvent, ChannelUpdateEvent, ChannelChatMessageEvent +from twitchAPI.object.eventsub import StreamOnlineEvent, ChannelUpdateEvent, ChannelChatMessageEvent, ChannelPointsCustomRewardRedemptionAddEvent from twitchAPI.oauth import validate_token from core.config import config from repositories.streamers import StreamerConfigRepository, StreamerConfig -from modules.stream_notifications.tasks import on_stream_state_change, on_stream_state_change_with_check, on_message +from modules.stream_notifications.tasks import on_stream_state_change, on_stream_state_change_with_check, on_message, on_redemption_reward_add_task from modules.stream_notifications.state import UpdateEvent, EventType from modules.stream_notifications.messages_proc import MessageEvent +from modules.stream_notifications.reward_redemption import RewardRedemption from .authorize import authorize @@ -42,6 +43,14 @@ class TwitchService: EventType.STREAM_ONLINE, ) + async def on_channel_points_custom_reward_redemption_add( + self, + event: ChannelPointsCustomRewardRedemptionAddEvent + ): + await on_redemption_reward_add_task( + RewardRedemption.from_twitch_event(event) + ) + async def on_message(self, event: ChannelChatMessageEvent): await on_message.kiq( MessageEvent.from_twitch_event(event) @@ -51,7 +60,8 @@ class TwitchService: self, method: Literal["listen_channel_update_v2"] | Literal["listen_stream_online"] - | Literal["listen_channel_chat_message"], + | Literal["listen_channel_chat_message"] + | Literal["listen_channel_points_custom_reward_redemption_add"], eventsub: EventSubWebhook, streamer: StreamerConfig, retry: int = 10 @@ -69,6 +79,11 @@ class TwitchService: str(config.TWITCH_ADMIN_USER_ID), self.on_message ) + case "listen_channel_points_custom_reward_redemption_add": + await eventsub.listen_channel_points_custom_reward_redemption_add( + str(streamer.twitch.id), + self.on_channel_points_custom_reward_redemption_add + ) case _: raise ValueError("Unknown method") @@ -84,6 +99,8 @@ class TwitchService: sub_type = "stream.online" case "listen_channel_chat_message": sub_type = "channel.chat.message" + case "listen_channel_points_custom_reward_redemption_add": + sub_type = "channel.channel_points_custom_reward_redemption.add" case _: raise ValueError("Unknown method") @@ -107,6 +124,7 @@ class TwitchService: self.subscribe_with_retry("listen_channel_update_v2", eventsub, streamer), self.subscribe_with_retry("listen_stream_online", eventsub, streamer), self.subscribe_with_retry("listen_channel_chat_message", eventsub, streamer), + self.subscribe_with_retry("listen_channel_points_custom_reward_redemption_add", eventsub, streamer) ) logger.info(f"Subscribe to events for {streamer.twitch.name} done")