diff --git a/mojang/__init__.py b/mojang/__init__.py new file mode 100644 index 00000000..1a03206f --- /dev/null +++ b/mojang/__init__.py @@ -0,0 +1,3 @@ +from .auth.session import user +from .user.profile import profile +from .user.api import * \ No newline at end of file diff --git a/pymojang/__init__.py b/mojang/auth/__init__.py similarity index 100% rename from pymojang/__init__.py rename to mojang/auth/__init__.py diff --git a/mojang/auth/security.py b/mojang/auth/security.py new file mode 100644 index 00000000..f3f4ae32 --- /dev/null +++ b/mojang/auth/security.py @@ -0,0 +1,16 @@ +from ..urls import MOJANG_API + +def is_secure(session): + url = MOJANG_API.join('user/security/location') + response = session._request('get', url) + return response.status_code == 204 + +def get_challenges(session): + url = MOJANG_API.join('user/security/challenges') + response = session._request('get', url) + return response.json() + +def verify_ip(session, answers: list): + url = MOJANG_API.join('user/security/location') + response = session._request('post', url, json=answers) + return response.status_code == 204 diff --git a/mojang/auth/session.py b/mojang/auth/session.py new file mode 100644 index 00000000..fd3e278b --- /dev/null +++ b/mojang/auth/session.py @@ -0,0 +1,65 @@ +import requests +from . import yggdrasil +from . import security +from ..user.profile import UserProfile + + +class UserSession: + + def __init__(self, username: str, password: str): + self.__session = requests.Session() + self.__session.headers.update({'Content-Type': 'application/json'}) + + self.__access_token = None + self.__client_token = None + + self.__username = username + self.__password = password + + self.__profile = None + + def _request(self, method: str, url: str, **kwargs): + _fct = getattr(self.__session, method) + return _fct(url, **kwargs) + + def _update_token(self, **kwargs): + self.__access_token = kwargs.get('access_token', self.__access_token) + self.__client_token = kwargs.get('client_token', self.__client_token) + if self.__access_token is None: + self.__session.headers.pop('Authorization') + else: + self.__session.headers.update({'Authorization': f'Bearer {self.__access_token}'}) + + # Connection / Disconnection + def connect(self): + if self.__password is not None: + yggdrasil.authenticate(self, self.__username, self.__password) + if self.secure: + self.__profile = UserProfile(self.__session, authenticated=self.secure, load=True) + + def close(self): + yggdrasil.invalidate(self, self.__access_token, self.__client_token) + + def close_all(self): + yggdrasil.signout(self, self.__username, self.__password) + + # Security + @property + def secure(self): + return security.is_secure(self) + + @property + def challenges(self): + return security.get_challenges(self) + + def verify(self, answers: list): + return security.verify_ip(self, answers) + + # Other + @property + def profile(self): + return self.__profile + + +def user(username: str, password: str): + return UserSession(username, password) diff --git a/mojang/auth/yggdrasil.py b/mojang/auth/yggdrasil.py new file mode 100644 index 00000000..2f8ab79b --- /dev/null +++ b/mojang/auth/yggdrasil.py @@ -0,0 +1,68 @@ +from ..urls import MOJANG_AUTHSERVER + +def authenticate(session, username: str, password: str, client_token=None): + url = MOJANG_AUTHSERVER.join('authenticate') + payload = { + 'username': username, + 'password': password, + 'clientToken': client_token + } + + response = session._request('post', url, json=payload) + if response.status_code == 200: + data = response.json() + session._update_token(access_token=data['accessToken'], client_token=data['clientToken']) + else: + pass + +def refresh(session, access_token: str, client_token: str): + url = MOJANG_AUTHSERVER.join('refresh') + payload = { + 'accessToken': access_token, + 'clientToken': client_token + } + + response = session._request('post', url, json=payload) + if response.status_code == 200: + data = response.json() + session._update_token(access_token=data['accessToken'], client_token=data['clientToken']) + else: + pass + +def validate(session, access_token: str, client_token: str): + url = MOJANG_AUTHSERVER.join('validate') + payload = { + 'accessToken': access_token, + 'clientToken': client_token + } + + response = session._request('post', url, json=payload) + return response.status_code == 204 + +def signout(session, username: str, password: str): + url = MOJANG_AUTHSERVER.join('signout') + payload = { + 'username': username, + 'password': password + } + + response = session._request('post', url, json=payload) + if response.status_code == 204: + session._update_token(access_token=None) + return True + else: + return False + +def invalidate(session, access_token: str, client_token: str): + url = MOJANG_AUTHSERVER.join('invalidate') + payload = { + 'accessToken': access_token, + 'clientToken': client_token + } + + response = session._request('post', url, json=payload) + if response.status_code == 204: + session._update_token(access_token=None) + return True + else: + return False diff --git a/pymojang/urls.py b/mojang/urls.py similarity index 100% rename from pymojang/urls.py rename to mojang/urls.py diff --git a/mojang/user/__init__.py b/mojang/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mojang/user/api.py b/mojang/user/api.py new file mode 100644 index 00000000..5dfb2bd4 --- /dev/null +++ b/mojang/user/api.py @@ -0,0 +1,56 @@ +import requests +from ..urls import MOJANG_STATUS, MOJANG_API, MOJANG_SESSION + +def status(service=None): + data = {} + response = requests.get(MOJANG_STATUS.join('check')) + if response.status_code == 200: + for status in response.json(): + data.update(status) + + if service: + return data[service] + + return data + +def names(player_id: str): + url = MOJANG_API.join('user/profiles/{}/names'.format(player_id)) + response = requests.get(url) + + names = [] + if response.status_code == 200: + data = response.json() + + for item in response.json(): + if 'changedToAt' in item: + item['changedToAt'] = dt.datetime.fromtimestamp(item['changedToAt']) + names.append((item['name'], item.get('changedToAt',None))) + + return names + +def uuid(username: str, timestamp=None, only_uuid=True): + url = MOJANG_API.join('users/profiles/minecraft/{}'.format(username)) + params = {'at': timestamp} if timestamp else {} + + data = {} + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + if only_uuid: + return data['id'] + + return data + +def uuids(usernames: list, only_uuid=True): + url = MOJANG_API.join('profiles/minecraft') + data = [] + + if len(usernames) > 0: + response = requests.post(url, json=usernames) + + if response.status_code == 200: + data = response.json() + if only_uuid: + data = list(map(lambda pdata: pdata['id'], data)) + + return data diff --git a/pymojang/user/cape.py b/mojang/user/cape.py similarity index 100% rename from pymojang/user/cape.py rename to mojang/user/cape.py diff --git a/mojang/user/profile.py b/mojang/user/profile.py new file mode 100644 index 00000000..8fdb7140 --- /dev/null +++ b/mojang/user/profile.py @@ -0,0 +1,116 @@ +import requests +import datetime as dt +import json +from base64 import urlsafe_b64decode +from ..urls import MINECRAFT_SERVICES, MOJANG_SESSION +from .skin import Skin +from .cape import Cape +from . import api + +class UserProperty: + + def __init__(self, name: str, fct): + self.__name = name + self.__fct = fct + + def __get__(self, obj, cls): + property_name = f'_{cls.__name__}__{self.__name}' + if hasattr(obj, property_name): + return getattr(obj, property_name) + else: + getattr(obj, self.__fct.__name__)() + return getattr(obj, property_name) + +class UserProfile: + + def __init__(self, session: requests.Session, username=None, authenticated=False, load=False): + self.__session = session + self.__username = username + self.__authenticated = authenticated + + if self.__authenticated: + self._load_profile() + self.__username = self.__name + self._load_uuid() + else: + self._load_uuid() + + if load: + self._load_names() + self._load_name_change() + if not self.__authenticated: + self._load_profile() + + def _load_uuid(self): + data = api.uuid(self.__username, only_uuid=False) + self.__user_id = data['id'] + self.__name = data['name'] + self.__legacy = data.get('legacy', False) + self.__demo = data.get('demo', False) + + def _load_names(self): + self.__names = api.names(self.__user_id) + + def _load_name_change(self): + if self.__authenticated: + url = MINECRAFT_SERVICES.join('minecraft/profile/namechange') + response = self.__session.get(url) + if response.status_code == 200: + data = response.json() + self.__created_at = dt.datetime.strptime(data['createdAt'], '%Y-%m-%dT%H:%M:%SZ') + self.__name_change_allowed = data['nameChangeAllowed'] + else: + pass + else: + self.__created_at = None + self.__name_change_allowed = None + + def _load_profile(self): + if self.__authenticated: + url = MINECRAFT_SERVICES.join('minecraft/profile') + response = self.__session.get(url) + + if response.status_code == 200: + data = response.json() + self.__user_id = data['id'] + self.__name = data['name'] + + self.__skins = [] + self.__capes = [] + + for skin in data['skins']: + self.__skins.append(Skin(skin['url'], skin['variant'].lower())) + + for cape in data['capes']: + self.__capes.append(Cape(cape['url'])) + else: + url = MOJANG_SESSION.join('session/minecraft/profile/{}'.format(self.__user_id)) + response = self.__session.get(url) + + if response.status_code == 200: + data = response.json() + self.__skins = [] + self.__capes = [] + + for d in data['properties']: + textures = json.loads(urlsafe_b64decode(d['value']))['textures'] + if 'SKIN' in textures.keys(): + self.__skins.append(Skin(textures['SKIN']['url'], textures['SKIN'].get('metadata',{}).get('model','classic'))) + if 'CAPE' in textures.keys(): + self.__capes.append(Cape(textures['CAPE']['url'])) + + name = UserProperty('name', _load_uuid) + uuid = UserProperty('user_id', _load_uuid) + is_legacy = UserProperty('legacy', _load_uuid) + is_demo = UserProperty('demo', _load_uuid) + + names = UserProperty('names', _load_names) + + created_at = UserProperty('created_at', _load_name_change) + name_change_allowed = UserProperty('name_change_allowed', _load_name_change) + + skins = UserProperty('skins', _load_profile) + capes = UserProperty('capes', _load_profile) + +def profile(username: str, load=False): + return UserProfile(requests.Session(), username=username, load=load) diff --git a/pymojang/user/skin.py b/mojang/user/skin.py similarity index 100% rename from pymojang/user/skin.py rename to mojang/user/skin.py diff --git a/mojang/utils/__init__.py b/mojang/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pymojang/utils/url.py b/mojang/utils/url.py similarity index 100% rename from pymojang/utils/url.py rename to mojang/utils/url.py diff --git a/pymojang/auth/__init__.py b/pymojang/auth/__init__.py deleted file mode 100644 index 26c8293a..00000000 --- a/pymojang/auth/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .security_check import SecurityCheck -from .yggdrasil import Yggdrasil \ No newline at end of file diff --git a/pymojang/auth/security_check.py b/pymojang/auth/security_check.py deleted file mode 100644 index 157c9e09..00000000 --- a/pymojang/auth/security_check.py +++ /dev/null @@ -1,26 +0,0 @@ -import requests -from ..urls import MOJANG_API - -class SecurityCheck: - - def __init__(self, session: requests.Session): - self._session = session - - @property - def ok(self): - check_url = MOJANG_API.join('user/security/location') - response = self._session.get(check_url) - return response.status_code == 204 - - @property - def challenges(self): - challenges_url = MOJANG_API.join('user/security/challenges') - response = self._session.get(challenges_url) - return response.json() - - def send_answers(self, answers: list): - answers_url = MOJANG_API.join('user/security/location') - response = self._session.post(answers_url, json=answers) - return response.status_code == 204 - - \ No newline at end of file diff --git a/pymojang/auth/yggdrasil.py b/pymojang/auth/yggdrasil.py deleted file mode 100644 index fcf21d03..00000000 --- a/pymojang/auth/yggdrasil.py +++ /dev/null @@ -1,85 +0,0 @@ -import requests -from ..utils import TokenPair -from ..urls import MOJANG_AUTHSERVER - -class Yggdrasil: - - def __init__(self,session: requests.Session, token_pair: TokenPair): - self._session = session - self._token_pair = token_pair - - if self._token_pair.access_token is not None: - self._session.headers.update({'Authorization': f'Bearer {self._token_pair.access_token}'}) - - def authenticate(self, username: str, password: str): - auth_url = MOJANG_AUTHSERVER.join('authenticate') - payload = { - 'username': username, - 'password': password, - 'clientToken': self._token_pair.client_token - } - - response = self._session.post(auth_url, json=payload) - if response.status_code == 200: - data = response.json() - self._token_pair.update(access_token=data['accessToken'], client_token=data['clientToken']) - self._session.headers.update({'Authorization': f'Bearer {self._token_pair.access_token}'}) - else: - pass - - def refresh(self): - refresh_url = MOJANG_AUTHSERVER.join('refresh') - payload = { - 'accessToken': self._token_pair.access_token, - 'clientToken': self._token_pair.client_token - } - - response = self._session.post(refresh_url, json=payload) - if response.status_code == 200: - data = response.json() - self._token_pair.update(access_token=data['accessToken'], client_token=data['clientToken']) - self._session.headers.update({'Authorization': f'Bearer {self._token_pair.access_token}'}) - else: - pass - - def validate(self): - validate_url = MOJANG_AUTHSERVER.join('validate') - payload = { - 'accessToken': self._token_pair.access_token, - 'clientToken': self._token_pair.client_token - } - - response = self._session.post(validate_url, json=payload) - return response.status_code == 204 - - def signout(self, username: str, password: str): - signout_url = MOJANG_AUTHSERVER.join('signout') - payload = { - 'username': username, - 'password': password - } - - response = self._session.post(signout_url, json=payload) - if response.status_code == 204: - self._token_pair.update(access_token=None) - self._session.headers.pop('Authorization') - - return True - else: - return False - - def invalidate(self): - invalidate_url = MOJANG_AUTHSERVER.join('invalidate') - payload = { - 'accessToken': self._token_pair.access_token, - 'clientToken': self._token_pair.client_token - } - - response = self._session.post(invalidate_url, json=payload) - if response.status_code == 204: - self._token_pair.update(access_token=None) - self._session.headers.pop('Authorization') - - return True - else: - return False diff --git a/pymojang/user/__init__.py b/pymojang/user/__init__.py deleted file mode 100644 index 89819d6d..00000000 --- a/pymojang/user/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import api -from .session import UserSession \ No newline at end of file diff --git a/pymojang/user/api.py b/pymojang/user/api.py deleted file mode 100644 index c844fb07..00000000 --- a/pymojang/user/api.py +++ /dev/null @@ -1,102 +0,0 @@ -import requests -import json -import datetime as dt -from base64 import urlsafe_b64decode -from .profile import UserProfile -from .skin import Skin -from .cape import Cape -from ..urls import MOJANG_STATUS, MOJANG_API, MOJANG_SESSION - -def api_status(): - result = {} - response = requests.get(MOJANG_STATUS.join('check')) - if response.status_code == 200: - data = response.json() - - for status in data: - for key, value in status.items(): - result[key] = value - - return result - -def get_name_history(player_id: str): - url = MOJANG_API.join('user/profiles/{}/names'.format(player_id)) - response = requests.get(url) - - names = [] - if response.status_code == 200: - data = response.json() - - for item in data: - if 'changedToAt' in item: - item['changedToAt'] = dt.datetime.fromtimestamp(item['changedToAt']) - names.append((item['name'], item.get('changedToAt',None))) - - return names - -def get_uuid(username: str, timestamp=None, only_uuid=True): - url = MOJANG_API.join('users/profiles/minecraft/{}'.format(username)) - params = {'at': timestamp} if timestamp else {} - - response = requests.get(url, params=params) - player_uuid = None - player_name = None - player_is_legacy = False - player_is_demo = False - if response.status_code == 200: - data = response.json() - - player_uuid = data['id'] - player_name = data['name'] - player_is_legacy = data.get('legacy', False) - player_is_demo = data.get('demo', False) - - if only_uuid: - return player_uuid - - return player_uuid, player_name, player_is_legacy, player_is_demo - -def get_uuids(usernames: list, only_uuid=True): - url = MOJANG_API.join('profiles/minecraft') - players_data = [] - - if len(usernames) > 0: - response = requests.post(url, json=usernames) - - if response.status_code == 200: - data = response.json() - - for player_data in data: - player_uuid = player_data['id'] - player_name = player_data['name'] - player_is_legacy = player_data.get('legacy', False) - player_is_demo = player_data.get('demo', False) - - if only_uuid: - players_data.append(player_uuid) - else: - players_data.append((player_uuid, player_name, player_is_legacy, player_is_demo)) - - return players_data - -def get_profile(player_id: str): - url = MOJANG_SESSION.join('session/minecraft/profile/{}'.format(player_id)) - response = requests.get(url) - profile = UserProfile() - - profile.names = get_name_history(player_id) - - if response.status_code == 200: - data = response.json() - - profile.id = data['id'] - profile.name = data['name'] - - for d in data['properties']: - textures = json.loads(urlsafe_b64decode(d['value']))['textures'] - if 'SKIN' in textures.keys(): - profile.skins.append(Skin(textures['SKIN']['url'], textures['SKIN'].get('metadata',{}).get('model','classic'))) - if 'CAPE' in textures.keys(): - profile.capes.append(Cape(textures['CAPE']['url'])) - - return profile diff --git a/pymojang/user/profile.py b/pymojang/user/profile.py deleted file mode 100644 index 9f83ddbe..00000000 --- a/pymojang/user/profile.py +++ /dev/null @@ -1,13 +0,0 @@ - -class UserProfile: - - def __init__(self): - self.created_at = None - self.name_change_allowed = None - - self.id = None - self.name = None - self.skins = [] - self.capes = [] - - self.names = [] diff --git a/pymojang/user/session.py b/pymojang/user/session.py deleted file mode 100644 index cc18e919..00000000 --- a/pymojang/user/session.py +++ /dev/null @@ -1,98 +0,0 @@ -import requests -import os -import datetime as dt -from . import api -from .profile import UserProfile -from ..auth import Yggdrasil, SecurityCheck -from ..utils import TokenPair -from .skin import Skin -from .cape import Cape -from ..urls import MINECRAFT_SERVICES - -class UserSession: - - def __init__(self, username: str, password: str, token_file=None): - self._session = requests.Session() - self._session.headers.update({'Content-Type': 'application/json'}) - - self._username = username - self._password = password - - self.token_pair = TokenPair(None, None) - if isinstance(token_file, str) and os.path.exists(token_file): - self.token_pair = TokenPair.from_pickle(token_file) - - self._auth = Yggdrasil(self._session, self.token_pair) - self._security = SecurityCheck(self._session) - self._profile = UserProfile() - - self._security_challenges = self._security.challenges - - def connect(self): - if self.token_pair.access_token is not None: - if not self._auth.validate(): - self._auth.refresh() - else: - self._auth.authenticate(self._username, self._password) - - self._load_user_data() - - def disconnect(self): - return self._auth.invalidate() - - def disconnect_all(self): - return self._auth.signout(self._username, self._password) - - def save(self, filename: str): - self.token_pair.to_pickle(filename) - - @property - def profile(self): - return self._profile - - # Security questions/answers - @property - def must_check_security(self): - return not self._security.ok - - @property - def security_challenges(self): - return self._security_challenges - - def send_security_answers(self, answers: list): - return self._security.send_answers(answers) - - # User data - def _load_user_data(self): - self._get_name_change() - self._get_profile() - - self._profile.names = api.get_name_history(self._profile.id) - - def _get_name_change(self): - name_change_url = MINECRAFT_SERVICES.join('minecraft/profile/namechange') - response = self._session.get(name_change_url) - - if response.status_code == 200: - data = response.json() - self._profile.created_at = dt.datetime.strptime(data['createdAt'], '%Y-%m-%dT%H:%M:%SZ') - self._profile.name_change_allowed = data['nameChangeAllowed'] - else: - pass - - def _get_profile(self): - profile_url = MINECRAFT_SERVICES.join('minecraft/profile') - response = self._session.get(profile_url) - - if response.status_code == 200: - data = response.json() - self._profile.id = data['id'] - self._profile.name = data['name'] - - for skin in data['skins']: - self._profile.skins.append(Skin(skin['url'], skin['variant'].lower())) - - for cape in data['capes']: - self._profile.capes.append(Cape(cape['url'])) - else: - pass diff --git a/pymojang/utils/__init__.py b/pymojang/utils/__init__.py deleted file mode 100644 index ec2d870e..00000000 --- a/pymojang/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .token import TokenPair \ No newline at end of file diff --git a/pymojang/utils/token.py b/pymojang/utils/token.py deleted file mode 100644 index 223d782b..00000000 --- a/pymojang/utils/token.py +++ /dev/null @@ -1,43 +0,0 @@ -import pickle - -class TokenPair: - - def __init__(self, access_token: str, client_token: str): - self.__access_token = access_token - self.__client_token = client_token - - @property - def access_token(self): - return self.__access_token - - @property - def client_token(self): - return self.__client_token - - def update(self, **kwargs): - self.__access_token = kwargs.get('access_token', self.access_token) - self.__client_token = kwargs.get('client_token', self.client_token) - - def to_pickle(self, filename: str): - with open(filename, 'wb') as f: - pickle.dump(self, f) - - @classmethod - def from_pickle(cls, filename: str): - obj = None - with open(filename, 'rb') as f: - obj = pickle.load(f) - - return obj - - def __getnewargs__(self): - return (self.access_token, self.client_token) - - def __iter__(self): - return (self.access_token, self.client_token).__iter__() - - def __str__(self): - return "('{}','{}')".format(self.access_token, self.client_token) - - def __repr__(self): - return "('{}','{}')".format(self.access_token, self.client_token) diff --git a/setup.py b/setup.py index f6431da6..d9bb3fea 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,6 @@ author_email='lucapalmi772@gmail.com', licence='MIT', description='It\'s a full wrapper arround de mojang API and authentication API', - packages=['pymojang'], + packages=['mojang'], install_requires=['requests','validators'] ) \ No newline at end of file