Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions components/discordcog.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
import os
import sys
from typing import Literal, Annotated

from pydantic import BaseModel, Base64Bytes, field_validator, AwareDatetime
from twitchio.ext.commands import Component, is_broadcaster

sys.path.append("..")
from config import discord_channel, discord_role

import datetime
import json

import pika
from twitch_commands import twitch_command_aliased
from twitchio.ext import commands


class Attachment(BaseModel):
filename: str
data: Base64Bytes

@field_validator("filename")
@classmethod
def validate_filename(cls, v: str) -> str:
if not v or v.strip() == "":
raise ValueError("filename cannot be empty")

# prevent path tricks
if os.path.basename(v) != v:
raise ValueError("filename must not contain path components")

return v


class SendDiscordMessage(BaseModel):
expires_at: Annotated[datetime.datetime, AwareDatetime]
action: Literal["send"]
attachment: Attachment | None = None
body: str
channel: str | None = None


class DiscordCog(Component):
def __init__(self, bot):
self.bot = bot
Expand All @@ -26,7 +52,7 @@ async def cmd_announce(self, ctx: commands.Context):
# noinspection PyMethodMayBeStatic
async def announce(self, text):
announcement = f"@{discord_role} " + text
delta = self.bot.countdown_to - datetime.datetime.now()
delta = self.bot.countdown_to - datetime.datetime.now().astimezone()

connection = pika.BlockingConnection(
pika.URLParameters(os.getenv("RABBIT_URL"))
Expand All @@ -35,12 +61,17 @@ async def announce(self, text):
channel.queue_declare(
queue="discord", durable=True, arguments={"x-message-ttl": 60000}
)
msg = SendDiscordMessage(
expires_at=self.bot.countdown_to,
action="send",
attachment=None,
body=announcement,
channel=discord_channel,
)
channel.basic_publish(
exchange="",
routing_key="discord",
body=json.dumps(
{"action": "send", "message": announcement, "channel": discord_channel}
).encode("utf-8"),
body=msg.model_dump_json(ensure_ascii=False),
properties=pika.BasicProperties(expiration=str(delta.seconds * 1000)),
)
channel.close()
Expand Down
19 changes: 19 additions & 0 deletions components/misccog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import telegram
import twitchio
from loguru import logger
import requests
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
from twitchio.ext import commands
from twitchio.ext.commands import is_broadcaster
Expand Down Expand Up @@ -77,6 +78,21 @@ async def event_message(self, payload: twitchio.ChatMessage) -> None:
async def cmd_ping(self, ctx: commands.Context):
await ctx.send("Yeth, Mathter?")

@commands.Component.listener()
async def event_raid(self, _: twitchio.ChannelRaid):
if self.bot.game.stars:
today = datetime.date.today()
resp = requests.post(
f"https://stars.iarazumov.com/stream/{today.strftime("%Y-%m-%d")}/end",
json={"name": self.bot.title},
headers={"Authorization": os.getenv("STARS_TOKEN")},
)

try:
resp.raise_for_status()
except Exception as e:
logger.opt(exception=e).exception("Failed to register stream!")

# noinspection PyUnusedLocal
@commands.Component.listener()
async def event_stream_online(self, payload: twitchio.StreamOnline) -> None:
Expand All @@ -90,6 +106,9 @@ async def event_stream_online(self, payload: twitchio.StreamOnline) -> None:
)

ann_texts = await self.bot.get_announce_text()
if not ann_texts:
logger.info("get_announce_text returned empty string - skipping announce")
return

logger.info("Getting Discord cog...")
discord_cog = self.bot.get_component("DiscordCog")
Expand Down
109 changes: 66 additions & 43 deletions components/obscog.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from obswebsocket import requests as obsws_requests
from pytils import numeral
from twitchio.ext import commands
from twitchio.ext.commands import Component
from twitchio.ext.commands import Component, Context
from twitchio.ext.commands import is_broadcaster

from models import SourceConfig
Expand Down Expand Up @@ -361,11 +361,13 @@ def write_countdown_html():
if len(parts) == 2:
m, s = parts
# noinspection PyShadowingNames
end_time = datetime.timedelta(minutes=m, seconds=s)
end_time = datetime.datetime.now() + end_time
delta = datetime.timedelta(minutes=m, seconds=s)
end_time = datetime.datetime.now().astimezone() + delta
elif len(parts) == 3:
h, m, s = parts
end_time = datetime.datetime.now().replace(hour=h, minute=m, second=s)
today = datetime.date.today()
time_ = datetime.time(hour=h, minute=m, second=s)
end_time = datetime.datetime.combine(today, time_).astimezone()
else:
self.bot.logger.error("Invalid call to countdown: {0}".format(args[0]))
return
Expand Down Expand Up @@ -433,10 +435,24 @@ def write_countdown_html():

asyncio.ensure_future(self.bot.my_run_commercial(self.bot.owner_id))

now = datetime.datetime.now()
now = datetime.datetime.now().astimezone()
dt = self.bot.countdown_to - now

asyncio.ensure_future(self.hide_zeroes(dt.seconds))

