1+ from operator import is_
12import discord
23from redbot .core .utils .chat_formatting import humanize_list
34from redbot .core .bot import Red
78from redbot .core .utils .chat_formatting import escape , inline , pagify
89
910from .streamtypes import (
11+ KickStream ,
1012 PicartoStream ,
1113 Stream ,
1214 TwitchStream ,
1315 YoutubeStream ,
1416)
1517from .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