Path: blob/master/ invest-robot-contest_TinkoffBotTwitch-main/venv/lib/python3.8/site-packages/twitchio/client.py
7796 views
"""1The MIT License (MIT)23Copyright (c) 2017-2021 TwitchIO45Permission is hereby granted, free of charge, to any person obtaining a6copy of this software and associated documentation files (the "Software"),7to deal in the Software without restriction, including without limitation8the rights to use, copy, modify, merge, publish, distribute, sublicense,9and/or sell copies of the Software, and to permit persons to whom the10Software is furnished to do so, subject to the following conditions:1112The above copyright notice and this permission notice shall be included in13all copies or substantial portions of the Software.1415THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS16OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING20FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER21DEALINGS IN THE SOFTWARE.22"""2324import asyncio25import inspect26import warnings27import logging28import traceback29import sys30from typing import Union, Callable, List, Optional, Tuple, Any, Coroutine3132from twitchio.errors import HTTPException33from . import models34from .websocket import WSConnection35from .http import TwitchHTTP36from .channel import Channel37from .message import Message38from .user import User, PartialUser, SearchUser39from .cache import user_cache, id_cache4041__all__ = ("Client",)4243logger = logging.getLogger("twitchio.client")444546class Client:47"""TwitchIO Client object that is used to interact with the Twitch API and connect to Twitch IRC over websocket.4849Parameters50------------51token: :class:`str`52An OAuth Access Token to login with on IRC and interact with the API.53client_secret: Optional[:class:`str`]54An optional application Client Secret used to generate Access Tokens automatically.55initial_channels: Optional[Union[:class:`list`, :class:`tuple`, Callable]]56An optional list, tuple or callable which contains channel names to connect to on startup.57If this is a callable, it must return a list or tuple.58loop: Optional[:class:`asyncio.AbstractEventLoop`]59The event loop the client will use to run.60heartbeat: Optional[float]61An optional float in seconds to send a PING message to the server. Defaults to 30.0.6263Attributes64------------65loop: :class:`asyncio.AbstractEventLoop`66The event loop the Client uses.67"""6869def __init__(70self,71token: str,72*,73client_secret: str = None,74initial_channels: Union[list, tuple, Callable] = None,75loop: asyncio.AbstractEventLoop = None,76heartbeat: Optional[float] = 30.0,77):7879self.loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop()80self._heartbeat = heartbeat8182token = token.replace("oauth:", "")8384self._http = TwitchHTTP(self, api_token=token, client_secret=client_secret)85self._connection = WSConnection(86client=self,87token=token,88loop=self.loop,89initial_channels=initial_channels,90heartbeat=heartbeat,91)9293self._events = {}94self._waiting: List[Tuple[str, Callable[[...], bool], asyncio.Future]] = []9596@classmethod97def from_client_credentials(98cls,99client_id: str,100client_secret: str,101*,102loop: asyncio.AbstractEventLoop = None,103heartbeat: Optional[float] = 30.0,104) -> "Client":105"""106creates a client application token from your client credentials.107108.. warning:109110this is not suitable for logging in to IRC.111112.. note:113114This classmethod skips :meth:`~.__init__`115116Parameters117------------118client_id: :class:`str`119120client_secret: :class:`str`121An application Client Secret used to generate Access Tokens automatically.122loop: Optional[:class:`asyncio.AbstractEventLoop`]123The event loop the client will use to run.124125Returns126--------127A new :class:`Client` instance128"""129self = cls.__new__(cls)130self.loop = loop or asyncio.get_event_loop()131self._http = TwitchHTTP(self, client_id=client_id, client_secret=client_secret)132self._heartbeat = heartbeat133self._connection = WSConnection(134client=self,135loop=self.loop,136initial_channels=None,137heartbeat=self._heartbeat,138) # The only reason we're even creating this is to avoid attribute errors139self._events = {}140self._waiting = []141return self142143def run(self):144"""145A blocking function that starts the asyncio event loop,146connects to the twitch IRC server, and cleans up when done.147"""148try:149self.loop.create_task(self.connect())150self.loop.run_forever()151except KeyboardInterrupt:152pass153finally:154self.loop.run_until_complete(self.close())155self.loop.close()156157async def start(self):158"""|coro|159160Connects to the twitch IRC server, and cleanly disconnects when done.161"""162if self.loop is not asyncio.get_running_loop():163raise RuntimeError(164f"Attempted to start a {self.__class__.__name__} instance on a different loop "165f"than the one it was initialized with."166)167try:168await self.connect()169finally:170await self.close()171172async def connect(self):173"""|coro|174175Connects to the twitch IRC server176"""177await self._connection._connect()178179async def close(self):180"""|coro|181182Cleanly disconnects from the twitch IRC server183"""184await self._connection._close()185186def run_event(self, event_name, *args):187name = f"event_{event_name}"188logger.debug(f"dispatching event {event_name}")189190async def wrapped(func):191try:192await func(*args)193except Exception as e:194if name == "event_error":195# don't enter a dispatch loop!196raise197198self.run_event("error", e)199200inner_cb = getattr(self, name, None)201if inner_cb is not None:202if inspect.iscoroutinefunction(inner_cb):203self.loop.create_task(wrapped(inner_cb))204else:205warnings.warn(206f"event '{name}' callback is not a coroutine",207category=RuntimeWarning,208)209210if name in self._events:211for event in self._events[name]:212self.loop.create_task(wrapped(event))213214for e, check, future in self._waiting:215if e == event_name:216if check(*args):217future.set_result(args)218if future.done():219self._waiting.remove((e, check, future))220221def add_event(self, callback: Callable, name: str = None) -> None:222try:223func = callback.func224except AttributeError:225func = callback226227if not inspect.iscoroutine(func) and not inspect.iscoroutinefunction(func):228raise ValueError("Event callback must be a coroutine")229230event_name = name or callback.__name__231callback._event = event_name # used to remove the event232233if event_name in self._events:234self._events[event_name].append(callback)235236else:237self._events[event_name] = [callback]238239def remove_event(self, callback: Callable) -> bool:240if not hasattr(callback, "_event"):241raise ValueError("Event callback is not a registered event")242243if callback in self._events[callback._event]:244self._events[callback._event].remove(callback)245return True246247return False248249def event(self, name: str = None) -> Callable:250def decorator(func: Callable) -> Callable:251self.add_event(func, name)252return func253254return decorator255256async def wait_for(257self,258event: str,259predicate: Callable[[], bool] = lambda *a: True,260*,261timeout=60.0,262) -> Tuple[Any]:263"""|coro|264265266Waits for an event to be dispatched, then returns the events data267268Parameters269-----------270event: :class:`str`271The event to wait for. Do not include the `event_` prefix272predicate: Callable[[...], bool]273A check that is fired when the desired event is dispatched. if the check returns false,274the waiting will continue until the timeout.275timeout: :class:`int`276How long to wait before timing out and raising an error.277278Returns279--------280The arguments passed to the event.281"""282fut = self.loop.create_future()283tup = (event, predicate, fut)284self._waiting.append(tup)285values = await asyncio.wait_for(fut, timeout)286return values287288def wait_for_ready(self) -> Coroutine[Any, Any, bool]:289"""|coro|290291Waits for the underlying connection to finish startup292293Returns294--------295:class:`bool` The state of the underlying flag. This will always be ``True``296"""297return self._connection.is_ready.wait()298299@id_cache()300def get_channel(self, name: str) -> Optional[Channel]:301"""Retrieve a channel from the cache.302303Parameters304-----------305name: str306The channel name to retrieve from cache. Returns None if no channel was found.307308Returns309--------310:class:`.Channel`311"""312name = name.lower()313314if name in self._connection._cache:315# Basically the cache doesn't store channels naturally, instead it stores a channel key316# With the associated users as a set.317# We create a Channel here and return it only if the cache has that channel key.318319return Channel(name=name, websocket=self._connection)320321async def join_channels(self, channels: Union[List[str], Tuple[str]]):322"""|coro|323324325Join the specified channels.326327Parameters328------------329channels: Union[List[str], Tuple[str]]330The channels in either a list or tuple form to join.331"""332await self._connection.join_channels(*channels)333334@property335def connected_channels(self) -> List[Channel]:336"""A list of currently connected :class:`.Channel`"""337return [self.get_channel(x) for x in self._connection._cache.keys()]338339@property340def events(self):341"""A mapping of events name to coroutine."""342return self._events343344@property345def nick(self):346"""The IRC bots nick."""347return self._http.nick or self._connection.nick348349@property350def user_id(self):351"""The IRC bot user id."""352return self._http.user_id or self._connection.user_id353354def create_user(self, user_id: int, user_name: str) -> PartialUser:355"""356A helper method to create a :class:`twitchio.PartialUser` from a user id and user name.357358Parameters359-----------360user_id: :class:`int`361The id of the user362user_name: :class:`str`363The name of the user364365Returns366--------367:class:`twitchio.PartialUser`368"""369return PartialUser(self._http, user_id, user_name)370371@user_cache()372async def fetch_users(373self,374names: List[str] = None,375ids: List[int] = None,376token: str = None,377force=False,378) -> List[User]:379"""|coro|380381Fetches users from the helix API382383Parameters384-----------385names: Optional[List[:class:`str`]]386usernames of people to fetch387ids: Optional[List[:class:`str`]]388ids of people to fetch389token: Optional[:class:`str`]390An optional OAuth token to use instead of the bot OAuth token391force: :class:`bool`392whether to force a fetch from the api, or check the cache first. Defaults to False393394Returns395--------396List[:class:`twitchio.User`]397"""398# the forced argument doesnt actually get used here, it gets used by the cache wrapper.399# But we'll include it in the args here so that sphinx catches it400assert names or ids401data = await self._http.get_users(ids, names, token=token)402return [User(self._http, x) for x in data]403404async def fetch_clips(self, ids: List[str]):405"""|coro|406407Fetches clips by clip id.408To fetch clips by user id, use :meth:`twitchio.PartialUser.fetch_clips`409410Parameters411-----------412ids: List[:class:`str`]413A list of clip ids414415Returns416--------417List[:class:`twitchio.Clip`]418"""419data = await self._http.get_clips(ids=ids)420return [models.Clip(self._http, d) for d in data]421422async def fetch_channel(self, broadcaster: str):423"""Retrieve channel information from the API.424425Parameters426-----------427broadcaster: str428The channel name or ID to request from API. Returns empty dict if no channel was found.429430Returns431--------432:class:`twitchio.ChannelInfo`433"""434435if not broadcaster.isdigit():436get_id = await self.fetch_users(names=[broadcaster.lower()])437if not get_id:438raise IndexError("Invalid channel name.")439broadcaster = get_id[0].id440try:441data = await self._http.get_channels(broadcaster)442443from .models import ChannelInfo444445return ChannelInfo(self._http, data=data[0])446447except HTTPException:448raise HTTPException("Incorrect channel ID.")449450async def fetch_videos(451self,452ids: List[int] = None,453game_id: int = None,454user_id: int = None,455period=None,456sort=None,457type=None,458language=None,459):460"""|coro|461462Fetches videos by id, game id, or user id463464Parameters465-----------466ids: Optional[List[:class:`int`]]467A list of video ids468game_id: Optional[:class:`int`]469A game to fetch videos from470user_id: Optional[:class:`int`]471A user to fetch videos from. See :meth:`twitchio.PartialUser.fetch_videos`472period: Optional[:class:`str`]473The period for which to fetch videos. Valid values are `all`, `day`, `week`, `month`. Defaults to `all`.474Cannot be used when video id(s) are passed475sort: :class:`str`476Sort orders of the videos. Valid values are `time`, `trending`, `views`, Defaults to `time`.477Cannot be used when video id(s) are passed478type: Optional[:class:`str`]479Type of the videos to fetch. Valid values are `upload`, `archive`, `highlight`. Defaults to `all`.480Cannot be used when video id(s) are passed481language: Optional[:class:`str`]482Language of the videos to fetch. Must be an `ISO-639-1 <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>`_ two letter code.483Cannot be used when video id(s) are passed484485Returns486--------487List[:class:`twitchio.Video`]488"""489from .models import Video490491data = await self._http.get_videos(492ids,493user_id=user_id,494game_id=game_id,495period=period,496sort=sort,497type=type,498language=language,499)500return [Video(self._http, x) for x in data]501502async def fetch_cheermotes(self, user_id: int = None):503"""|coro|504505506Fetches cheermotes from the twitch API507508Parameters509-----------510user_id: Optional[:class:`int`]511The channel id to fetch from.512513Returns514--------515List[:class:`twitchio.CheerEmote`]516"""517data = await self._http.get_cheermotes(str(user_id) if user_id else None)518return [models.CheerEmote(self._http, x) for x in data]519520async def fetch_top_games(self) -> List[models.Game]:521"""|coro|522523Fetches the top games from the api524525Returns526--------527List[:class:`twitchio.Game`]528"""529data = await self._http.get_top_games()530return [models.Game(d) for d in data]531532async def fetch_games(self, ids: List[int] = None, names: List[str] = None) -> List[models.Game]:533"""|coro|534535Fetches games by id or name.536At least one id or name must be provided537538Parameters539-----------540ids: Optional[List[:class:`int`]]541An optional list of game ids542names: Optional[List[:class:`str`]]543An optional list of game names544545Returns546--------547List[:class:`twitchio.Game`]548"""549data = await self._http.get_games(ids, names)550return [models.Game(d) for d in data]551552async def fetch_tags(self, ids: List[str] = None):553"""|coro|554555Fetches stream tags.556557Parameters558-----------559ids: Optional[List[:class:`str`]]560The ids of the tags to fetch561562Returns563--------564List[:class:`twitchio.Tag`]565"""566data = await self._http.get_stream_tags(ids)567return [models.Tag(x) for x in data]568569async def fetch_streams(570self,571user_ids: List[int] = None,572game_ids: List[int] = None,573user_logins: List[str] = None,574languages: List[str] = None,575token: str = None,576):577"""|coro|578579Fetches live streams from the helix API580581Parameters582-----------583user_ids: Optional[List[:class:`int`]]584user ids of people whose streams to fetch585game_ids: Optional[List[:class:`int`]]586game ids of streams to fetch587user_logins: Optional[List[:class:`str`]]588user login names of people whose streams to fetch589languages: Optional[List[:class:`str`]]590language for the stream(s). ISO 639-1 or two letter code for supported stream language591token: Optional[:class:`str`]592An optional OAuth token to use instead of the bot OAuth token593594Returns595--------596List[:class:`twitchio.Stream`]597"""598from .models import Stream599600assert user_ids or game_ids or user_logins601data = await self._http.get_streams(602game_ids=game_ids,603user_ids=user_ids,604user_logins=user_logins,605languages=languages,606token=token,607)608return [Stream(self._http, x) for x in data]609610async def fetch_teams(611self,612team_name: str = None,613team_id: str = None,614):615"""|coro|616617Fetches information for a specific Twitch Team.618619Parameters620-----------621name: Optional[:class:`str`]622Team name to fetch623id: Optional[:class:`str`]624Team id to fetch625626Returns627--------628List[:class:`twitchio.Team`]629"""630from .models import Team631632assert team_name or team_id633data = await self._http.get_teams(634team_name=team_name,635team_id=team_id,636)637638return Team(self._http, data[0])639640async def search_categories(self, query: str):641"""|coro|642643Searches twitches categories644645Parameters646-----------647query: :class:`str`648The query to search for649650Returns651--------652List[:class:`twitchio.Game`]653"""654data = await self._http.get_search_categories(query)655return [models.Game(x) for x in data]656657async def search_channels(self, query: str, *, live_only=False):658"""|coro|659660Searches channels for the given query661662Parameters663-----------664query: :class:`str`665The query to search for666live_only: :class:`bool`667Only search live channels. Defaults to False668669Returns670--------671List[:class:`twitchio.SearchUser`]672"""673data = await self._http.get_search_channels(query, live=live_only)674return [SearchUser(self._http, x) for x in data]675676async def delete_videos(self, token: str, ids: List[int]) -> List[int]:677"""|coro|678679Delete videos from the api. Returns the video ids that were successfully deleted.680681Parameters682-----------683token: :class:`str`684An oauth token with the channel:manage:videos scope685ids: List[:class:`int`]686A list of video ids from the channel of the oauth token to delete687688Returns689--------690List[:class:`int`]691"""692resp = []693for chunk in [ids[x : x + 3] for x in range(0, len(ids), 3)]:694resp.append(await self._http.delete_videos(token, chunk))695696return resp697698async def get_webhook_subscriptions(self):699"""|coro|700701Fetches your current webhook subscriptions. Requires your bot to be logged in with an app access token.702703Returns704--------705List[:class:`twitchio.WebhookSubscription`]706"""707data = await self._http.get_webhook_subs()708return [models.WebhookSubscription(x) for x in data]709710async def event_token_expired(self):711"""|coro|712713714A special event called when the oauth token expires. This is a hook into the http system, it will call this715when a call to the api fails due to a token expiry. This function should return either a new token, or `None`.716Returning `None` will cause the client to attempt an automatic token generation.717718.. note::719This event is a callback hook. It is not a dispatched event. Any errors raised will be passed to the720:func:`~event_error` event.721"""722return None723724async def event_mode(self, channel: Channel, user: User, status: str):725"""|coro|726727728Event called when a MODE is received from Twitch.729730Parameters731------------732channel: :class:`.Channel`733Channel object relevant to the MODE event.734user: :class:`.User`735User object containing relevant information to the MODE.736status: str737The JTV status received by Twitch. Could be either o+ or o-.738Indicates a moderation promotion/demotion to the :class:`.User`739"""740pass741742async def event_userstate(self, user: User):743"""|coro|744745746Event called when a USERSTATE is received from Twitch.747748Parameters749------------750user: :class:`.User`751User object containing relevant information to the USERSTATE.752"""753pass754755async def event_raw_usernotice(self, channel: Channel, tags: dict):756"""|coro|757758759Event called when a USERNOTICE is received from Twitch.760Since USERNOTICE's can be fairly complex and vary, the following sub-events are available:761762:meth:`event_usernotice_subscription` :763Called when a USERNOTICE Subscription or Re-subscription event is received.764765.. tip::766767For more information on how to handle USERNOTICE's visit:768https://dev.twitch.tv/docs/irc/tags/#usernotice-twitch-tags769770771Parameters772------------773channel: :class:`.Channel`774Channel object relevant to the USERNOTICE event.775tags : dict776A dictionary with the relevant information associated with the USERNOTICE.777This could vary depending on the event.778"""779pass780781async def event_usernotice_subscription(self, metadata):782"""|coro|783784785Event called when a USERNOTICE subscription or re-subscription event is received from Twitch.786787Parameters788------------789metadata: :class:`NoticeSubscription`790The object containing various metadata about the subscription event.791For ease of use, this contains a :class:`User` and :class:`Channel`.792793"""794pass795796async def event_part(self, user: User):797"""|coro|798799800Event called when a PART is received from Twitch.801802Parameters803------------804user: :class:`.User`805User object containing relevant information to the PART.806"""807pass808809async def event_join(self, channel: Channel, user: User):810"""|coro|811812813Event called when a JOIN is received from Twitch.814815Parameters816------------817channel: :class:`.Channel`818The channel associated with the JOIN.819user: :class:`.User`820User object containing relevant information to the JOIN.821"""822pass823824async def event_message(self, message: Message):825"""|coro|826827828Event called when a PRIVMSG is received from Twitch.829830Parameters831------------832message: :class:`.Message`833Message object containing relevant information.834"""835pass836837async def event_error(self, error: Exception, data: str = None):838"""|coro|839840841Event called when an error occurs while processing data.842843Parameters844------------845error: Exception846The exception raised.847data: str848The raw data received from Twitch. Depending on how this is called, this could be None.849850Example851---------852.. code:: py853854@bot.event()855async def event_error(error, data):856traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)857"""858traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)859860async def event_ready(self):861"""|coro|862863864Event called when the Bot has logged in and is ready.865866Example867---------868.. code:: py869870@bot.event()871async def event_ready():872print(f'Logged into Twitch | {bot.nick}')873"""874pass875876async def event_raw_data(self, data: str):877"""|coro|878879880Event called with the raw data received by Twitch.881882Parameters883------------884data: str885The raw data received from Twitch.886887Example888---------889.. code:: py890891@bot.event()892async def event_raw_data(data):893print(data)894"""895pass896897898