Skip to content

Commit b8e47c0

Browse files
committed
[Streams] Add kick.com streams alerts
1 parent 6f5f34c commit b8e47c0

File tree

3 files changed

+282
-16
lines changed

3 files changed

+282
-16
lines changed

redbot/cogs/streams/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class InvalidYoutubeCredentials(StreamsError):
2727
pass
2828

2929

30+
class InvalidKickCredentials(StreamsError):
31+
pass
32+
33+
3034
class YoutubeQuotaExceeded(StreamsError):
3135
pass
3236

redbot/cogs/streams/streams.py

Lines changed: 157 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from operator import is_
12
import discord
23
from redbot.core.utils.chat_formatting import humanize_list
34
from redbot.core.bot import Red
@@ -7,13 +8,15 @@
78
from redbot.core.utils.chat_formatting import escape, inline, pagify
89

910
from .streamtypes import (
11+
KickStream,
1012
PicartoStream,
1113
Stream,
1214
TwitchStream,
1315
YoutubeStream,
1416
)
1517
from .errors import (
1618
APIError,
19+
InvalidKickCredentials,
1720
InvalidTwitchCredentials,
1821
InvalidYoutubeCredentials,
1922
OfflineStream,
@@ -51,6 +54,7 @@ class Streams(commands.Cog):
5154
"tokens": {},
5255
"streams": [],
5356
"notified_owner_missing_twitch_secret": False,
57+
"notified_owner_missing_kick_secret": False,
5458
}
5559

