diff --git a/rblxopencloud/__init__.py b/rblxopencloud/__init__.py index 516d670..1852842 100644 --- a/rblxopencloud/__init__.py +++ b/rblxopencloud/__init__.py @@ -1,11 +1,14 @@ from typing import Literal +import requests -VERSION: str = "1.4.0" -VERSION_INFO: Literal['alpha', 'beta', 'final'] = "final" +VERSION: str = "1.5.1" +VERSION_INFO: Literal['alpha', 'beta', 'final'] = "alpha" user_agent: str = f"rblx-open-cloud/{VERSION} (https://github.com/treeben77/rblx-open-cloud)" -del Literal +request_session: requests.Session = requests.Session() + +del Literal, requests from .experience import * from .datastore import * diff --git a/rblxopencloud/creator.py b/rblxopencloud/creator.py index 4eb08c9..7b89de1 100644 --- a/rblxopencloud/creator.py +++ b/rblxopencloud/creator.py @@ -1,10 +1,10 @@ from .exceptions import InvalidAsset, InvalidKey, ModeratedText, rblx_opencloudException, RateLimited, ServiceUnavailable -import requests, json, io +import json, io from typing import Union, Optional, TYPE_CHECKING import urllib3 from enum import Enum from datetime import datetime -from . import user_agent +from . import user_agent, request_session if TYPE_CHECKING: from .user import User @@ -68,7 +68,7 @@ def fetch_operation(self) -> Optional[Asset]: Checks if the asset has finished proccessing, if so returns the :class:`rblx-open-cloud.Asset` object. """ - response = requests.get(f"https://apis.roblox.com/assets/v1/{self.__path}", + response = request_session.get(f"https://apis.roblox.com/assets/v1/{self.__path}", headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "user-agent": user_agent}) if response.ok: @@ -140,7 +140,7 @@ def upload_asset(self, file: io.BytesIO, asset_type: Union[AssetType, str], name }), "fileContent": (file.name, file.read(), mimetypes.get(file.name.split(".")[-1])) }) - response = requests.post(f"https://apis.roblox.com/assets/v1/assets", + response = request_session.post(f"https://apis.roblox.com/assets/v1/assets", headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "content-type": contentType, "user-agent": user_agent}, data=body) if response.status_code == 400 and response.json()["message"] == "\"InvalidImage\"": raise InvalidAsset(f"The file is not a supported type, or is corrupted") @@ -151,7 +151,7 @@ def upload_asset(self, file: io.BytesIO, asset_type: Union[AssetType, str], name elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") elif not response.ok: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - response_op = requests.get(f"https://apis.roblox.com/assets/v1/{response.json()['path']}", + response_op = request_session.get(f"https://apis.roblox.com/assets/v1/{response.json()['path']}", headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "user-agent": user_agent}) if not response_op.ok: @@ -184,7 +184,7 @@ def update_asset(self, asset_id: int, file: io.BytesIO) -> Union[Asset, PendingA }), "fileContent": (file.name, file.read(), mimetypes.get(file.name.split(".")[-1])) }) - response = requests.patch(f"https://apis.roblox.com/assets/v1/assets/{asset_id}", + response = request_session.patch(f"https://apis.roblox.com/assets/v1/assets/{asset_id}", headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "content-type": contentType, "user-agent": user_agent}, data=body) if response.status_code == 400 and response.json()["message"] == "\"InvalidImage\"": raise InvalidAsset(f"The file is not a supported type, or is corrupted") @@ -195,7 +195,7 @@ def update_asset(self, asset_id: int, file: io.BytesIO) -> Union[Asset, PendingA elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") elif not response.ok: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - response_op = requests.get(f"https://apis.roblox.com/assets/v1/{response.json()['path']}", + response_op = request_session.get(f"https://apis.roblox.com/assets/v1/{response.json()['path']}", headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "user-agent": user_agent}) if not response_op.ok: diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index d2a57cf..3670a53 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -1,8 +1,8 @@ from .exceptions import rblx_opencloudException, InvalidKey, NotFound, RateLimited, ServiceUnavailable, PreconditionFailed -import requests, json, datetime +import json, datetime import base64, hashlib, urllib.parse from typing import Union, Optional, Iterable, TYPE_CHECKING -from . import user_agent +from . import user_agent, request_session if TYPE_CHECKING: from .experience import Experience @@ -120,7 +120,7 @@ def list_keys(self, prefix: str="", limit: Optional[int]=None) -> Iterable[Liste nextcursor = "" yields = 0 while limit == None or yields < limit: - response = requests.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries", + response = request_session.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "datastoreName": self.name, "scope": self.scope, @@ -155,7 +155,7 @@ def get(self, key: str) -> tuple[Union[str, dict, list, int, float], EntryInfo]: if not scope: scope, key = key.split("/", maxsplit=1) except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for DataStore without a scope.") - response = requests.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", + response = request_session.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "datastoreName": self.name, "scope": scope, @@ -198,7 +198,7 @@ def set(self, key: str, value: Union[str, dict, list, int, float], users:Optiona if users == None: users = [] data = json.dumps(value) - response = requests.post(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", + response = request_session.post(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", headers={"x-api-key": self.__api_key, "user-agent": user_agent, "roblox-entry-userids": json.dumps(users), "roblox-entry-attributes": json.dumps(metadata), "content-md5": base64.b64encode(hashlib.md5(data.encode()).digest())}, data=data, params={ "datastoreName": self.name, @@ -250,7 +250,7 @@ def increment(self, key: str, increment: Union[int, float], users:Optional[list[ raise ValueError("a scope and key seperated by a forward slash is required for DataStore without a scope.") if users == None: users = [] - response = requests.post(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/increment", + response = request_session.post(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/increment", headers={"x-api-key": self.__api_key, "user-agent": user_agent, "roblox-entry-userids": json.dumps(users), "roblox-entry-attributes": json.dumps(metadata)}, params={ "datastoreName": self.name, "scope": scope, @@ -285,7 +285,7 @@ def remove(self, key: str) -> None: if not scope: scope, key = key.split("/", maxsplit=1) except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for DataStore without a scope.") - response = requests.delete(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", + response = request_session.delete(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "datastoreName": self.name, "scope": scope, @@ -329,7 +329,7 @@ def list_versions(self, key: str, after: Optional[datetime.datetime]=None, befor nextcursor = "" yields = 0 while limit == None or yields < limit: - response = requests.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/versions", + response = request_session.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/versions", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "datastoreName": self.name, "scope": scope, @@ -368,7 +368,7 @@ def get_version(self, key: str, version: str) -> tuple[Union[str, dict, list, in if not scope: scope, key = key.split("/", maxsplit=1) except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for DataStore without a scope.") - response = requests.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/versions/version", + response = request_session.get(f"https://apis.roblox.com/datastores/v1/universes/{self.experience.id}/standard-datastores/datastore/entries/entry/versions/version", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "datastoreName": self.name, "scope": scope, @@ -465,7 +465,7 @@ def sort_keys(self, descending: bool=True, limit: Union[None, int]=None, min=Non nextcursor = "" yields = 0 while limit == None or yields < limit: - response = requests.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(self.scope)}/entries", + response = request_session.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(self.scope)}/entries", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={ "max_page_size": limit if limit and limit < 100 else 100, "order_by": "desc" if descending else None, @@ -500,7 +500,7 @@ def get(self, key: str) -> int: else: scope = self.scope except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - response = requests.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", + response = request_session.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", headers={"x-api-key": self.__api_key, "user-agent": user_agent}) if response.status_code == 200: return int(response.json()["value"]) @@ -528,12 +528,12 @@ def set(self, key: str, value: int, exclusive_create: bool=False, exclusive_upda if exclusive_create and exclusive_update: raise ValueError("exclusive_create and exclusive_updated can not both be True") if not exclusive_create: - response = requests.patch(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", + response = request_session.patch(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={"allow_missing": not exclusive_update}, json={ "value": value }) else: - response = requests.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries", + response = request_session.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, params={"id": key}, json={ "value": value }) @@ -563,7 +563,7 @@ def increment(self, key: str, increment: int) -> None: else: scope = self.scope except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - response = requests.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}:increment", + response = request_session.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}:increment", headers={"x-api-key": self.__api_key, "user-agent": user_agent}, json={ "amount": increment }) @@ -588,7 +588,7 @@ def remove(self, key: str) -> None: else: scope = self.scope except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - response = requests.delete(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", + response = request_session.delete(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", headers={"x-api-key": self.__api_key, "user-agent": user_agent}) if response.status_code in [200, 204]: return None diff --git a/rblxopencloud/experience.py b/rblxopencloud/experience.py index d4c7d0a..29d1633 100644 --- a/rblxopencloud/experience.py +++ b/rblxopencloud/experience.py @@ -1,8 +1,8 @@ from .exceptions import rblx_opencloudException, InvalidKey, NotFound, RateLimited, ServiceUnavailable -import requests, io +import io from typing import Optional, Iterable, Literal, Union from .datastore import DataStore, OrderedDataStore -from . import user_agent +from . import user_agent, request_session __all__ = ( "Experience", @@ -81,7 +81,7 @@ def list_data_stores(self, prefix: Optional[str]="", limit: Optional[int]=None, nextcursor = "" yields = 0 while limit == None or yields < limit: - response = requests.get(f"https://apis.roblox.com/datastores/v1/universes/{self.id}/standard-datastores", + response = request_session.get(f"https://apis.roblox.com/datastores/v1/universes/{self.id}/standard-datastores", headers={"x-api-key": self.__api_key}, params={ "prefix": prefix, "cursor": nextcursor if nextcursor else None @@ -116,7 +116,7 @@ def publish_message(self, topic: str, data: str) -> None: topic: str - The topic to send the message in data: str - The message to send. Open Cloud does not support sending dictionaries/tables with publishing messages. You'll have to json encode it before sending it, and decode it in Roblox. """ - response = requests.post(f"https://apis.roblox.com/messaging-service/v1/universes/{self.id}/topics/{topic}", + response = request_session.post(f"https://apis.roblox.com/messaging-service/v1/universes/{self.id}/topics/{topic}", json={"message": data}, headers={"x-api-key" if not self.__api_key.startswith("Bearer ") else "authorization": self.__api_key, "user-agent": user_agent}) if response.status_code == 200: return elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") @@ -139,7 +139,7 @@ def upload_place(self, place_id:int, file: io.BytesIO, publish: Optional[bool] = file: io.BytesIO - The file to upload. The file should be opened in bytes. publish: Optional[bool] - Wether to publish the place as well. Defaults to `False`. """ - response = requests.post(f"https://apis.roblox.com/universes/v1/{self.id}/places/{place_id}/versions", + response = request_session.post(f"https://apis.roblox.com/universes/v1/{self.id}/places/{place_id}/versions", headers={"x-api-key": self.__api_key, 'content-type': 'application/octet-stream', "user-agent": user_agent}, data=file.read(), params={ "versionType": "Published" if publish else "Saved" }) diff --git a/rblxopencloud/oauth2.py b/rblxopencloud/oauth2.py index eb81cc5..70fbbde 100644 --- a/rblxopencloud/oauth2.py +++ b/rblxopencloud/oauth2.py @@ -1,16 +1,16 @@ from .exceptions import rblx_opencloudException, InvalidKey, ServiceUnavailable, InsufficientScope, InvalidCode from urllib import parse -import requests, datetime, time +import datetime, time from typing import Optional, Union, TYPE_CHECKING from .user import User from .group import Group from .experience import Experience -import requests, base64, jwt +import base64, jwt from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_der_public_key from cryptography.hazmat.backends import default_backend import hashlib, string, secrets -from . import user_agent +from . import user_agent, request_session __all__ = ( "OAuth2App", @@ -65,7 +65,7 @@ def fetch_userinfo(self) -> User: Returns a `rblx-open-cloud.User` object for this authorization. You can use this object to directly access user resources (like uploading files), if it was authorized. """ - response = requests.get("https://apis.roblox.com/oauth/v1/userinfo", headers={ + response = request_session.get("https://apis.roblox.com/oauth/v1/userinfo", headers={ "authorization": f"Bearer {self.token}", "user-agent": user_agent }) user = User(response.json().get("id") or response.json().get("sub"), f"Bearer {self.token}") @@ -86,7 +86,7 @@ def fetch_resources(self) -> Resources: Fetches the authorized accounts (users and groups), and experiences. """ - response = requests.post("https://apis.roblox.com/oauth/v1/token/resources", data={ + response = request_session.post("https://apis.roblox.com/oauth/v1/token/resources", data={ "token": self.token, "client_id": self.app.id, "client_secret": self.app._OAuth2App__secret @@ -128,7 +128,7 @@ def fetch_token_info(self) -> AccessTokenInfo: Fetches information the token such as the user's id, the authorized scope, and it's expiry time. """ - response = requests.post("https://apis.roblox.com/oauth/v1/token/introspect", data={ + response = request_session.post("https://apis.roblox.com/oauth/v1/token/introspect", data={ "token": self.token, "client_id": self.app.id, "client_secret": self.app._OAuth2App__secret @@ -242,7 +242,7 @@ def exchange_code(self, code: str, code_verifier: Optional[str]=None) -> AccessT code: str - The code from the authorization server. code_verifier: Optional[str] - the string for this OAuth2 flow generated by `OAuth2App.generate_code_verifier()`. """ - response = requests.post("https://apis.roblox.com/oauth/v1/token", data={ + response = request_session.post("https://apis.roblox.com/oauth/v1/token", data={ "client_id": self.id, "client_secret": self.__secret, "redirect_uri": self.redirect_uri, @@ -253,7 +253,7 @@ def exchange_code(self, code: str, code_verifier: Optional[str]=None) -> AccessT id_token = None if response.json().get("id_token"): if not self.__openid_certs_cache or time.time() - self.__openid_certs_cache_updated > self.openid_certs_cache_seconds: - certs = requests.get("https://apis.roblox.com/oauth/v1/certs", headers={"user-agent": user_agent}) + certs = request_session.get("https://apis.roblox.com/oauth/v1/certs", headers={"user-agent": user_agent}) if not certs.ok: raise ServiceUnavailable("Failed to retrieve OpenID certs.") self.__openid_certs_cache = [] @@ -287,7 +287,7 @@ def refresh_token(self, refresh_token: str) -> AccessToken: ### Parameters refresh_token: str - The refresh token to refresh. """ - response = requests.post("https://apis.roblox.com/oauth/v1/token", data={ + response = request_session.post("https://apis.roblox.com/oauth/v1/token", data={ "client_id": self.id, "client_secret": self.__secret, "grant_type": "refresh_token", @@ -304,7 +304,7 @@ def revoke_token(self, token: str): token: str - The refresh token to refresh. """ - response = requests.post("https://apis.roblox.com/oauth/v1/token/revoke", data={ + response = request_session.post("https://apis.roblox.com/oauth/v1/token/revoke", data={ "token": token, "client_id": self.id, "client_secret": self.__secret