Skip to content
This repository has been archived by the owner on Jun 29, 2020. It is now read-only.

[WIP] Rest API #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions examples/basic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,35 @@

mc = MixerClient(scopes=s)

@mc.event("login_success")
async def login_success():
@mc.event("ready")
async def client_ready():
print('--------------------------')
print('Logged In As:')
print('username:', mc.current_user['username'])
print('id:', mc.current_user['id'])
print('username:', mc.me.name)
print('id:', mc.me.id)
print('channel id:', mc.me.channel.id)
print('---------------------------')

@mc.event("user_join")
async def user_join(user):
print('User Joined:')
print('user name:', user.data['username'])
print('user id:', user.data['id'])
print('user name:', user.name)
print('user id:', user.id)
print('---------------------------')

@mc.event("user_left")
async def user_left(user):
print('User Left:')
print('user name:', user.data['username'])
print('user id:', user.data['id'])
print('user name:', user.name)
print('user id:', user.id)
print('---------------------------')

@mc.event("message")
async def message_handler(message):
async def message_handler(message: message.ChatMessage):
print('New Message:')
print('content:', message.data['message']['message'][0]['text'])
print('author name:', message.data['user_name'])
print('author id:', message.data['user_id'])
print('channel id:', message.channel.id)
print('content:', message.content)
print('author id:', message.author.id)
print('---------------------------')

mc.run(secret='', id='')
12 changes: 10 additions & 2 deletions pyxer/cache.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Dict, Any

from .utils import get
from .mappings import Channel, ChatMessage


class MixerCache:
users: Dict[int, Any]
messages: Dict[int, Any]
users: Dict[int, Any] = {} # TODO: have one base user object
messages: Dict[str, ChatMessage] = {}
channels: Dict[int, Channel] = {}

def __init__(self, ws):
self._ws = ws
Expand All @@ -16,3 +18,9 @@ def update_user(self, data: Dict[str, Any]):

def update_message(self, data: Dict[str, Any]):
...

def get_channel(self, channel_id: int):
return self.channels.get(channel_id, None)

def set_channel(self, channel: Channel):
self.channels[channel.id] = channel
27 changes: 24 additions & 3 deletions pyxer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

from .http import HTTPClient
from .ws import WebSocketClient
from .cache import MixerCache
from .scopes import Scopes
from .oauth import OAuthHandler
from .utils import as_go_link
from .event import Listener
from .mappings import Channel, ChatMessage, User


class MixerClient:
Expand All @@ -16,6 +18,7 @@ class MixerClient:
oauth: OAuthHandler
current_user: Dict[str, Any]
ws: WebSocketClient
cache: MixerCache

def __init__(self, *, scopes: Scopes):
self.scopes = scopes
Expand Down Expand Up @@ -51,14 +54,16 @@ async def login(self, *, secret: str, id: str, open_browser: bool=True):

data = await self.http.get_current_user()

self.current_user = data
self.me = User(client=self, data=data)

async def connect(self):
channel_id = self.current_user['channel']['id']
user_id = self.current_user['channel']['userId']
channel_id = self.me.channel.id
user_id = self.me.channel.user_id

ws = WebSocketClient.start(self, channel_id=channel_id, user_id=user_id)
self.ws = await asyncio.wait_for(ws, timeout=50.0)
self.cache = self.ws.cache

while True:
await self.ws.handle_message()

Expand Down Expand Up @@ -100,3 +105,19 @@ def wrapper(coro):
self.listeners.append(ret)
return ret
return wrapper

async def find_channel(self, channel_id: str):
'''
Finds a channel. This will first check the cache if it is available,
then perform a request if not found.
'''
cached = self.cache.get_channel(channel_id)
if cached:
return cached

data = await self.http.get_channel(channel_id)

channel = Channel(client=self, data=data)
self.cache.set_channel(channel)

return channel
57 changes: 54 additions & 3 deletions pyxer/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, Optional, List
from yarl import URL
from aiohttp import ClientSession
from .mappings.channel import Channel, ExpandedChannel


