class MojangAPI(object): """ A thin wrapper for the the core portion of the Mojang API References ---------- * http://wiki.vg/Mojang_API """ def __init__(self, auth, host=DEFAULT_MOJANG_API_HOST): self.auth = auth self.api = APIHost(host) if self.auth.accessToken: bearer = "Bearer " + self.auth.accessToken self.api.headers["Authorization"] = bearer def username_to_uuid(self, username, at_time=0): return self.api.get("/users/profiles/minecraft/%s?at=%i" % (username, at_time)) def uuid_name_history(self, uuid): return self.api.get("/user/profiles/%s/names" % uuid) def playernames_to_uuids(self, playernames): return self.api.post("/profiles/minecraft", list(playernames)) def change_skin(self, uuid, skin_url, slim=False): payload = {"model": "slim" if slim else "", "url": skin_url} return self.api.post_encoded("/user/profile/%s/skin" % uuid) def upload_skin(self, uuid, skin_stream, slim=False): payload = {"model": "slim" if slim else "", "file": ("harambe.png", skin_stream, "image/png")} return self.api.post_form("/user/profile/%s/skin" % uuid, payload) def upload_skin_filename(self, uuid, skin_filename, slim=False): with open(skin_filename) as skin_stream: return upload_skin(self, uuid, skin_stream, slim) def reset_skin(self, uuid): return self.api.delete("/user/profile/%s/skin" % uuid) def whoami(self): return self.api.get("/user") def statistics(self, which=DEFAULT_STATISTICS): which = {"metricKeys": list(which)} return self.api.post("/orders/statistics", which)
class Authentication(object): """ A thin wrapper for the Mojang authentiation scheme, 'Yggdrasil' References ---------- * http://wiki.vg/Authentication """ def __init__(self, username, clientToken=None, accessToken=None, host=HOST_YGGDRASIL, agent=MINECRAFT_AGENT_V1): self.api = APIHost(host) self.username = username self.user = None self.agent = agent self.clientToken = clientToken self.accessToken = accessToken self.selectedProfile = None def authenticate(self, password): """ generate an accessToken for this session """ payload = { "username": self.username, "password": password, "requestUser": True } if self.agent: payload["agent"] = self.agent if self.clientToken: payload["clientToken"] = self.clientToken try: ret = self.api.post("/authenticate", payload) except HTTPError as err: # if it's just a 403, that means the auth was wrong, so # it's simple failure. Any other kind of error is a # different kind of problem, so we'll propagate it up. if err.response.status_code == 403: return False else: raise else: self.clientToken = ret["clientToken"] self.accessToken = ret["accessToken"] self.selectedProfile = ret.get("selectedProfile") self.user = ret.get("user") return True def refresh(self): """ ensure that this session remains valid. May result in a new accessToken. """ payload = { "accessToken": self.accessToken, "clientToken": self.clientToken, "requestUser": True } try: ret = self.api.post("/refresh", payload) except HTTPError as err: # a 403 just means the session was completely invalid, # which is expected behavior in many circumstances. In # that case, we just return False. Any other error gets # propagated up. if err.response.status_code == 403: return False else: raise else: self.clientToken = ret["clientToken"] self.accessToken = ret["accessToken"] self.selectedProfile = ret.get("selectedProfile") self.user = ret.get("user") return True def validate(self): """ check that the session is currently valid, and can be used to perform other actions. An invalid session will need to be renewed or a full re-auth may be required. """ if not self.accessToken: return False payload = { "accessToken": self.accessToken, } try: ret = self.api.post("/validate", payload) except HTTPError as err: # one again, 403 is an expected possibility. Everything # else is wonky. if err.response.status_code == 403: return False else: raise else: return True def signout(self, password): """ invalidates all sessions against the specified account """ payload = { "username": self.username, "password": password, } try: ret = self.api.post("/signout", payload) except HTTPError as err: # 403 means bad username/password in this case if err.response.status_code == 403: return False else: raise else: return True def invalidate(self): """ invalidates the current session """ if not self.accessToken: return None payload = { "accessToken": self.accessToken, "clientToken": self.clientToken, } # even if we're already invalidated, this won't raise an # HTTPError, so we won't try to filter out a 403 ret = self.api.post("/invalidate", payload) self.accessToken = None return True def load(self, filename): """ set the state of this session to the what is represented in the JSON data stored in filename. Errors (access, malformed JSON, etc) while loading will be propagated. """ with open(filename) as fd: session = load(fd) if "host" in session: host = session.pop("host") self.api = APIHost(host) self.__dict__.update(session) def save(self, filename): """ save the state of this session to JSON data and write it to filename """ session = dict(self.__dict__) session["host"] = self.api._host del session["api"] with open(filename, "w") as fd: dump(session, fd) def ensureClientToken(self): """ generate a clientToken for this session if one doesn't already exist """ if not self.clientToken: self.clientToken = generate_clientToken()
class RealmsAPI(object): """ A thin wrapper for the Mojang Realms API References ---------- * http://wiki.vg/Realms_API """ def __init__(self, auth, host=DEFAULT_REALMS_HOST, version=DEFAULT_REALMS_VERSION): # compose the necessary cookies from data in the auth object self.auth = auth sid = "token:%s:%s" % (auth.accessToken, auth.selectedProfile["id"]) user = auth.selectedProfile["name"] self.api = APIHost(host) self.api.cookies.set("sid", sid) self.api.cookies.set("user", user) self.api.cookies.set("version", version) def mco_available(self): return self.api.get("/mco/available") def mco_client_outdated(self): return self.api.get("/mco/client/outdated") def mco_tos_agree(self): return self.api.post("/mco/tos/agreed") def realm_list(self): """ List the realms available for the given account auth """ return self.api.get("/worlds") def realm_info(self, realm_id): """ Information about a specific realm by ID """ return self.api.get("/worlds/%i" % realm_id) def realm_join(self, realm_id): """ Wakes up a realm so that it can be joined, returns a string specifying the IP_ADDRESS:PORT of the running server """ return self.api.get("/worlds/%i/join" % realm_id) def realm_backups(self, realm_id): """ Show the backups available for the given realm ID """ return self.api.get("/worlds/%i/backups" % realm_id) def realm_world_url(self, realm_id, world): """ Show the download URL for the latest world backup for the given realm ID """ return self.api.get("/worlds/%i/slot/%i/download" % (realm_id, world)) def realm_ops_list(self, realm_id): return self.api.get("/ops/%i" % realm_id) def realm_subscription(self, realm_id): return self.api.get("/subscriptions/%i" % realm_id)