This commit is contained in:
2024-10-04 18:48:32 +02:00
parent 83a407a84c
commit 055e9579da
9 changed files with 130 additions and 60 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
venv
.DS_Store

29
configs/hafmc.toml Normal file
View File

@@ -0,0 +1,29 @@
[twitch]
id = 59900845
name = "hafmc"
[notifications]
start_stream = '''
HafMC сейчас стримит {title} ({category})!
Присоединяйся: https://twitch.tv/hafmc
'''
change_category = '''
HafMC начал играть в {category}!
Присоединяйся: https://twitch.tv/hafmc
'''
[integrations]
[integrations.discord]
guild_id = 1198051900906549329
notifications_channel_id = 1198296540964475002
[integrations.discord.games_list]
channel_id = 1201810638800691210
message_id = 1239664178038313012
[integrations.telegram]
notifications_channel_id = -1001939021131

18
configs/ssstano.toml Normal file
View File

@@ -0,0 +1,18 @@
[twitch]
id = 188615689
name = "ssstano"
[notifications]
start_stream = '''
ssstano ебашит LIVE стрим для everyone
https://www.twitch.tv/ssstano
https://www.twitch.tv/ssstano
https://www.twitch.tv/ssstano
'''
[integrations]
[integrations.telegram]
notifications_channel_id = -1002152372995

View File

@@ -15,6 +15,7 @@ FROM python:3.12-slim AS runtime
RUN apt update && apt install -y --no-install-recommends netcat-traditional wkhtmltopdf && apt clean
COPY ./src/ /app
COPY ./configs/ /app/configs
ENV PATH="/opt/venv/bin:$PATH"
ENV VENV_PATH=/opt/venv

View File

@@ -1,29 +1,38 @@
import json
import tomllib
from pydantic import BaseModel, field_validator
from pydantic_settings import BaseSettings
from pathlib import Path
class TwitchConfig(BaseModel):
CHANNEL_ID: str
CHANNEL_NAME: str
id: int
name: str
class NotificationsConfig(BaseModel):
start_stream: str
change_category: str | None = None
class GamesListConfig(BaseModel):
channel_id: int
message_id: int
class DiscordConfig(BaseModel):
GUILD_ID: int
CHANNEL_ID: int
guild_id: int
notifications_channel_id: int
games_list: GamesListConfig | None = None
GAME_LIST_CHANNEL_ID: int
GAME_LIST_MESSAGE_ID: int
class TelegramConfig(BaseModel):
notifications_channel_id: int
class IntegrationsConfig(BaseModel):
discord: DiscordConfig | None = None
telegram: TelegramConfig | None = None
class StreamerConfig(BaseModel):
TWITCH: TwitchConfig
DISCORD: DiscordConfig | None = None
TELEGRAM_CHANNEL_ID: int | None = None
START_STREAM_MESSAGE: str | None = None
CHANGE_CATEGORY_MESSAGE: str | None = None
twitch: TwitchConfig
notifications: NotificationsConfig
integrations: IntegrationsConfig
class Config(BaseSettings):
@@ -45,13 +54,16 @@ class Config(BaseSettings):
SECRETS_FILE_PATH: str
@field_validator("STREAMERS", mode="before")
def check_streamers(cls, value):
if isinstance(value, str):
return json.loads(value)
return value
config_dir = Path("/app/configs")
streamers = []
for toml_file in config_dir.glob("*.toml"):
if toml_file.is_file():
with open(toml_file, "rb") as f:
streamer_config = tomllib.load(f)
streamers.append(StreamerConfig(**streamer_config))
return streamers if streamers else value
config = Config() # type: ignore

View File