5660
guild_defaults = {
@@ -70,6 +74,7 @@ def __init__(self, bot: Red):
7074
super().__init__()
7175
self.config: Config = Config.get_conf(self, 26262626)
7276
self.ttv_bearer_cache: dict = {}
77+
self.kick_bearer_cache: dict = {}
7378
self.config.register_global(**self.global_defaults)
7479
self.config.register_guild(**self.guild_defaults)
7580
self.config.register_role(**self.role_defaults)
@@ -105,6 +110,8 @@ async def cog_load(self) -> None:
105110
async def on_red_api_tokens_update(self, service_name, api_tokens):
106111
if service_name == "twitch":
107112
await self.get_twitch_bearer_token(api_tokens)
113+
elif service_name == "kick":
114+
await self.get_kick_bearer_token(api_tokens)
108115

109116
async def move_api_keys(self) -> None:
110117
"""Move the API keys from cog stored config to core bot config if they exist."""
@@ -126,7 +133,7 @@ async def _notify_owner_about_missing_twitch_secret(self) -> None:
126133
"1. Go to this page: {link}.\n"
127134
'2. Click "Manage" on your application.\n'
128135
'3. Click on "New secret".\n'
129-
"5. Copy your client ID and your client secret into:\n"
136+
"4. Copy your client ID and your client secret into:\n"
130137
"{command}"
131138
"\n\n"
132139
"Note: These tokens are sensitive and should only be used in a private channel "
@@ -142,6 +149,28 @@ async def _notify_owner_about_missing_twitch_secret(self) -> None:
142149
await send_to_owners_with_prefix_replaced(self.bot, message)
143150
await self.config.notified_owner_missing_twitch_secret.set(True)
144151

152+
async def _notify_owner_about_missing_kick_secret(self) -> None:
153+
message = _(
154+
"You need a client secret key if you want to use the Kick API on this cog.\n"
155+
"Follow these steps:\n"
156+
"1. Go to this page: {link}.\n"
157+
'2. Click "Manage" on your application.\n'
158+
"3. Copy your client ID and your client secret into:\n"
159+
"{command}"
160+
"\n\n"
161+
"Note: These tokens are sensitive and should only be used in a private channel "
162+
"or in DM with the bot."
163+
).format(
164+
link="https://kick.com/settings/developer",
165+
command=inline(
166+
"[p]set api twitch client_id {} client_secret {}".format(
167+
_("<your_client_id_here>"), _("<your_client_secret_here>")
168+
)
169+
),
170+
)
171+
await send_to_owners_with_prefix_replaced(self.bot, message)
172+
await self.config.notified_owner_missing_kick_secret.set(True)
173+
145174
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
146175
tokens = (
147176
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
@@ -198,9 +227,64 @@ async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> No
198227
self.ttv_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
199228

200229
async def maybe_renew_twitch_bearer_token(self) -> None:
201-
if self.ttv_bearer_cache:
202-
if self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60:
203-
await self.get_twitch_bearer_token()
230+
if (
231+
self.ttv_bearer_cache
232+
and self.ttv_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
233+
):
234+
await self.get_twitch_bearer_token()
235+
236+
async def get_kick_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
237+
tokens = await self.bot.get_shared_api_tokens("kick") if api_tokens is None else api_tokens
238+
if tokens.get("client_id"):
239+
notified_owner_missing_kick_secret = (
240+
await self.config.notified_owner_missing_kick_secret()
241+
)
242+
try:
243+
tokens["client_secret"]
244+
if notified_owner_missing_kick_secret is True:
245+
await self.config.notified_owner_missing_kick_secret.set(False)
246+
except KeyError:
247+
if notified_owner_missing_kick_secret is False:
248+
asyncio.create_task(self._notify_owner_about_missing_kick_secret())
249+
async with aiohttp.ClientSession() as session:
250+
async with session.post(
251+
"https://id.kick.com/oauth/token",
252+
params={
253+
"client_id": tokens.get("client_id", ""),
254+
"client_secret": tokens.get("client_secret", ""),
255+
"grant_type": "client_credentials",
256+
},
257+
) as req:
258+
try:
259+
data = await req.json()
260+
except aiohttp.ContentTypeError:
261+
data = {}
262+
263+
if req.status == 200:
264+
pass
265+
elif req.status == 401 and data.get("error") == "invalid_client":
266+
log.error("Kick API request failed authentication: set Client ID is invalid.")
267+
elif "error" in data:
268+
log.error(
269+
"Kick OAuth2 API request failed with status code %s and error message: %s",
270+
req.status,
271+
data["error"],
272+
)
273+
else:
274+
log.error("Kick OAuth2 API request failed with status code %s", req.status)
275+
276+
if req.status != 200:
277+
return
278+
279+
self.kick_bearer_cache = data
280+
self.kick_bearer_cache["expires_at"] = datetime.now().timestamp() + data.get("expires_in")
281+
282+
async def maybe_renew_kick_token(self) -> None:
283+
if (
284+
self.kick_bearer_cache
285+
and self.kick_bearer_cache["expires_at"] - datetime.now().timestamp() <= 60
286+
):
287+
await self.get_kick_bearer_token()
204288

205289
@commands.guild_only()
206290
@commands.command()
@@ -242,10 +326,18 @@ async def picarto(self, ctx: commands.Context, channel_name: str):
242326
stream = PicartoStream(_bot=self.bot, name=channel_name)
243327
await self.check_online(ctx, stream)
244328

329+
@commands.guild_only()
330+
@commands.command()
331+
async def kickstream(self, ctx: commands.Context, channel_name: str):
332+
"""Check if a Kick channel is live."""
333+
token = self.kick_bearer_cache.get("access_token")
334+
stream = _streamtypes.KickStream(_bot=self.bot, name=channel_name, token=token)
335+
await self.check_online(ctx, stream)
336+
245337
async def check_online(
246338
self,
247339
ctx: commands.Context,
248-
stream: Union[PicartoStream, YoutubeStream, TwitchStream],
340+
stream: Union[PicartoStream, YoutubeStream, TwitchStream, KickStream],
249341
):
250342
try:
251343
info = await stream.is_online()
@@ -265,6 +357,12 @@ async def check_online(
265357
"The YouTube API key is either invalid or has not been set. See {command}."
266358
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
267359
)
360+
except InvalidKickCredentials:
361+
await ctx.send(
362+
_("The Kick API key is either invalid or has not been set. See {command}.").format(
363+
command=inline(f"{ctx.clean_prefix}streamset kicktoken")
364+
)
365+
)
268366
except YoutubeQuotaExceeded:
269367
await ctx.send(
270368
_(
@@ -363,6 +461,18 @@ async def picarto_alert(
363461
"""Toggle alerts in this channel for a Picarto stream."""
364462
await self.stream_alert(ctx, PicartoStream, channel_name, discord_channel)
365463

464+
@streamalert.command(name="kick")
465+
async def kick_alert(
466+
self,
467+
ctx: commands.Context,
468+
channel_name: str,
469+
discord_channel: Union[
470+
discord.TextChannel, discord.VoiceChannel, discord.StageChannel
471+
] = commands.CurrentChannel,
472+
):
473+
"""Toggle alerts in this channel for a Kick stream."""
474+
await self.stream_alert(ctx, KickStream, channel_name, discord_channel)
475+
366476
@streamalert.command(name="stop", usage="[disable_all=No]")
367477
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
368478
"""Disable all stream alerts in this channel or server.
@@ -435,6 +545,7 @@ async def stream_alert(self, ctx: commands.Context, _class, channel_name, discor
435545
token = await self.bot.get_shared_api_tokens(_class.token_name)
436546
is_yt = _class.__name__ == "YoutubeStream"
437547
is_twitch = _class.__name__ == "TwitchStream"
548+
is_kick = _class.__name__ == "KickStream"
438549
if is_yt and not self.check_name_or_id(channel_name):
439550
stream = _class(_bot=self.bot, id=channel_name, token=token, config=self.config)
440551
elif is_twitch:
@@ -445,6 +556,10 @@ async def stream_alert(self, ctx: commands.Context, _class, channel_name, discor
445556
token=token.get("client_id"),
446557
bearer=self.ttv_bearer_cache.get("access_token", None),
447558
)
559+
elif is_kick:
560+
await self.maybe_renew_kick_token()
561+
token = self.kick_bearer_cache.get("access_token")
562+
stream = _class(_bot=self.bot, name=channel_name, token=token)
448563
else:
449564
if is_yt:
450565
stream = _class(
@@ -464,8 +579,7 @@ async def stream_alert(self, ctx: commands.Context, _class, channel_name, discor
464579
except InvalidYoutubeCredentials:
465580
await ctx.send(
466581
_(
467-
"The YouTube API key is either invalid or has not been set. See "
468-
"{command}."
582+
"The YouTube API key is either invalid or has not been set. See {command}."
469583
).format(command=inline(f"{ctx.clean_prefix}streamset youtubekey"))
470584
)
471585
return
@@ -476,6 +590,13 @@ async def stream_alert(self, ctx: commands.Context, _class, channel_name, discor
476590
" Try again later or contact the owner if this continues."
477591
)
478592
)
593+
except InvalidKickCredentials:
594+
await ctx.send(
595+
_(
596+
"The Kick API key is either invalid or has not been set. See {command}."
597+
).format(command=inline(f"{ctx.clean_prefix}streamset kicktoken"))
598+
)
599+
return
479600
except APIError as e:
480601
log.error(
481602
"Something went wrong whilst trying to contact the stream service's API.\n"
@@ -537,6 +658,30 @@ async def twitchtoken(self, ctx: commands.Context):
537658

538659
await ctx.maybe_send_embed(message)
539660

661+
@streamset.command()
662+
@commands.is_owner()
663+
async def kicktoken(self, ctx: commands.Context):
664+
"""Explain how to set the Kick token."""
665+
message = _(
666+
"To get one, do the following:\n"
667+
"1. Go to this page: {link}.\n"
668+
"2. Click on *Create new*.\n"
669+
"3. Fill the name and description, for *Redirection URL* add *http://localhost*.\n"
670+
"4. Click on *Create Application*.\n"
671+
"5. Copy your client ID and your client secret into:\n"
672+
"{command}"
673+
"\n\n"
674+
"Note: These tokens are sensitive and should only be used in a private channel\n"
675+
"or in DM with the bot.\n"
676+
).format(
677+
link="https://kick.com/settings/developer",
678+
command="`{}set api kick client_id {} client_secret {}`".format(
679+
ctx.clean_prefix, _("<your_client_id_here>"), _("<your_client_secret_here>")
680+
),
681+
)
682+
683+
await ctx.maybe_send_embed(message)
684+
540685
@streamset.command()
541686
@commands.is_owner()
542687
async def youtubekey(self, ctx: commands.Context):
@@ -826,15 +971,18 @@ async def check_streams(self):
826971
for stream in self.streams:
827972
try:
828973
try:
829-
is_rerun = False
830-
is_schedule = False
974+
is_rerun, is_schedule = False, False
831975
if stream.__class__.__name__ == "TwitchStream":
832976
await self.maybe_renew_twitch_bearer_token()
833977
embed, is_rerun = await stream.is_online()
834978

835979
elif stream.__class__.__name__ == "YoutubeStream":
836980
embed, is_schedule = await stream.is_online()
837981

982+
elif stream.__class__.__name__ == "KickStream":
983+
await self.maybe_renew_kick_token()
984+
embed = await stream.is_online()
985+
838986
else:
839987
embed = await stream.is_online()
840988
except StreamNotFound:

0 commit comments

Comments
 (0)