mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-26 00:34:24 -04:00
[Streams] Add kick.com streams support (#6547)
Co-authored-by: palmtree5 <3577255+palmtree5@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,10 @@ class InvalidYoutubeCredentials(StreamsError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKickCredentials(StreamsError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class YoutubeQuotaExceeded(StreamsError):
|
class YoutubeQuotaExceeded(StreamsError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from operator import is_
|
||||||
import discord
|
import discord
|
||||||
from redbot.core.utils.chat_formatting import humanize_list
|
from redbot.core.utils.chat_formatting import humanize_list
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
@@ -7,6 +8,7 @@ from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replace
|
|||||||
from redbot.core.utils.chat_formatting import escape, inline, pagify
|
from redbot.core.utils.chat_formatting import escape, inline, pagify
|
||||||
|
|
||||||
from .streamtypes import (
|
from .streamtypes import (
|
||||||
|
KickStream,
|
||||||
PicartoStream,
|
PicartoStream,
|
||||||
Stream,
|
Stream,
|
||||||
TwitchStream,
|
TwitchStream,
|
||||||
@@ -14,6 +16,7 @@ from .streamtypes import (
|
|||||||
)
|
)
|
||||||
from .errors import (
|
from .errors import (
|
||||||
APIError,
|
APIError,
|
||||||
|
InvalidKickCredentials,
|
||||||
InvalidTwitchCredentials,
|
InvalidTwitchCredentials,
|
||||||
InvalidYoutubeCredentials,
|
InvalidYoutubeCredentials,
|
||||||
OfflineStream,
|
OfflineStream,
|
||||||
@@ -51,6 +54,7 @@ class Streams(commands.Cog):
|
|||||||
"tokens": {},
|
"tokens": {},
|
||||||
"streams": [],
|
"streams": [],
|
||||||
"notified_owner_missing_twitch_secret": False,
|
"notified_owner_missing_twitch_secret": False,
|
||||||
|
"notified_owner_missing_kick_secret": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
guild_defaults = {
|
guild_defaults = {
|
||||||
@@ -70,6 +74,7 @@ class Streams(commands.Cog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.config: Config = Config.get_conf(self, 26262626)
|
self.config: Config = Config.get_conf(self, 26262626)
|
||||||
self.ttv_bearer_cache: dict = {}
|
self.ttv_bearer_cache: dict = {}
|
||||||
|
self.kick_bearer_cache: dict = {}
|
||||||
self.config.register_global(**self.global_defaults)
|
self.config.register_global(**self.global_defaults)
|
||||||
self.config.register_guild(**self.guild_defaults)
|
self.config.register_guild(**self.guild_defaults)
|
||||||
self.config.register_role(**self.role_defaults)
|
self.config.register_role(**self.role_defaults)
|
||||||
@@ -105,6 +110,8 @@ class Streams(commands.Cog):
|
|||||||
async def on_red_api_tokens_update(self, service_name, api_tokens):
|
async def on_red_api_tokens_update(self, service_name, api_tokens):
|
||||||
if service_name == "twitch":
|
if service_name == "twitch":
|
||||||
await self.get_twitch_bearer_token(api_tokens)
|
await self.get_twitch_bearer_token(api_tokens)
|
||||||
|
elif service_name == "kick":
|
||||||
|
await self.get_kick_bearer_token(api_tokens)
|
||||||
|
|
||||||
async def move_api_keys(self) -> None:
|
async def move_api_keys(self) -> None:
|
||||||
"""Move the API keys from cog stored config to core bot config if they exist."""
|
"""Move the API keys from cog stored config to core bot config if they exist."""
|
||||||
@@ -126,7 +133,7 @@ class Streams(commands.Cog):
|
|||||||
"1. Go to this page: {link}.\n"
|
"1. Go to this page: {link}.\n"
|
||||||
'2. Click "Manage" on your application.\n'
|
'2. Click "Manage" on your application.\n'
|
||||||
'3. Click on "New secret".\n'
|
'3. Click on "New secret".\n'
|
||||||
"5. Copy your client ID and your client secret into:\n"
|
"4. Copy your client ID and your client secret into:\n"
|
||||||
"{command}"
|
"{command}"
|
||||||
"\n\n"
|
"\n\n"
|
||||||
"Note: These tokens are sensitive and should only be used in a private channel "
|
"Note: These tokens are sensitive and should only be used in a private channel "
|
||||||
@@ -142,6 +149,28 @@ class Streams(commands.Cog):
|
|||||||
await send_to_owners_with_prefix_replaced(self.bot, message)
|
await send_to_owners_with_prefix_replaced(self.bot, message)
|
||||||
await self.config.notified_owner_missing_twitch_secret.set(True)
|
await self.config.notified_owner_missing_twitch_secret.set(True)
|
||||||
|
|
||||||
|
async def _notify_owner_about_missing_kick_secret(self) -> None:
|
||||||
|
message = _(
|
||||||
|
"You need a client secret key if you want to use the Kick API on this cog.\n"
|
||||||
|
"Follow these steps:\n"
|
||||||
|
"1. Go to this page: {link}.\n"
|
||||||
|
'2. Click "Manage" on your application.\n'
|
||||||
|
"3. Copy your client ID and your client secret into:\n"
|
||||||
|
"{command}"
|
||||||
|
"\n\n"
|
||||||
|
"Note: These tokens are sensitive and should only be used in a private channel "
|
||||||
|
"or in DM with the bot."
|
||||||
|
).format(
|
||||||
|
link="https://kick.com/settings/developer",
|
||||||
|
command=inline(
|
||||||
|
"[p]set api kick client_id {} client_secret {}".format(
|
||||||
|
_("<your_client_id_here>"), _("<your_client_secret_here>")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await send_to_owners_with_prefix_replaced(self.bot, message)
|
||||||
|
await self.config.notified_owner_missing_kick_secret.set(True)
|
||||||
|
|
||||||
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
||||||
tokens = (
|
tokens = (
|
||||||
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
|
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
|
||||||
@@ -198,10 +227,65 @@ class Streams(commands.Cog):
|
|||||||
self.ttv_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
self.ttv_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
||||||
|
|
||||||
async def maybe_renew_twitch_bearer_token(self) -> None:
|
async def maybe_renew_twitch_bearer_token(self) -> None:
|
||||||
if self.ttv_bearer_cache:
|
if (
|
||||||
if self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60:
|
self.ttv_bearer_cache
|
||||||
|
and self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
|
||||||
|
):
|
||||||
await self.get_twitch_bearer_token()
|
await self.get_twitch_bearer_token()
|
||||||
|
|
||||||
|
async def get_kick_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
|
||||||
|
tokens = await self.bot.get_shared_api_tokens("kick") if api_tokens is None else api_tokens
|
||||||
|
if tokens.get("client_id"):
|
||||||
|
notified_owner_missing_kick_secret = (
|
||||||
|
await self.config.notified_owner_missing_kick_secret()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tokens["client_secret"]
|
||||||
|
if notified_owner_missing_kick_secret is True:
|
||||||
|
await self.config.notified_owner_missing_kick_secret.set(False)
|
||||||
|
except KeyError:
|
||||||
|
if notified_owner_missing_kick_secret is False:
|
||||||
|
asyncio.create_task(self._notify_owner_about_missing_kick_secret())
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
"https://id.kick.com/oauth/token",
|
||||||
|
params={
|
||||||
|
"client_id": tokens.get("client_id", ""),
|
||||||
|
"client_secret": tokens.get("client_secret", ""),
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
},
|
||||||
|
) as req:
|
||||||
|
try:
|
||||||
|
data = await req.json()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if req.status == 200:
|
||||||
|
pass
|
||||||
|
elif req.status == 401 and data.get("error") == "invalid_client":
|
||||||
|
log.error("Kick API request failed authentication: set Client ID is invalid.")
|
||||||
|
elif "error" in data:
|
||||||
|
log.error(
|
||||||
|
"Kick OAuth2 API request failed with status code %s and error message: %s",
|
||||||
|
req.status,
|
||||||
|
data["error"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.error("Kick OAuth2 API request failed with status code %s", req.status)
|
||||||
|
|
||||||
|
if req.status != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.kick_bearer_cache = data
|
||||||
|
self.kick_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
|
||||||
|
|
||||||
|
async def maybe_renew_kick_token(self) -> None:
|
||||||
|
if (
|
||||||
|
self.kick_bearer_cache
|
||||||
|
and self.kick_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
|
||||||
|
):
|
||||||
|
await self.get_kick_bearer_token()
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def twitchstream(self, ctx: commands.Context, channel_name: str):
|
async def twitchstream(self, ctx: commands.Context, channel_name: str):
|
||||||
@@ -242,10 +326,19 @@ class Streams(commands.Cog):
|
|||||||
stream = PicartoStream(_bot=self.bot, name=channel_name)
|
stream = PicartoStream(_bot=self.bot, name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
|
async def kickstream(self, ctx: commands.Context, channel_name: str):
|
||||||
|
"""Check if a Kick channel is live."""
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
token = self.kick_bearer_cache.get("access_token")
|
||||||
|
stream = _streamtypes.KickStream(_bot=self.bot, name=channel_name, token=token)
|
||||||
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
async def check_online(
|
async def check_online(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
stream: Union[PicartoStream, YoutubeStream, TwitchStream],
|
stream: Union[PicartoStream, YoutubeStream, TwitchStream, KickStream],
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
info = await stream.is_online()
|
info = await stream.is_online()
|
||||||
@@ -265,6 +358,12 @@ class Streams(commands.Cog):
|
|||||||
"The YouTube API key is either invalid or has not been set. See {command}."
|
"The YouTube API key is either invalid or has not been set. See {command}."
|
||||||
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
||||||
)
|
)
|
||||||
|
except InvalidKickCredentials:
|
||||||
|
await ctx.send(
|
||||||
|
_("The Kick API key is either invalid or has not been set. See {command}.").format(
|
||||||
|
command=inline(f"{ctx.clean_prefix}streamset kicktoken")
|
||||||
|
)
|
||||||
|
)
|
||||||
except YoutubeQuotaExceeded:
|
except YoutubeQuotaExceeded:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
@@ -363,6 +462,18 @@ class Streams(commands.Cog):
|
|||||||
"""Toggle alerts in this channel for a Picarto stream."""
|
"""Toggle alerts in this channel for a Picarto stream."""
|
||||||
await self.stream_alert(ctx, PicartoStream, channel_name, discord_channel)
|
await self.stream_alert(ctx, PicartoStream, channel_name, discord_channel)
|
||||||
|
|
||||||
|
@streamalert.command(name="kick")
|
||||||
|
async def kick_alert(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
channel_name: str,
|
||||||
|
discord_channel: Union[
|
||||||
|
discord.TextChannel, discord.VoiceChannel, discord.StageChannel
|
||||||
|
] = commands.CurrentChannel,
|
||||||
|
):
|
||||||
|
"""Toggle alerts in this channel for a Kick stream."""
|
||||||
|
await self.stream_alert(ctx, KickStream, channel_name, discord_channel)
|
||||||
|
|
||||||
@streamalert.command(name="stop", usage="[disable_all=No]")
|
@streamalert.command(name="stop", usage="[disable_all=No]")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Disable all stream alerts in this channel or server.
|
"""Disable all stream alerts in this channel or server.
|
||||||
@@ -435,6 +546,7 @@ class Streams(commands.Cog):
|
|||||||
token = await self.bot.get_shared_api_tokens(_class.token_name)
|
token = await self.bot.get_shared_api_tokens(_class.token_name)
|
||||||
is_yt = _class.__name__ == "YoutubeStream"
|
is_yt = _class.__name__ == "YoutubeStream"
|
||||||
is_twitch = _class.__name__ == "TwitchStream"
|
is_twitch = _class.__name__ == "TwitchStream"
|
||||||
|
is_kick = _class.__name__ == "KickStream"
|
||||||
if is_yt and not self.check_name_or_id(channel_name):
|
if is_yt and not self.check_name_or_id(channel_name):
|
||||||
stream = _class(_bot=self.bot, id=channel_name, token=token, config=self.config)
|
stream = _class(_bot=self.bot, id=channel_name, token=token, config=self.config)
|
||||||
elif is_twitch:
|
elif is_twitch:
|
||||||
@@ -445,6 +557,10 @@ class Streams(commands.Cog):
|
|||||||
token=token.get("client_id"),
|
token=token.get("client_id"),
|
||||||
bearer=self.ttv_bearer_cache.get("access_token", None),
|
bearer=self.ttv_bearer_cache.get("access_token", None),
|
||||||
)
|
)
|
||||||
|
elif is_kick:
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
token = self.kick_bearer_cache.get("access_token")
|
||||||
|
stream = _class(_bot=self.bot, name=channel_name, token=token)
|
||||||
else:
|
else:
|
||||||
if is_yt:
|
if is_yt:
|
||||||
stream = _class(
|
stream = _class(
|
||||||
@@ -464,8 +580,7 @@ class Streams(commands.Cog):
|
|||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"The YouTube API key is either invalid or has not been set. See "
|
"The YouTube API key is either invalid or has not been set. See {command}."
|
||||||
"{command}."
|
|
||||||
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -476,6 +591,13 @@ class Streams(commands.Cog):
|
|||||||
" Try again later or contact the owner if this continues."
|
" Try again later or contact the owner if this continues."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except InvalidKickCredentials:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"The Kick API key is either invalid or has not been set. See {command}."
|
||||||
|
).format(command=inline(f"{ctx.clean_prefix}streamset kicktoken"))
|
||||||
|
)
|
||||||
|
return
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
log.error(
|
log.error(
|
||||||
"Something went wrong whilst trying to contact the stream service's API.\n"
|
"Something went wrong whilst trying to contact the stream service's API.\n"
|
||||||
@@ -537,6 +659,30 @@ class Streams(commands.Cog):
|
|||||||
|
|
||||||
await ctx.maybe_send_embed(message)
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
|
@streamset.command()
|
||||||
|
@commands.is_owner()
|
||||||
|
async def kicktoken(self, ctx: commands.Context):
|
||||||
|
"""Explain how to set the Kick token."""
|
||||||
|
message = _(
|
||||||
|
"To get one, do the following:\n"
|
||||||
|
"1. Go to this page: {link}.\n"
|
||||||
|
"2. Click on *Create new*.\n"
|
||||||
|
"3. Fill the name and description, for *Redirection URL* add *http://localhost*.\n"
|
||||||
|
"4. Click on *Create Application*.\n"
|
||||||
|
"5. Copy your client ID and your client secret into:\n"
|
||||||
|
"{command}"
|
||||||
|
"\n\n"
|
||||||
|
"Note: These tokens are sensitive and should only be used in a private channel\n"
|
||||||
|
"or in DM with the bot.\n"
|
||||||
|
).format(
|
||||||
|
link="https://kick.com/settings/developer",
|
||||||
|
command="`{}set api kick client_id {} client_secret {}`".format(
|
||||||
|
ctx.clean_prefix, _("<your_client_id_here>"), _("<your_client_secret_here>")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context):
|
async def youtubekey(self, ctx: commands.Context):
|
||||||
@@ -830,8 +976,7 @@ class Streams(commands.Cog):
|
|||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
is_rerun = False
|
is_rerun, is_schedule = False, False
|
||||||
is_schedule = False
|
|
||||||
if stream.__class__.__name__ == "TwitchStream":
|
if stream.__class__.__name__ == "TwitchStream":
|
||||||
await self.maybe_renew_twitch_bearer_token()
|
await self.maybe_renew_twitch_bearer_token()
|
||||||
embed, is_rerun = await stream.is_online()
|
embed, is_rerun = await stream.is_online()
|
||||||
@@ -839,6 +984,10 @@ class Streams(commands.Cog):
|
|||||||
elif stream.__class__.__name__ == "YoutubeStream":
|
elif stream.__class__.__name__ == "YoutubeStream":
|
||||||
embed, is_schedule = await stream.is_online()
|
embed, is_schedule = await stream.is_online()
|
||||||
|
|
||||||
|
elif stream.__class__.__name__ == "KickStream":
|
||||||
|
await self.maybe_renew_kick_token()
|
||||||
|
embed = await stream.is_online()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
embed = await stream.is_online()
|
embed = await stream.is_online()
|
||||||
except StreamNotFound:
|
except StreamNotFound:
|
||||||
@@ -1016,6 +1165,8 @@ class Streams(commands.Cog):
|
|||||||
if _class.__name__ == "TwitchStream":
|
if _class.__name__ == "TwitchStream":
|
||||||
raw_stream["token"] = token.get("client_id")
|
raw_stream["token"] = token.get("client_id")
|
||||||
raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None)
|
raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None)
|
||||||
|
elif _class.__name__ == "KickStream":
|
||||||
|
raw_stream["token"] = self.kick_bearer_cache.get("access_token", None)
|
||||||
else:
|
else:
|
||||||
if _class.__name__ == "YoutubeStream":
|
if _class.__name__ == "YoutubeStream":
|
||||||
raw_stream["config"] = self.config
|
raw_stream["config"] = self.config
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ import contextlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from dateutil.parser import parse as parse_time
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime, timezone
|
||||||
from random import choice
|
from random import choice
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
from datetime import datetime, timedelta, timezone
|
from typing import ClassVar, List, Optional, Tuple
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from typing import ClassVar, Optional, List, Tuple
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
|
from dateutil.parser import parse as parse_time
|
||||||
|
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_number
|
||||||
|
|
||||||
from .errors import (
|
from .errors import (
|
||||||
APIError,
|
APIError,
|
||||||
OfflineStream,
|
InvalidKickCredentials,
|
||||||
InvalidTwitchCredentials,
|
InvalidTwitchCredentials,
|
||||||
InvalidYoutubeCredentials,
|
InvalidYoutubeCredentials,
|
||||||
|
OfflineStream,
|
||||||
StreamNotFound,
|
StreamNotFound,
|
||||||
YoutubeQuotaExceeded,
|
YoutubeQuotaExceeded,
|
||||||
)
|
)
|
||||||
from redbot.core.i18n import Translator
|
|
||||||
from redbot.core.utils.chat_formatting import humanize_number, humanize_timedelta
|
|
||||||
|
|
||||||
TWITCH_BASE_URL = "https://api.twitch.tv"
|
TWITCH_BASE_URL = "https://api.twitch.tv"
|
||||||
TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users"
|
TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users"
|
||||||
@@ -35,6 +37,10 @@ YOUTUBE_SEARCH_ENDPOINT = YOUTUBE_BASE_URL + "/search"
|
|||||||
YOUTUBE_VIDEOS_ENDPOINT = YOUTUBE_BASE_URL + "/videos"
|
YOUTUBE_VIDEOS_ENDPOINT = YOUTUBE_BASE_URL + "/videos"
|
||||||
YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
||||||
|
|
||||||
|
KICK_BASE_URL = "https://api.kick.com/public/v1/"
|
||||||
|
KICK_USERS_ENDPOINT = KICK_BASE_URL + "users"
|
||||||
|
KICK_CHANNELS_ENDPOINT = KICK_BASE_URL + "channels"
|
||||||
|
|
||||||
_ = Translator("Streams", __file__)
|
_ = Translator("Streams", __file__)
|
||||||
|
|
||||||
log = logging.getLogger("red.core.cogs.Streams")
|
log = logging.getLogger("red.core.cogs.Streams")
|
||||||
@@ -509,3 +515,108 @@ class PicartoStream(Stream):
|
|||||||
|
|
||||||
embed.set_footer(text=_("{adult}Category: {category} | Tags: {tags}").format(**data))
|
embed.set_footer(text=_("{adult}Category: {category} | Tags: {tags}").format(**data))
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
class KickStream(Stream):
|
||||||
|
token_name = "kick"
|
||||||
|
platform_name = "Kick"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.id = kwargs.pop("id", None)
|
||||||
|
self._display_name = None
|
||||||
|
self._token = kwargs.pop("token", None)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> Optional[str]:
|
||||||
|
return self._display_name or self.name
|
||||||
|
|
||||||
|
@display_name.setter
|
||||||
|
def display_name(self, value: str) -> None:
|
||||||
|
self._display_name = value
|
||||||
|
|
||||||
|
async def get_data(self, url: str, params: dict = {}) -> Tuple[Optional[int], dict]:
|
||||||
|
if self._token is None:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {self._token}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=headers, params=params, timeout=60) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
return resp.status, {}
|
||||||
|
|
||||||
|
data = await resp.json(encoding="utf-8")
|
||||||
|
return resp.status, data["data"][0] if data["data"] else []
|
||||||
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as exc:
|
||||||
|
log.warning("Connection error occurred when fetching Kick stream", exc_info=exc)
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
async def is_online(self):
|
||||||
|
channel_code, channel_data = await self.get_data(
|
||||||
|
KICK_CHANNELS_ENDPOINT, {"slug": self.name}
|
||||||
|
)
|
||||||
|
if not channel_data:
|
||||||
|
raise StreamNotFound()
|
||||||
|
|
||||||
|
if channel_code == 200:
|
||||||
|
if channel_data["stream"]["is_live"] is False:
|
||||||
|
raise OfflineStream()
|
||||||
|
|
||||||
|
self.id = channel_data["broadcaster_user_id"]
|
||||||
|
user_profile_data = await self._fetch_user_profile()
|
||||||
|
|
||||||
|
final_data = dict.fromkeys(
|
||||||
|
("game_name", "followers", "name", "slug", "profile_picture", "view_count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_profile_data is not None:
|
||||||
|
final_data["user_name"] = self.display_name = user_profile_data["name"]
|
||||||
|
final_data["profile_picture"] = user_profile_data["profile_picture"]
|
||||||
|
|
||||||
|
stream_data = channel_data["stream"]
|
||||||
|
final_data["game_name"] = channel_data["category"]["name"]
|
||||||
|
final_data["title"] = channel_data["stream_title"]
|
||||||
|
final_data["thumbnail_url"] = stream_data["thumbnail"]
|
||||||
|
final_data["view_count"] = stream_data["viewer_count"]
|
||||||
|
final_data["slug"] = channel_data["slug"]
|
||||||
|
|
||||||
|
return self.make_embed(final_data)
|
||||||
|
elif channel_code == 401:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
elif channel_code == 400:
|
||||||
|
raise StreamNotFound()
|
||||||
|
else:
|
||||||
|
raise APIError(channel_code, stream_data)
|
||||||
|
|
||||||
|
async def _fetch_user_profile(self):
|
||||||
|
code, data = await self.get_data(KICK_USERS_ENDPOINT, {"id": self.id})
|
||||||
|
if code == 200:
|
||||||
|
if not data:
|
||||||
|
raise StreamNotFound()
|
||||||
|
return data
|
||||||
|
elif code == 400:
|
||||||
|
raise StreamNotFound()
|
||||||
|
elif code == 401:
|
||||||
|
raise InvalidKickCredentials()
|
||||||
|
else:
|
||||||
|
raise APIError(code, data)
|
||||||
|
|
||||||
|
def make_embed(self, data):
|
||||||
|
url = f"https://www.kick.com/{data['slug']}" if data["slug"] is not None else None
|
||||||
|
logo = (
|
||||||
|
data["profile_picture"] or "https://www.google.com/s2/favicons?domain=kick.com&sz=256"
|
||||||
|
)
|
||||||
|
status = data["title"] or _("Untitled broadcast")
|
||||||
|
embed = discord.Embed(title=status, url=url, color=0x00E701)
|
||||||
|
embed.set_author(name=data["user_name"])
|
||||||
|
embed.add_field(name=_("Total views"), value=humanize_number(data["view_count"]))
|
||||||
|
embed.set_thumbnail(url=logo)
|
||||||
|
if data["thumbnail_url"]:
|
||||||
|
embed.set_image(url=rnd(data["thumbnail_url"]))
|
||||||
|
if data["game_name"]:
|
||||||
|
embed.set_footer(text=_("Playing: ") + data["game_name"])
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{0.__class__.__name__}: {0.name} (ID: {0.id})>".format(self)
|
||||||
|
|||||||
Reference in New Issue
Block a user