class HTTPConfig:
Expand Down Expand Up @@ -29,10 +30,24 @@ async def request(self, verb: str, endpoint: str, *, query: Optional[Dict[str, s
headers['Authorization'] = f'Bearer {self.config.access_token}'

async with self.session.request(verb, url, json=data, headers=headers) as r:
#data = await r.json() if r.headers['content-type'] == 'application/json' else await r.text(encoding='utf-8')
data = await r.json() if 'application/json' in r.headers.get('content-type', []) else await r.text(encoding='utf-8')

if r.status != 204:
return await r.json()
if 300 > r.status >= 200:
return data

if r.status == 429:
retry_after = data['retry_after'] / 1000.0

print(f"You are being ratelimited! Retrying request after {retry_after} seconds.")
await asyncio.sleep(retry_after)
await self.request(verb, endpoint, query=query, data=data, headers=headers, clazz=clazz)

return

if r.status == 403:
raise Exception('Forbidden.')
elif r.status == 404:
raise Exception('Not Found.')

def get_shortcode(self, scope: List[str]):
scopes = " ".join(scope)
Expand Down Expand Up @@ -67,5 +82,41 @@ def refresh_tokens(self, token: str):
def get_current_user(self):
return self.request('GET', 'users/current')

def get_channel_id(self, username: str):
'''
Gets a single user's channel id

Args:
username (str): The username of the channel you want to get

Returns:
json: The id of the requested user
'''
return self.request('GET', f'/channels/{username}?fields=id')

def get_connection_info(self, channel_id: int):
'''
Gets the info of the supplied channel

Args:
channel_id (str): The channel whos information you want to get

Returns:
json: The channels chatroom settings if authenticated
'''
return self.request('GET', f'chats/{channel_id}')

def get_channel(self, channel_id: str):
'''
Gets a single channel's info

Args:
channel_id (str): The channel ID

Returns:
json: The extended information of the requested channel
'''
return self.request('GET', f'channels/{channel_id}')

def get_ingests(self):
return self.request('GET', 'ingests')
3 changes: 3 additions & 0 deletions pyxer/mappings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .channel import Channel, ChannelAdvanced, ExpandedChannel
from .message import ChatMessage
from .user import Author, User, PartialUser
41 changes: 41 additions & 0 deletions pyxer/mappings/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from datetime import datetime


class Object:
def __init__(self, *, client):
self.client = client


class Timestamped(Object):
created_at: datetime
updated_at: datetime
deleted_at: datetime

def __init__(self, *, client, data):
super().__init__(client=client)

created_at = data.pop('createdAt', None)
if created_at:
self.created_at = datetime.fromisoformat(created_at[:-1])
# datetime does not support ISO 8601,
# but all datetimes should be received as UTC,
# so removing "Z" (for UTC) is enough for it to
# be parsed properly.

updated_at = data.pop('updatedAt', None)
if updated_at:
self.updated_at = datetime.fromisoformat(updated_at[:-1])

deleted_at = data.pop('deletedAt', None)
if deleted_at:
self.deleted_at = datetime.fromisoformat(deleted_at[:-1])


class Resource:
id: int
type: str
relid: int
url: str
store: str
remotePath: str
meta: str = None
75 changes: 75 additions & 0 deletions pyxer/mappings/channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from dataclasses import dataclass
from .base import Timestamped, Resource


class Channel(Timestamped):
id: int
userId: int
token: str
online: bool
featured: bool
featureLevel: int
partnered: bool
suspended: bool
name: str
audience: str
viewersTotal: int
viewersCurrent: int
numFollowers: int
description: str
interactive: bool
ftl: int
hasVod: bool
badgeId: int
bannerUrl: str
hosteeId: int
hasTranscodes: bool
vodsEnabled: bool

interactiveGameId: int = None
languageId: str = None
coverId: int = None
thumbnailId: int = None
typeId: int = None
transcodingProfileId: int = None
costreamId: str = None

def __init__(self, *, client, data):
super().__init__(client=client, data=data)
self._parse_data(data)

def _parse_data(self, data):
self.id = data['id']
self.user_id = data['userId'] # TODO: fetch and serialise
self.token = data.get('token')
self.online = data['online']
self.featured = data['featured']
self.feature_level = data['featureLevel']
self.partnered = data['partnered']
self.suspended = data['suspended']
self.name = data['name']
self.audience = data['audience']
self.total_viewers = data['viewersTotal']
self.current_viewers = data['viewersCurrent']
self.followers = data['numFollowers']
self.description = data['description']
self.ftl = data['ftl']
self.has_vod = data['hasVod']
self.badge_id = data['badgeId']
self.banner_url = data['bannerUrl']
self.hosteeId = data['hosteeId']
self.hasTranscodes = data['hasTranscodes']
self.vodsEnabled = data['vodsEnabled']

# TODO: complete these classes

class ChannelAdvanced(Channel):
type: dict = None
user: dict = None


class ExpandedChannel(ChannelAdvanced):
preferences: dict = None
thumbnail: Resource = None
cover: Resource = None
badge: Resource = None
26 changes: 26 additions & 0 deletions pyxer/mappings/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List, Dict
from .base import Timestamped, Resource
from .channel import Channel
from .user import Author


class MessageContent:
def __init__(self, message_list):
self._message_list = message_list

def __str__(self):
return ''.join(m['text'] for m in self._message_list)

class ChatMessage(Timestamped):
def __init__(self, *, client, data):
super().__init__(client=client, data=data)

@classmethod
async def _received(cls, *, client, data):
inst = cls(client=client, data=data)
inst.channel = await client.find_channel(data.pop('channel'))
inst.id = data.pop('id')
inst.content = MessageContent(data.pop('message')['message'])
inst.author = Author(client=client, data=data)

return inst
Loading