if self.bot.game.stars:
today = now.date()
resp = requests.post(
f"https://stars.iarazumov.com/stream/{today.strftime("%Y-%m-%d")}/start",
json={"name": self.bot.title},
headers={"Authorization": os.getenv("STARS_TOKEN")},
)

try:
resp.raise_for_status()
except Exception as e:
logger.opt(exception=e).exception("Failed to register stream!")

# @routines.routine(seconds=s, minutes=m, hours=h, wait_first=True,
# iterations=1)

Expand Down Expand Up @@ -547,45 +563,12 @@ async def start_(self, ctx: commands.Context):
self.set_music_source("")
await self.bot.update_track_text()

await self.bot.get_game_v5()

# if self.bot.game.use_game_capture:
self.show_hide_scene_item("Game", "Game Capture", True)
self.show_hide_scene_item("Game", "Window Capture", False)
if self.bot.game.obs_window != "X":
source: obsws_requests.GetInputSettings = self.ws.call(
obsws_requests.GetInputSettings(inputName="Game Capture")
)
settings = source.getInputSettings()
parts = self.bot.game.obs_window.split("\r\n")
while len(parts) < 3:
parts.append("")

win, pid = OBSCog.find_window_by_title_and_class(
parts[1], parts[2], self.bot.game.obs_window_title_glob
)
if win is None:
logger.error(f"Can't find window title={parts[1]}, class={parts[2]}!")
await ctx.send("Окно игры не найдено")
else:
settings["capture_window"] = f"{win.id}"
self.ws.call(
obsws_requests.SetInputSettings(
inputName="Game Capture",
inputSettings=settings,
overlay=False,
)
)
await ctx.send("Захват окна настроен")

if pid:
OBSCog.move_pid_to_sink(pid, "GameSink")
await ctx.send("Захват звука настроен")
else:
logger.error(f"Window {win} {win.id} doesn't have a pid!")
await ctx.send("Процесс игры не найден!")
logger.debug(f"Set capture window to {settings['capture_window']}")
# else:
# self.show_hide_scene_item("Game", "Game Capture", False)
# self.show_hide_scene_item("Game", "Window Capture", True)
await self.set_capture_window(ctx)

scene_obj: SourceConfig
for scene_obj in SourceConfig.select(
Expand Down Expand Up @@ -627,6 +610,44 @@ async def start_(self, ctx: commands.Context):

self.ws_call(obsws_requests.StartRecord())

async def set_capture_window(self, ctx: Context):
if self.bot.game.obs_window != "X":
source: obsws_requests.GetInputSettings = self.ws.call(
obsws_requests.GetInputSettings(inputName="Game Capture")
)
settings = source.getInputSettings()
parts = self.bot.game.obs_window.split("\r\n")
while len(parts) < 3:
parts.append("")

win, pid = OBSCog.find_window_by_title_and_class(
parts[1], parts[2], self.bot.game.obs_window_title_glob
)
if win is None:
logger.error(f"Can't find window title={parts[1]}, class={parts[2]}!")
await ctx.send("Окно игры не найдено")
else:
settings["capture_window"] = f"{win.id}"
self.ws.call(
obsws_requests.SetInputSettings(
inputName="Game Capture",
inputSettings=settings,
overlay=False,
)
)
await ctx.send("Захват окна настроен")

if pid:
OBSCog.move_pid_to_sink(pid, "GameSink")
await ctx.send("Захват звука настроен")
else:
logger.error(f"Window {win} {win.id} doesn't have a pid!")
await ctx.send("Процесс игры не найден!")
logger.debug(f"Set capture window to {settings['capture_window']}")
# else:
# self.show_hide_scene_item("Game", "Game Capture", False)
# self.show_hide_scene_item("Game", "Window Capture", True)

@is_broadcaster()
@twitch_command_aliased(name="resume")
async def resume(self, ctx: commands.Context):
Expand Down Expand Up @@ -679,6 +700,9 @@ async def resume(self, ctx: commands.Context):
return

self.switch_to("Game")
# self.show_hide_scene_item("Game", "Game Capture", True)
# self.show_hide_scene_item("Game", "Window Capture", False)
await self.set_capture_window(ctx)

try:
res = await self.bot.my_get_stream()
Expand Down Expand Up @@ -796,8 +820,7 @@ async def enable_rip(self, state):
@is_broadcaster()
@twitch_command_aliased(name="save")
async def save_window(self, ctx: commands.Context):
if self.bot.game is None:
self.bot.get_game_v5()
await self.bot.get_game_v5()

source = self.ws.call(obsws_requests.GetInputSettings(inputName="Game Capture"))

Expand Down
17 changes: 17 additions & 0 deletions migrations/0014_stars_flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sys

sys.path.insert(0, "..")

from playhouse.migrate import *

from config import database_file

my_db = SqliteDatabase("../" + database_file)
migrator = SqliteMigrator(my_db)

stars_field = BooleanField(default=False)

with my_db.atomic():
migrate(
migrator.add_column("gameconfig", "stars", stars_field),
)
Loading
Loading