Refactor streamers configs

This commit is contained in:
2024-11-17 02:12:07 +01:00
parent 3197a788f8
commit cb83a4f03f
9 changed files with 87 additions and 132 deletions

View File

@@ -1,43 +0,0 @@
[twitch]
id = 59900845
name = "hafmc"
[notifications]
start_stream = '''
HafMC стримит {title} ({category})!
Присоединяйся: https://twitch.tv/hafmc
{role}
'''
change_category = '''
HafMC играет в {category} ({title})!
Присоединяйся: https://twitch.tv/hafmc
{role}
'''
[integrations]
[integrations.discord]
guild_id = 1198051900906549329
notifications_channel_id = 1198296540964475002
[integrations.discord.games_list]
channel_id = 1201810638800691210
message_id = 1239664178038313012
[integrations.discord.roles]
"Counter-Strike" = 1198372462476398823
"SIGame" = 1198372523046346833
"Battlefield 1" = 1198377604533735454
"Battlefield V" = 1198377604533735454
"Among Us" = 1198377604533735454
"BattleBit Remastered" = 1198381867343286352
"Lethal Company" = 1198383849785270302
"HELLDIVERS 2" = 1230292196805054488
"Satisfactory" = 1292091826802528308
[integrations.telegram]
notifications_channel_id = -1001939021131

View File

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

View File