@@ -17,13 +17,16 @@ def get_game_list_channel_to_message_map() -> dict[int, int]:
result = {}
for streamer in config.STREAMERS:
if streamer.DISCORD is None:
if (integration := streamer.integrations.discord) is None:
continue
if streamer.DISCORD.GAME_LIST_CHANNEL_ID is None or streamer.DISCORD.GAME_LIST_MESSAGE_ID is None:
if (games_list := integration.games_list) is None:
continue
result[streamer.DISCORD.GAME_LIST_CHANNEL_ID] = streamer.DISCORD.GAME_LIST_MESSAGE_ID
if games_list.channel_id is None or games_list.message_id is None:
continue
result[games_list.channel_id] = games_list.message_id
return result
@@ -39,14 +42,14 @@ class DiscordClient(discord.Client):
async def setup_hook(self):
for streamer in config.STREAMERS:
if streamer.DISCORD is None:
if (integration := streamer.integrations.discord) is None:
continue
if streamer.DISCORD.GAME_LIST_CHANNEL_ID is None or streamer.DISCORD.GAME_LIST_MESSAGE_ID is None:
if integration.games_list is None:
continue
self.tree.copy_global_to(guild=Object(id=streamer.DISCORD.GUILD_ID))
await self.tree.sync(guild=Object(id=streamer.DISCORD.GUILD_ID))
self.tree.copy_global_to(guild=Object(id=integration.guild_id))
await self.tree.sync(guild=Object(id=integration.guild_id))
async def on_ready(self):
await self.change_presence(

View File

@@ -33,14 +33,18 @@ async def notify_discord(msg: str, channel_id: str):
async def notify(msg: str, streamer_config: StreamerConfig):
if streamer_config.DISCORD is not None:
try:
await notify_discord(msg, str(streamer_config.DISCORD.CHANNEL_ID))
except Exception as e:
logger.error("Failed to notify discord", exc_info=e)
integrations = streamer_config.integrations
if streamer_config.TELEGRAM_CHANNEL_ID is not None:
try:
await notify_telegram(msg, str(streamer_config.TELEGRAM_CHANNEL_ID))
except Exception as e:
logger.error("Failed to notify telegram", exc_info=e)
if (discord := integrations.discord) is not None:
if discord.notifications_channel_id is not None:
try:
await notify_discord(msg, str(discord.notifications_channel_id))
except Exception as e:
logger.error("Failed to notify discord", exc_info=e)
if (telegram := integrations.telegram) is not None:
if telegram.notifications_channel_id is not None:
try:
await notify_telegram(msg, str(telegram.notifications_channel_id))
except Exception as e:
logger.error("Failed to notify telegram", exc_info=e)

View File

@@ -82,7 +82,7 @@ async def edit_events(
async def syncronize(twitch: TwitchConfig, discord_guild_id: int):
twitch_events = await get_twitch_events(twitch.CHANNEL_ID)
twitch_events = await get_twitch_events(str(twitch.id))
discord_events = await get_discord_events(discord_guild_id)
twitch_events_with_id = [(event.uid, event) for event in twitch_events]
@@ -91,9 +91,9 @@ async def syncronize(twitch: TwitchConfig, discord_guild_id: int):
for event in discord_events
]
await add_events(discord_guild_id, twitch.CHANNEL_NAME, twitch_events_with_id, discord_events_with_id)
await add_events(discord_guild_id, twitch.name, twitch_events_with_id, discord_events_with_id)
await remove_events(discord_guild_id, twitch_events_with_id, discord_events_with_id)
await edit_events(discord_guild_id, twitch.CHANNEL_NAME, twitch_events_with_id, discord_events_with_id)
await edit_events(discord_guild_id, twitch.name, twitch_events_with_id, discord_events_with_id)
async def start_synchronizer():
@@ -102,10 +102,10 @@ async def start_synchronizer():
while True:
try:
for streamer in config.STREAMERS:
if streamer.DISCORD is None:
if (integration := streamer.integrations.discord) is None:
continue
await syncronize(streamer.TWITCH, streamer.DISCORD.GUILD_ID)
await syncronize(streamer.twitch, integration.guild_id)
except Exception as e:
logging.error(e)

View File

@@ -60,7 +60,7 @@ class TwitchService:
def __init__(self, twitch: Twitch):
self.twitch = twitch
self.state: dict[str, State | None] = {}
self.state: dict[int, State | None] = {}
@classmethod
async def authorize(cls):
@@ -78,31 +78,31 @@ class TwitchService:
return twitch
def get_streamer_config(self, streamer_id: str) -> StreamerConfig:
def get_streamer_config(self, streamer_id: int) -> StreamerConfig:
for streamer in config.STREAMERS:
if streamer.TWITCH.CHANNEL_ID == streamer_id:
if streamer.twitch.id == streamer_id:
return streamer
raise ValueError(f"Streamer with id {streamer_id} not found")
async def notify_online(self, streamer_id: str):
async def notify_online(self, streamer_id: int):
current_state = self.state.get(streamer_id)
if current_state is None:
raise RuntimeError("State is None")
streamer = self.get_streamer_config(streamer_id)
if streamer.START_STREAM_MESSAGE is None:
if streamer.notifications.start_stream is None:
return
msg = streamer.START_STREAM_MESSAGE.replace("\\n", "\n").format(
msg = streamer.notifications.start_stream.format(
title=current_state.title,
category=current_state.category
)
await notify(msg, streamer)
async def notify_change_category(self, streamer_id: str):
async def notify_change_category(self, streamer_id: int):
current_state = self.state.get(streamer_id)
if current_state is None:
@@ -113,20 +113,21 @@ class TwitchService:
streamer = self.get_streamer_config(streamer_id)
if streamer.CHANGE_CATEGORY_MESSAGE is None:
if streamer.notifications.change_category is None:
return
msg = streamer.CHANGE_CATEGORY_MESSAGE.replace("\\n", "\n").format(
msg = streamer.notifications.change_category.format(
title=current_state.title,
category=current_state.category
)
await notify(msg, streamer)
async def get_current_stream(self, streamer_id: str, retry_count: int = 5, delay: int = 5):
async def get_current_stream(self, streamer_id: int, retry_count: int = 5, delay: int = 5):
remain_retry = retry_count
while remain_retry > 0:
stream = await first(self.twitch.get_streams(user_id=[streamer_id]))
stream = await first(self.twitch.get_streams(user_id=[str(streamer_id)]))
if stream is not None:
return stream
@@ -137,7 +138,7 @@ class TwitchService:
return None
async def on_channel_update(self, event: ChannelUpdateEvent):
brodcaster_id = event.event.broadcaster_user_id
brodcaster_id = int(event.event.broadcaster_user_id)
stream = await self.get_current_stream(brodcaster_id)
if stream is None:
@@ -158,7 +159,7 @@ class TwitchService:
if changed:
await self.notify_change_category(brodcaster_id)
async def _on_stream_online(self, streamer_id: str):
async def _on_stream_online(self, streamer_id: int):
current_stream = await self.get_current_stream(streamer_id)
if current_stream is None:
return
@@ -180,7 +181,7 @@ class TwitchService:
await self.notify_online(streamer_id)
async def on_stream_online(self, event: StreamOnlineEvent):
await self._on_stream_online(event.event.broadcaster_user_id)
await self._on_stream_online(int(event.event.broadcaster_user_id))
async def run(self):
eventsub = EventSubWebhook(
@@ -191,15 +192,16 @@ class TwitchService:
)
for streamer in config.STREAMERS:
current_stream = await self.get_current_stream(streamer.TWITCH.CHANNEL_ID)
current_stream = await self.get_current_stream(streamer.twitch.id)
if current_stream:
self.state[streamer.TWITCH.CHANNEL_ID] = State(
self.state[streamer.twitch.id] = State(
title=current_stream.title,
category=current_stream.game_name,
last_live_at=datetime.now()
)
else:
self.state[streamer.TWITCH.CHANNEL_ID] = None
self.state[streamer.twitch.id] = None
try:
await eventsub.unsubscribe_all()
@@ -209,8 +211,8 @@ class TwitchService:
logger.info("Subscribe to events...")
for streamer in config.STREAMERS:
await eventsub.listen_channel_update_v2(streamer.TWITCH.CHANNEL_ID, self.on_channel_update)
await eventsub.listen_stream_online(streamer.TWITCH.CHANNEL_ID, self.on_stream_online)
await eventsub.listen_channel_update_v2(str(streamer.twitch.id), self.on_channel_update)
await eventsub.listen_stream_online(str(streamer.twitch.id), self.on_stream_online)
logger.info("Twitch service started")
@@ -218,7 +220,7 @@ class TwitchService:
await sleep(self.UPDATE_DELAY)
for streamer in config.STREAMERS:
await self._on_stream_online(streamer.TWITCH.CHANNEL_ID)
await self._on_stream_online(streamer.twitch.id)
finally:
await eventsub.stop()
await self.twitch.close()