Skip to content

Commit f56b13f

Browse files
committed
Add user emote endpoint
1 parent 417584d commit f56b13f

File tree

5 files changed

+148
-1
lines changed

5 files changed

+148
-1
lines changed

docs/changelog.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
=======
55
- TwitchIO
66
- Additions
7-
- Added :class:`~twitchio.AdSchedule`
7+
- Added :class:`~twitchio.AdSchedule` and :class:`~twitchio.Emote`
88
- Added the new ad-related methods for :class:`~twitchio.PartialUser`:
99
- :func:`~twitchio.PartialUser.fetch_ad_schedule`
1010
- :func:`~twitchio.PartialUser.snooze_ad`
11+
- Added new method :func:`~twitchio.PartialUser.fetch_user_emotes` to :class:`~twitchio.PartialUser`
1112
- Added :func:`~twitchio.PartialUser.fetch_moderated_channels` to :class:`~twitchio.PartialUser`
1213

1314
- Bug fixes

docs/reference.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ CustomRewardRedemption
220220
:members:
221221
:inherited-members:
222222

223+
Emote
224+
------
225+
.. attributetable:: Emote
226+
227+
.. autoclass:: Emote
228+
:members:
229+
223230
Extension
224231
-----------
225232
.. attributetable:: Extension

twitchio/http.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,13 @@ async def get_search_channels(self, query: str, token: str = None, live: bool =
705705
Route("GET", "search/channels", query=[("query", query), ("live_only", str(live))], token=token)
706706
)
707707

