mirror of
https://github.com/kurbezz/discord-bot.git
synced 2025-12-06 15:15:37 +01:00
Update
This commit is contained in:
121
poetry.lock
generated
121
poetry.lock
generated
@@ -1,10 +1,9 @@
|
||||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "24.1.0"
|
||||
description = "File support for asyncio."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -16,7 +15,6 @@ files = [
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.3.5"
|
||||
description = "Happy Eyeballs for asyncio"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -28,7 +26,6 @@ files = [
|
||||
name = "aiohttp"
|
||||
version = "3.10.2"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -125,7 +122,6 @@ speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
|
||||
name = "aiosignal"
|
||||
version = "1.3.1"
|
||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
@@ -140,7 +136,6 @@ frozenlist = ">=1.1.0"
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -148,11 +143,30 @@ files = [
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.4.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
||||
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
@@ -168,11 +182,21 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
|
||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.7.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "discord-py"
|
||||
version = "2.4.0"
|
||||
description = "A Python wrapper for the Discord API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -193,7 +217,6 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"]
|
||||
name = "frozenlist"
|
||||
version = "1.4.1"
|
||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -276,11 +299,66 @@ files = [
|
||||
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.5"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
@@ -292,7 +370,6 @@ files = [
|
||||
name = "multidict"
|
||||
version = "6.0.5"
|
||||
description = "multidict implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
@@ -392,7 +469,6 @@ files = [
|
||||
name = "pydantic"
|
||||
version = "2.8.2"
|
||||
description = "Data validation using Python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -415,7 +491,6 @@ email = ["email-validator (>=2.0.0)"]
|
||||
name = "pydantic-core"
|
||||
version = "2.20.1"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -517,7 +592,6 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
name = "pydantic-settings"
|
||||
version = "2.4.0"
|
||||
description = "Settings management using Pydantic"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -538,7 +612,6 @@ yaml = ["pyyaml (>=6.0.1)"]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
@@ -553,7 +626,6 @@ six = ">=1.5"
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -568,7 +640,6 @@ cli = ["click (>=5.0)"]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
@@ -576,11 +647,21 @@ files = [
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twitchapi"
|
||||
version = "4.2.1"
|
||||
description = "A Python 3.7+ implementation of the Twitch Helix API, PubSub, EventSub and Chat"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
@@ -597,7 +678,6 @@ typing-extensions = "*"
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
@@ -609,7 +689,6 @@ files = [
|
||||
name = "yarl"
|
||||
version = "1.9.4"
|
||||
description = "Yet another URL library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
@@ -712,4 +791,4 @@ multidict = ">=4.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "ecdeecc583d261bc5c6fbf0c07c2d31e7b11941f2a63667d280b14a9104dae03"
|
||||
content-hash = "20a20db770b371c4b3e388034c4bd30335349d9a69f61a52c997ea3d2fdcd4fe"
|
||||
|
||||
@@ -13,6 +13,7 @@ twitchapi = "^4.2.1"
|
||||
pydantic = "^2.8.2"
|
||||
pydantic-settings = "^2.4.0"
|
||||
aiofiles = "^24.1.0"
|
||||
httpx = "^0.27.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
36
src/services/notification.py
Normal file
36
src/services/notification.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from asyncio import gather
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from config import config
|
||||
|
||||
|
||||
async def notify_telegram(msg: str):
|
||||
async with AsyncClient() as client:
|
||||
await client.post(
|
||||
f"https://api.telegram.org/bot{config.TELEGRAM_BOT_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": config.TELEGRAM_CHANNEL_ID,
|
||||
"text": msg,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def notify_discord(msg: str):
|
||||
async with AsyncClient() as client:
|
||||
await client.post(
|
||||
f"https://discord.com/api/v10/channels/{config.DISCORD_CHANNEL_ID}/messages",
|
||||
headers={
|
||||
"Authorization": f"Bot {config.DISCORD_BOT_TOKEN}"
|
||||
},
|
||||
json={
|
||||
"content": msg,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def notify(msg: str):
|
||||
await gather(
|
||||
notify_telegram(msg),
|
||||
notify_discord(msg)
|
||||
)
|
||||
@@ -1,18 +1,26 @@
|
||||
from asyncio import Lock, sleep
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from twitchAPI.helper import first
|
||||
from twitchAPI.eventsub.webhook import EventSubWebhook
|
||||
from twitchAPI.twitch import Twitch
|
||||
from twitchAPI.type import AuthScope
|
||||
from twitchAPI.object.eventsub import ChannelChatMessageEvent, StreamOnlineEvent, StreamOfflineEvent
|
||||
from twitchAPI.object.eventsub import ChannelChatMessageEvent, StreamOnlineEvent, StreamOfflineEvent, ChannelUpdateEvent
|
||||
|
||||
import aiofiles
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import config
|
||||
from services.notification import notify
|
||||
|
||||
|
||||
class State:
|
||||
pass
|
||||
class State(BaseModel):
|
||||
title: str
|
||||
category: str
|
||||
is_live: bool
|
||||
last_live_at: datetime
|
||||
|
||||
|
||||
class TokenStorage:
|
||||
@@ -42,6 +50,8 @@ class TwitchService:
|
||||
AuthScope.CHAT_EDIT,
|
||||
]
|
||||
|
||||
ONLINE_NOTIFICATION_DELAY = 5 * 60
|
||||
|
||||
def __init__(self, twitch: Twitch):
|
||||
self.twitch = twitch
|
||||
|
||||
@@ -61,14 +71,83 @@ class TwitchService:
|
||||
|
||||
return twitch
|
||||
|
||||
async def notify_online(self):
|
||||
if self.state is None:
|
||||
raise RuntimeError("State is None")
|
||||
|
||||
msg = f"HafMC сейчас стримит {self.state.title} ({self.state.category})! \nПрисоединяйся: https://twitch.tv/hafmc"
|
||||
|
||||
await notify(msg)
|
||||
|
||||
async def notify_change_category(self):
|
||||
if self.state is None:
|
||||
raise RuntimeError("State is None")
|
||||
|
||||
msg = f"HafMC начал играть в {self.state.category}! \nПрисоединяйся: https://twitch.tv/hafmc"
|
||||
|
||||
await notify(msg)
|
||||
|
||||
async def get_current_stream(self, retry_count: int = 5, delay: int = 5):
|
||||
remain_retry = retry_count
|
||||
|
||||
while remain_retry > 0:
|
||||
stream = await first(self.twitch.get_streams(user_id=[config.TWITCH_CHANNEL_ID]))
|
||||
|
||||
if stream is not None:
|
||||
return stream
|
||||
|
||||
remain_retry -= 1
|
||||
await sleep(delay)
|
||||
|
||||
return None
|
||||
|
||||
async def on_channel_chat_message(self, event: ChannelChatMessageEvent):
|
||||
print("on_channel_chat_message", event)
|
||||
if self.state is None or (datetime.now() - self.state.last_live_at).seconds <= self.ONLINE_NOTIFICATION_DELAY:
|
||||
return
|
||||
|
||||
current_stream = await self.get_current_stream()
|
||||
if current_stream is None:
|
||||
return
|
||||
|
||||
self.state.last_live_at = datetime.now()
|
||||
|
||||
async def on_channel_update(self, event: ChannelUpdateEvent):
|
||||
if self.state is None:
|
||||
return
|
||||
|
||||
if self.state.category == event.event.category_name:
|
||||
return
|
||||
|
||||
self.state.title = event.event.title
|
||||
self.state.category = event.event.category_name
|
||||
self.state.last_live_at = datetime.now()
|
||||
|
||||
await self.notify_change_category()
|
||||
|
||||
async def on_stream_online(self, event: StreamOnlineEvent):
|
||||
print("on_stream_online", event)
|
||||
current_stream = await self.get_current_stream()
|
||||
if current_stream is None:
|
||||
raise RuntimeError("Stream not found")
|
||||
|
||||
state = State(
|
||||
title=current_stream.title,
|
||||
category=current_stream.game_name,
|
||||
is_live=True,
|
||||
last_live_at=datetime.now()
|
||||
)
|
||||
|
||||
if self.state is None:
|
||||
self.state = state
|
||||
await self.notify_online()
|
||||
|
||||
if (datetime.now() - self.state.last_live_at).seconds >= self.ONLINE_NOTIFICATION_DELAY:
|
||||
self.state = state
|
||||
await self.notify_online()
|
||||
|
||||
async def on_stream_offline(self, event: StreamOfflineEvent):
|
||||
print("on_stream_offline", event)
|
||||
if self.state:
|
||||
self.state.is_live = False
|
||||
self.last_live_at = datetime.now()
|
||||
|
||||
async def run(self):
|
||||
eventsub = EventSubWebhook(
|
||||
@@ -78,12 +157,22 @@ class TwitchService:
|
||||
message_deduplication_history_length=50
|
||||
)
|
||||
|
||||
current_stream = await self.get_current_stream()
|
||||
if current_stream:
|
||||
self.state = State(
|
||||
title=current_stream.title,
|
||||
category=current_stream.game_name,
|
||||
is_live=current_stream.type == "live",
|
||||
last_live_at=datetime.now()
|
||||
)
|
||||
|
||||
try:
|
||||
await eventsub.unsubscribe_all()
|
||||
|
||||
eventsub.start()
|
||||
|
||||
await eventsub.listen_channel_chat_message(config.TWITCH_CHANNEL_ID, config.TWITCH_ADMIN_USER_ID, self.on_channel_chat_message)
|
||||
await eventsub.listen_channel_update_v2(config.TWITCH_CHANNEL_ID, self.on_channel_update)
|
||||
await eventsub.listen_stream_online(config.TWITCH_CHANNEL_ID, self.on_stream_online)
|
||||
await eventsub.listen_stream_offline(config.TWITCH_CHANNEL_ID, self.on_stream_offline)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user