@@ -1,39 +1,4 @@
import tomllib
from pydantic import BaseModel, field_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pathlib import Path
class TwitchConfig(BaseModel):
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
notifications_channel_id: int
games_list: GamesListConfig | None = None
roles: dict[str, int] | None = None
class TelegramConfig(BaseModel):
notifications_channel_id: int
class IntegrationsConfig(BaseModel):
discord: DiscordConfig | None = None
telegram: TelegramConfig | None = None
class StreamerConfig(BaseModel):
twitch: TwitchConfig
notifications: NotificationsConfig
integrations: IntegrationsConfig
class Config(BaseSettings): class Config(BaseSettings):
@@ -51,22 +16,9 @@ class Config(BaseSettings):
TWITCH_CALLBACK_URL: str TWITCH_CALLBACK_URL: str
TWITCH_CALLBACK_PORT: int = 80 TWITCH_CALLBACK_PORT: int = 80
STREAMERS: list[StreamerConfig] = []
MONGODB_URI: str MONGODB_URI: str
SECRETS_FILE_PATH: str SECRETS_FILE_PATH: str
@field_validator("STREAMERS", mode="before")
def check_streamers(cls, 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 config = Config() # type: ignore

31
src/domain/streamers.py Normal file
View File

@@ -0,0 +1,31 @@
from pydantic import BaseModel
class TwitchConfig(BaseModel):
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
notifications_channel_id: int
games_list: GamesListConfig | None = None
roles: dict[str, int] | None = None
class TelegramConfig(BaseModel):
notifications_channel_id: int
class IntegrationsConfig(BaseModel):
discord: DiscordConfig | None = None
telegram: TelegramConfig | None = None
class StreamerConfig(BaseModel):
twitch: TwitchConfig
notifications: NotificationsConfig
integrations: IntegrationsConfig

View File

@@ -8,15 +8,18 @@ from discord import app_commands
from modules.games_list.games_list import GameList, GameItem from modules.games_list.games_list import GameList, GameItem
from core.config import config from core.config import config
from repositories.streamers import StreamerConfigRepository
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_game_list_channel_to_message_map() -> dict[int, int]: async def get_game_list_channel_to_message_map() -> dict[int, int]:
result = {} result = {}
for streamer in config.STREAMERS: streamers = await StreamerConfigRepository.all()
for streamer in streamers:
if (integration := streamer.integrations.discord) is None: if (integration := streamer.integrations.discord) is None:
continue continue
@@ -41,7 +44,9 @@ class DiscordClient(discord.Client):
self.tree = app_commands.CommandTree(self) self.tree = app_commands.CommandTree(self)
async def setup_hook(self): async def setup_hook(self):
for streamer in config.STREAMERS: streamers = await StreamerConfigRepository.all()
for streamer in streamers:
if (integration := streamer.integrations.discord) is None: if (integration := streamer.integrations.discord) is None:
continue continue
@@ -82,7 +87,7 @@ async def add(
game: str, game: str,
date: str | None = None date: str | None = None
): ):
channel_to_message = get_game_list_channel_to_message_map() channel_to_message = await get_game_list_channel_to_message_map()
if interaction.channel is None: if interaction.channel is None:
await interaction.response.send_message("Команда не доступна в этом канале (#1)", ephemeral=True) await interaction.response.send_message("Команда не доступна в этом канале (#1)", ephemeral=True)
@@ -114,7 +119,7 @@ async def game_list_autocomplete(
if not isinstance(interaction.channel, Messageable): if not isinstance(interaction.channel, Messageable):
return [] return []
channel_to_message = get_game_list_channel_to_message_map() channel_to_message = await get_game_list_channel_to_message_map()
message_id = channel_to_message.get(interaction.channel.id) message_id = channel_to_message.get(interaction.channel.id)
if message_id is None: if message_id is None:
return [] return []
@@ -130,7 +135,7 @@ async def game_list_autocomplete(
@app_commands.describe(game="Игра") @app_commands.describe(game="Игра")
@app_commands.autocomplete(game=game_list_autocomplete) @app_commands.autocomplete(game=game_list_autocomplete)
async def delete(interaction: discord.Interaction, game: str): async def delete(interaction: discord.Interaction, game: str):
channel_to_message = get_game_list_channel_to_message_map() channel_to_message = await get_game_list_channel_to_message_map()
if interaction.channel is None: if interaction.channel is None:
await interaction.response.send_message("Команда не доступна в этом канале (#1)", ephemeral=True) await interaction.response.send_message("Команда не доступна в этом канале (#1)", ephemeral=True)

View File

@@ -2,7 +2,10 @@ from asyncio import sleep
import logging import logging
from datetime import datetime from datetime import datetime
from core.config import config, TwitchConfig from core.config import config
from domain.streamers import TwitchConfig
from repositories.streamers import StreamerConfigRepository
from .twitch_events import get_twitch_events, TwitchEvent from .twitch_events import get_twitch_events, TwitchEvent
from .discord_events import ( from .discord_events import (
@@ -101,7 +104,9 @@ async def start_synchronizer():
while True: while True:
try: try:
for streamer in config.STREAMERS: streamers = await StreamerConfigRepository().all()
for streamer in streamers:
if (integration := streamer.integrations.discord) is None: if (integration := streamer.integrations.discord) is None:
continue continue
@@ -109,4 +114,4 @@ async def start_synchronizer():
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
await sleep(5 * 30) await sleep(5 * 60)

View File

@@ -8,8 +8,9 @@ from twitchAPI.twitch import Twitch
from twitchAPI.type import AuthScope from twitchAPI.type import AuthScope
from twitchAPI.object.eventsub import StreamOnlineEvent, ChannelUpdateEvent from twitchAPI.object.eventsub import StreamOnlineEvent, ChannelUpdateEvent
from core.config import config, StreamerConfig from core.config import config
from modules.stream_notifications.notification import notify from modules.stream_notifications.notification import notify
from repositories.streamers import StreamerConfigRepository
from .state import State from .state import State
from .token_storage import TokenStorage from .token_storage import TokenStorage
@@ -50,19 +51,12 @@ class TwitchService:
return twitch return twitch
def get_streamer_config(self, streamer_id: int) -> StreamerConfig:
for streamer in config.STREAMERS:
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: int): async def notify_online(self, streamer_id: int):
current_state = self.state.get(streamer_id) current_state = self.state.get(streamer_id)
if current_state is None: if current_state is None:
raise RuntimeError("State is None") raise RuntimeError("State is None")
streamer = self.get_streamer_config(streamer_id) streamer = await StreamerConfigRepository.get_by_twitch_id(streamer_id)
if streamer.notifications.start_stream is None: if streamer.notifications.start_stream is None:
return return
@@ -78,7 +72,7 @@ class TwitchService:
if (datetime.now() - current_state.last_live_at).seconds >= self.ONLINE_NOTIFICATION_DELAY: if (datetime.now() - current_state.last_live_at).seconds >= self.ONLINE_NOTIFICATION_DELAY:
return return
streamer = self.get_streamer_config(streamer_id) streamer = await StreamerConfigRepository.get_by_twitch_id(streamer_id)
if streamer.notifications.change_category is None: if streamer.notifications.change_category is None:
return return
@@ -154,7 +148,9 @@ class TwitchService:
message_deduplication_history_length=50 message_deduplication_history_length=50
) )
for streamer in config.STREAMERS: streamers = await StreamerConfigRepository.all()
for streamer in streamers:
current_stream = await self.get_current_stream(streamer.twitch.id) current_stream = await self.get_current_stream(streamer.twitch.id)
if current_stream: if current_stream:
@@ -173,7 +169,7 @@ class TwitchService:
logger.info("Subscribe to events...") logger.info("Subscribe to events...")
for streamer in config.STREAMERS: for streamer in streamers:
logger.info(f"Subscribe to events for {streamer.twitch.name}") logger.info(f"Subscribe to events for {streamer.twitch.name}")
await eventsub.listen_channel_update_v2(str(streamer.twitch.id), self.on_channel_update) 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) await eventsub.listen_stream_online(str(streamer.twitch.id), self.on_stream_online)
@@ -184,7 +180,7 @@ class TwitchService:
while True: while True:
await sleep(self.UPDATE_DELAY) await sleep(self.UPDATE_DELAY)
for streamer in config.STREAMERS: for streamer in streamers:
await self._on_stream_online(streamer.twitch.id) await self._on_stream_online(streamer.twitch.id)
finally: finally:
await eventsub.stop() await eventsub.stop()

View File

@@ -0,0 +1,28 @@
from domain.streamers import StreamerConfig
from core.mongo import mongo_manager
class StreamerConfigRepository:
COLLECTION_NAME = "streamers"
@classmethod
async def get_by_twitch_id(cls, twitch_id: int) -> StreamerConfig:
async with mongo_manager.connect() as client:
db = client.get_default_database()
collection = db[cls.COLLECTION_NAME]
doc = await collection.find_one({"twitch.id": twitch_id})
if doc is None:
raise ValueError(f"Streamer with twitch id {twitch_id} not found")
return StreamerConfig(**doc)
@classmethod
async def all(cls) -> list[StreamerConfig]:
async with mongo_manager.connect() as client:
db = client.get_default_database()
collection = db[cls.COLLECTION_NAME]
cursor = await collection.find()
return [StreamerConfig(**doc) async for doc in cursor]