708+
async def get_user_emotes(self, user_id: str, broadcaster_id: Optional[str], token: str):
709+
q: List = [("user_id", user_id)]
710+
if broadcaster_id:
711+
q.append(("broadcaster_id", broadcaster_id))
712+
713+
return await self.request(Route("GET", "chat/emotes/user", query=q, token=token))
714+
708715
async def get_stream_key(self, token: str, broadcaster_id: str):
709716
return await self.request(
710717
Route("GET", "streams/key", query=[("broadcaster_id", broadcaster_id)], token=token), paginate=False

twitchio/models.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@
3131
from .user import BitLeaderboardUser, PartialUser, User
3232

3333
if TYPE_CHECKING:
34+
from typing_extensions import Literal
3435
from .http import TwitchHTTP
36+
3537
__all__ = (
3638
"AdSchedule",
3739
"BitsLeaderboard",
3840
"Clip",
3941
"CheerEmote",
4042
"CheerEmoteTier",
43+
"Emote",
4144
"GlobalEmote",
4245
"ChannelEmote",
4346
"HypeTrainContribution",
@@ -651,6 +654,104 @@ def __repr__(self):
651654
)
652655

653656

657+
class Emote:
658+
"""
659+
Represents an Emote.
660+
661+
.. note::
662+
663+
It seems twitch is sometimes returning duplicate information from the emotes endpoint.
664+
To deduplicate your emotes, you can call ``set()`` on the list of emotes (or any other hashmap), which will remove the duplicates.
665+
666+
.. code-block:: python
667+
668+
my_list_of_emotes = await user.get_user_emotes(...)
669+
deduplicated_emotes = set(my_list_of_emotes)
670+
671+
Attributes
672+
-----------
673+
id: :class:`str`
674+
The unique ID of the emote.
675+
set_id: Optional[:class:`str`]
676+
The ID of the set this emote belongs to.
677+
Will be ``None`` if the emote doesn't belong to a set.
678+
owner_id: Optional[:class:`str`]
679+
The ID of the channel this emote belongs to.
680+
name: :class:`str`
681+
The name of this emote, as the user sees it.
682+
type: :class:`str`
683+
The reason this emote is available to the user.
684+
Some available values (twitch hasn't documented this properly, there might be more):
685+
686+
- follower
687+
- subscription
688+
- bitstier
689+
- hypetrain
690+
- globals (global emotes)
691+
692+
scales: list[:class:`str`]
693+
The available scaling for this emote. These are typically floats (ex. "1.0", "2.0").
694+
format_static: :class:`bool`
695+
Whether this emote is available as a static (PNG) file.
696+
format_animated: :class:`bool`
697+
Whether this emote is available as an animated (GIF) file.
698+
theme_light: :class:`bool`
699+
Whether this emote is available in light theme background mode.
700+
theme_dark: :class:`bool`
701+
Whether this emote is available in dark theme background mode.
702+
"""
703+
704+
__slots__ = "id", "set_id", "owner_id", "name", "type", "scales", "format_static", "format_animated", "theme_light", "theme_dark"
705+
706+
def __init__(self, data: dict) -> None:
707+
self.id: str = data["id"]
708+
self.set_id: Optional[str] = data["emote_set_id"] and None
709+
self.owner_id: Optional[str] = data["owner_id"] and None
710+
self.name: str = data["name"]
711+
self.type: str = data["emote_type"]
712+
self.scales: List[str] = data["scale"]
713+
self.theme_dark: bool = "dark" in data["theme_mode"]
714+
self.theme_light: bool = "light" in data["theme_mode"]
715+
self.format_static: bool = "static" in data["format"]
716+
self.format_animated: bool = "animated" in data["format"]
717+
718+
def url_for(self, format: Literal["static", "animated"], theme: Literal["dark", "light"], scale: str) -> str:
719+
"""
720+
Returns a cdn url that can be used to download or serve the emote on a website.
721+
This function validates that the arguments passed are possible values to serve the emote.
722+
723+
Parameters
724+
-----------
725+
format: Literal["static", "animated"]
726+
The format of the emote. You can check what formats are available using :attr:`~.format_static` and :attr:`~.format_animated`.
727+
theme: Literal["dark", "light"]
728+
The theme of the emote. You can check what themes are available using :attr:`~.format_dark` and :attr:`~.format_light`.
729+
scale: :class:`str`
730+
The scale of the emote. This should be formatted in this format: ``"1.0"``.
731+
The scales available for this emote can be checked via :attr:`~.scales`.
732+
733+
Returns
734+
--------
735+
:class:`str`
736+
"""
737+
if scale not in self.scales:
738+
raise ValueError(f"scale for this emote must be one of {', '.join(self.scales)}, not {scale}")
739+
740+
if (theme == "dark" and not self.theme_dark) or (theme == "light" and not self.theme_light):
741+
raise ValueError(f"theme {theme} is not an available value for this emote")
742+
743+
if (format == "static" and not self.format_static) or (format == "animated" and not self.format_animated):
744+
raise ValueError(f"format {format} is not an available value for this emote")
745+
746+
return f"https://static-cdn.jtvnw.net/emoticons/v2/{self.id}/{format}/{theme}/{scale}"
747+
748+
def __repr__(self) -> str:
749+
return f"<Emote id={self.id} name={self.name}>"
750+
751+
def __hash__(self) -> int: # this exists so we can do set(list of emotes) to get rid of duplicates
752+
return hash(self.id)
753+
754+
654755
class Marker:
655756
"""
656757
Represents a stream Marker

twitchio/user.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
AdSchedule,
4141
BitsLeaderboard,
4242
Clip,
43+
Emote,
4344
ExtensionBuilder,
4445
Tag,
4546
FollowEvent,
@@ -638,6 +639,32 @@ async def fetch_channel_emotes(self):
638639

639640
data = await self._http.get_channel_emotes(str(self.id))
640641
return [ChannelEmote(self._http, x) for x in data]
642+
643+
async def fetch_user_emotes(self, token: str, broadcaster: Optional[PartialUser] = None) -> List[Emote]:
644+
"""|coro|
645+
646+
Fetches emotes the user has access to. Optionally, you can filter by a broadcaster.
647+
648+
.. note::
649+
650+
As of writing, this endpoint seems extrememly unoptimized by twitch, and may (read: will) take a lot of API requests to load.
651+
See https://github.com/twitchdev/issues/issues/921 .
652+
653+
Parameters
654+
-----------
655+
token: :class:`str`
656+
An OAuth token belonging to this user with the ``user:read:emotes`` scope.
657+
broadcaster: Optional[:class:`~twitchio.PartialUser`]
658+
A channel to filter the results with.
659+
Filtering will return all emotes available to the user on that channel, including global emotes.
660+
661+
Returns
662+
--------
663+
List[:class:`~twitchio.Emote`]
664+
"""
665+
from .models import Emote
666+
data = await self._http.get_user_emotes(str(self.id), broadcaster and str(broadcaster.id), token)
667+
return [Emote(d) for d in data]
641668

642669
async def follow(self, userid: int, token: str, *, notifications=False):
643670
"""|coro|
@@ -666,6 +693,10 @@ async def unfollow(self, userid: int, token: str):
666693
667694
Unfollows the user
668695
696+
.. warning::
697+
698+
This method is obsolete as Twitch removed the endpoint.
699+
669700
Parameters
670701
-----------
671702
userid: :class:`int`

0 commit comments

Comments
 (0)