cloudapi.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from typing import Literal, Callable, Any
  2. from gql import Client, gql
  3. from gql.transport.requests import RequestsHTTPTransport
  4. import requests
  5. import json
  6. from .utils import typed_dataclass, inf
  7. @typed_dataclass
  8. class RoomInfo:
  9. id: str
  10. name: str
  11. description: str
  12. url: str
  13. scene_id: str
  14. room_size: int
  15. lobby_count: int
  16. member_count: int
  17. user_data: dict
  18. is_public: bool
  19. preview_image_url: str
  20. @classmethod
  21. def from_obj(cls, data: dict):
  22. if not data.get("type") == "room":
  23. return data
  24. return cls(
  25. id=data.get("id", ""),
  26. name=data.get("name", ""),
  27. description=data.get("description", ""),
  28. url=data.get("url", ""),
  29. scene_id=data.get("scene_id", ""),
  30. room_size=data.get("room_size") or 0,
  31. lobby_count=data.get("lobby_count") or 0,
  32. member_count=data.get("member_count") or 0,
  33. user_data=data.get("user_data") or {},
  34. is_public=data.get("is_public", True),
  35. preview_image_url=data.get("images", {}).get("preview", {}).get("url") or "",
  36. )
  37. @typed_dataclass
  38. class AvatarInfo:
  39. id: str
  40. name: str
  41. description: str
  42. url: str
  43. preview_images: dict
  44. gltfs: dict
  45. attributions: dict
  46. allow_remixing: bool
  47. @classmethod
  48. def from_obj(cls, data: dict):
  49. if not data.get("type") == "avatar_listing":
  50. return data
  51. return cls(
  52. id=data.get("id", ""),
  53. name=data.get("name", ""),
  54. description=data.get("description", ""),
  55. url=data.get("url", ""),
  56. preview_images=dict(data.get("images", {})).get("preview") or {},
  57. gltfs=data.get("gltfs") or {},
  58. attributions=data.get("attributions") or {},
  59. allow_remixing=data.get("allow_remixing", True),
  60. )
  61. class CloudAPI:
  62. def __init__(self, host: str, app_token: str = None, user_token: str = None, user_id: str = None):
  63. """Hubs Cloud API client.
  64. :param host: The host of the room, e.g. "hubs.mozilla.com"
  65. :param app_token: The API key app token (from https://<host>/token)
  66. :param user_token: The API key user token (from https://<host>/token)
  67. """
  68. self.host = host
  69. self.gqlapp_transport = None
  70. self.gqlapp_client = None
  71. self.app_token = app_token
  72. if app_token is not None:
  73. self._gql_app_connect()
  74. self.gqluser_transport = None
  75. self.gqluser_client = None
  76. self.user_token = user_token
  77. if user_token is not None:
  78. self._gql_user_connect()
  79. self.user_id = user_id
  80. def _gql_app_connect(self, app_token: str = None):
  81. self.app_token = app_token or self.app_token
  82. self.gqlapp_transport = RequestsHTTPTransport(
  83. url=f"https://{self.host}/api/v2_alpha/graphiql",
  84. use_json=True,
  85. headers={
  86. "Content-type": "application/json",
  87. "Authorization": "Bearer " + app_token,
  88. },
  89. verify=True,
  90. retries=3,
  91. )
  92. self.gqlapp_client = Client(transport=self.gqlapp_transport, fetch_schema_from_transport=True)
  93. def _gql_user_connect(self, user_token: str = None):
  94. self.user_token = user_token or self.user_token
  95. self.gqluser_transport = RequestsHTTPTransport(
  96. url=f"https://{self.host}/api/v2_alpha/graphiql",
  97. use_json=True,
  98. headers={
  99. "Content-type": "application/json",
  100. "Authorization": "Bearer " + user_token,
  101. },
  102. verify=True,
  103. retries=3,
  104. )
  105. self.gqluser_client = Client(transport=self.gqluser_transport, fetch_schema_from_transport=True)
  106. def _v1api_query(self, route: str, params: dict = {}, method: Literal["GET", "POST"] = "GET"):
  107. headers = {
  108. 'Accept': 'application/json',
  109. 'User-Agent': 'HubsClient/0.1.0',
  110. }
  111. if self.user_token is not None:
  112. headers['Authorization'] = f'Bearer {self.user_token}'
  113. match method:
  114. case "GET":
  115. return requests.get(f"https://{self.host}/api/v1/{route}", params=params, headers=headers)
  116. case "POST":
  117. return requests.post(
  118. f"https://{self.host}/api/v1/{route}",
  119. data=json.dumps(params).encode('utf-8'),
  120. headers=headers & {'Content-Type': 'application/json'},
  121. )
  122. def media_search(
  123. self,
  124. type: Literal["rooms", "scene_listings", "avatar_listings", "scenes", "avatars", "favorites", "assets"],
  125. query: str = None,
  126. _parser: Callable[[dict], Any] | None = None,
  127. page_limit = None,
  128. **kwargs,
  129. ):
  130. entries = []
  131. cursor = 1
  132. while (cursor or inf) < (page_limit or inf) + 1:
  133. resp = self._v1api_query(
  134. "media/search", params={"source": type, "q": query, "user": self.user_id, "cursor": cursor, **kwargs}
  135. ).json(object_hook=_parser)
  136. cursor = resp["meta"]["next_cursor"]
  137. entries.extend(resp["entries"])
  138. return entries
  139. def get_public_rooms(self, **kwargs):
  140. return self.media_search("rooms", filter="public", _parser=RoomInfo.from_obj, **kwargs)
  141. def get_avatars(self, **kwargs):
  142. return self.media_search("avatar_listings", _parser=AvatarInfo.from_obj, **kwargs)