subscription.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # mautrix-instagram - A Matrix-Instagram puppeting bridge.
  2. # Copyright (C) 2020 Tulir Asokan
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. from __future__ import annotations
  17. from typing import Any
  18. from enum import Enum
  19. from uuid import uuid4
  20. import json
  21. class SkywalkerSubscription:
  22. @staticmethod
  23. def direct_sub(user_id: str | int) -> str:
  24. return f"ig/u/v1/{user_id}"
  25. @staticmethod
  26. def live_sub(user_id: str | int) -> str:
  27. return f"ig/live_notification_subscribe/{user_id}"
  28. class GraphQLQueryID(Enum):
  29. APP_PRESENCE = "17846944882223835"
  30. ASYNC_AD_SUB = "17911191835112000"
  31. CLIENT_CONFIG_UPDATE = "17849856529644700"
  32. DIRECT_STATUS = "17854499065530643"
  33. DIRECT_TYPING = "17867973967082385"
  34. LIVE_WAVE = "17882305414154951"
  35. INTERACTIVITY_ACTIVATE_QUESTION = "18005526940184517"
  36. INTERACTIVITY_REALTIME_QUESTION_SUBMISSION_STATUS = "18027779584026952"
  37. INTERACTIVITY_SUB = "17907616480241689"
  38. LIVE_REALTIME_COMMENTS = "17855344750227125"
  39. LIVE_TYPING_INDICATOR = "17926314067024917"
  40. MEDIA_FEEDBACK = "17877917527113814"
  41. REACT_NATIVE_OTA = "17861494672288167"
  42. VIDEO_CALL_CO_WATCH_CONTROL = "17878679623388956"
  43. VIDEO_CALL_IN_ALERT = "17878679623388956"
  44. VIDEO_CALL_PROTOTYPE_PUBLISH = "18031704190010162"
  45. VIDEO_CALL_PARTICIPANT_DELIVERY = "17977239895057311"
  46. ZERO_PROVISION = "17913953740109069"
  47. INAPP_NOTIFICATION = "17899377895239777"
  48. BUSINESS_DELIVERY = "17940467278199720"
  49. everclear_subscriptions = {
  50. "inapp_notification_subscribe_comment": GraphQLQueryID.INAPP_NOTIFICATION.value,
  51. "inapp_notification_subscribe_comment_mention_and_reply": GraphQLQueryID.INAPP_NOTIFICATION.value,
  52. "video_call_participant_state_delivery": GraphQLQueryID.VIDEO_CALL_PARTICIPANT_DELIVERY.value,
  53. "inapp_notification_subscribe_story_emoji_reaction": GraphQLQueryID.INAPP_NOTIFICATION.value,
  54. "inapp_notification_subscribe_prompt_sticker_reply": GraphQLQueryID.INAPP_NOTIFICATION.value,
  55. "inapp_notification_subscribe_fundraiser_cohost_invited": GraphQLQueryID.INAPP_NOTIFICATION.value,
  56. "inapp_notification_subscribe_watch_receipt": GraphQLQueryID.INAPP_NOTIFICATION.value,
  57. }
  58. class GraphQLSubscription:
  59. @staticmethod
  60. def _fmt(
  61. query_id: GraphQLQueryID, input_params: Any, client_logged: bool | None = None
  62. ) -> str:
  63. params = {
  64. "input_data": input_params,
  65. **(
  66. {"%options": {"client_logged": client_logged}} if client_logged is not None else {}
  67. ),
  68. }
  69. return f"1/graphqlsubscriptions/{query_id.value}/{json.dumps(params)}"
  70. @classmethod
  71. def app_presence(
  72. cls, subscription_id: str | None = None, client_logged: bool | None = None
  73. ) -> str:
  74. return cls._fmt(
  75. GraphQLQueryID.APP_PRESENCE,
  76. input_params={"client_subscription_id": subscription_id or str(uuid4())},
  77. client_logged=client_logged,
  78. )
  79. @classmethod
  80. def async_ad(
  81. cls,
  82. user_id: str,
  83. subscription_id: str | None = None,
  84. client_logged: bool | None = None,
  85. ) -> str:
  86. return cls._fmt(
  87. GraphQLQueryID.ASYNC_AD_SUB,
  88. input_params={
  89. "client_subscription_id": subscription_id or str(uuid4()),
  90. "user_id": user_id,
  91. },
  92. client_logged=client_logged,
  93. )
  94. @classmethod
  95. def client_config_update(
  96. cls, subscription_id: str | None = None, client_logged: bool | None = None
  97. ) -> str:
  98. return cls._fmt(
  99. GraphQLQueryID.CLIENT_CONFIG_UPDATE,
  100. input_params={"client_subscription_id": subscription_id or str(uuid4())},
  101. client_logged=client_logged,
  102. )
  103. @classmethod
  104. def direct_status(
  105. cls, subscription_id: str | None = None, client_logged: bool | None = None
  106. ) -> str:
  107. return cls._fmt(
  108. GraphQLQueryID.DIRECT_STATUS,
  109. input_params={"client_subscription_id": subscription_id or str(uuid4())},
  110. client_logged=client_logged,
  111. )
  112. @classmethod
  113. def direct_typing(cls, user_id: str, client_logged: bool | None = None) -> str:
  114. return cls._fmt(
  115. GraphQLQueryID.DIRECT_TYPING,
  116. input_params={"user_id": user_id},
  117. client_logged=client_logged,
  118. )
  119. @classmethod
  120. def ig_live_wave(
  121. cls,
  122. broadcast_id: str,
  123. receiver_id: str,
  124. subscription_id: str | None = None,
  125. client_logged: bool | None = None,
  126. ) -> str:
  127. return cls._fmt(
  128. GraphQLQueryID.LIVE_WAVE,
  129. input_params={
  130. "client_subscription_id": subscription_id or str(uuid4()),
  131. "broadcast_id": broadcast_id,
  132. "receiver_id": receiver_id,
  133. },
  134. client_logged=client_logged,
  135. )
  136. @classmethod
  137. def interactivity_activate_question(
  138. cls,
  139. broadcast_id: str,
  140. subscription_id: str | None = None,
  141. client_logged: bool | None = None,
  142. ) -> str:
  143. return cls._fmt(
  144. GraphQLQueryID.INTERACTIVITY_ACTIVATE_QUESTION,
  145. input_params={
  146. "client_subscription_id": subscription_id or str(uuid4()),
  147. "broadcast_id": broadcast_id,
  148. },
  149. client_logged=client_logged,
  150. )
  151. @classmethod
  152. def interactivity_realtime_question_submissions_status(
  153. cls,
  154. broadcast_id: str,
  155. subscription_id: str | None = None,
  156. client_logged: bool | None = None,
  157. ) -> str:
  158. return cls._fmt(
  159. GraphQLQueryID.INTERACTIVITY_REALTIME_QUESTION_SUBMISSION_STATUS,
  160. input_params={
  161. "client_subscription_id": subscription_id or str(uuid4()),
  162. "broadcast_id": broadcast_id,
  163. },
  164. client_logged=client_logged,
  165. )
  166. @classmethod
  167. def interactivity(
  168. cls,
  169. broadcast_id: str,
  170. subscription_id: str | None = None,
  171. client_logged: bool | None = None,
  172. ) -> str:
  173. return cls._fmt(
  174. GraphQLQueryID.INTERACTIVITY_SUB,
  175. input_params={
  176. "client_subscription_id": subscription_id or str(uuid4()),
  177. "broadcast_id": broadcast_id,
  178. },
  179. client_logged=client_logged,
  180. )
  181. @classmethod
  182. def live_realtime_comments(
  183. cls,
  184. broadcast_id: str,
  185. subscription_id: str | None = None,
  186. client_logged: bool | None = None,
  187. ) -> str:
  188. return cls._fmt(
  189. GraphQLQueryID.LIVE_REALTIME_COMMENTS,
  190. input_params={
  191. "client_subscription_id": subscription_id or str(uuid4()),
  192. "broadcast_id": broadcast_id,
  193. },
  194. client_logged=client_logged,
  195. )
  196. @classmethod
  197. def live_realtime_typing_indicator(
  198. cls,
  199. broadcast_id: str,
  200. subscription_id: str | None = None,
  201. client_logged: bool | None = None,
  202. ) -> str:
  203. return cls._fmt(
  204. GraphQLQueryID.LIVE_TYPING_INDICATOR,
  205. input_params={
  206. "client_subscription_id": subscription_id or str(uuid4()),
  207. "broadcast_id": broadcast_id,
  208. },
  209. client_logged=client_logged,
  210. )
  211. @classmethod
  212. def media_feedback(
  213. cls,
  214. feedback_id: str,
  215. subscription_id: str | None = None,
  216. client_logged: bool | None = None,
  217. ) -> str:
  218. return cls._fmt(
  219. GraphQLQueryID.MEDIA_FEEDBACK,
  220. input_params={
  221. "client_subscription_id": subscription_id or str(uuid4()),
  222. "feedback_id": feedback_id,
  223. },
  224. client_logged=client_logged,
  225. )
  226. @classmethod
  227. def react_native_ota_update(
  228. cls,
  229. build_number: str,
  230. subscription_id: str | None = None,
  231. client_logged: bool | None = None,
  232. ) -> str:
  233. return cls._fmt(
  234. GraphQLQueryID.REACT_NATIVE_OTA,
  235. input_params={
  236. "client_subscription_id": subscription_id or str(uuid4()),
  237. "build_number": build_number,
  238. },
  239. client_logged=client_logged,
  240. )
  241. @classmethod
  242. def video_call_co_watch_control(
  243. cls,
  244. video_call_id: str,
  245. subscription_id: str | None = None,
  246. client_logged: bool | None = None,
  247. ) -> str:
  248. return cls._fmt(
  249. GraphQLQueryID.VIDEO_CALL_CO_WATCH_CONTROL,
  250. input_params={
  251. "client_subscription_id": subscription_id or str(uuid4()),
  252. "video_call_id": video_call_id,
  253. },
  254. client_logged=client_logged,
  255. )
  256. @classmethod
  257. def video_call_in_call_alert(
  258. cls,
  259. video_call_id: str,
  260. subscription_id: str | None = None,
  261. client_logged: bool | None = None,
  262. ) -> str:
  263. return cls._fmt(
  264. GraphQLQueryID.VIDEO_CALL_IN_ALERT,
  265. input_params={
  266. "client_subscription_id": subscription_id or str(uuid4()),
  267. "video_call_id": video_call_id,
  268. },
  269. client_logged=client_logged,
  270. )
  271. @classmethod
  272. def video_call_prototype_publish(
  273. cls,
  274. video_call_id: str,
  275. subscription_id: str | None = None,
  276. client_logged: bool | None = None,
  277. ) -> str:
  278. return cls._fmt(
  279. GraphQLQueryID.VIDEO_CALL_PROTOTYPE_PUBLISH,
  280. input_params={
  281. "client_subscription_id": subscription_id or str(uuid4()),
  282. "video_call_id": video_call_id,
  283. },
  284. client_logged=client_logged,
  285. )
  286. @classmethod
  287. def zero_provision(
  288. cls,
  289. device_id: str,
  290. subscription_id: str | None = None,
  291. client_logged: bool | None = None,
  292. ) -> str:
  293. return cls._fmt(
  294. GraphQLQueryID.ZERO_PROVISION,
  295. input_params={
  296. "client_subscription_id": subscription_id or str(uuid4()),
  297. "device_id": device_id,
  298. },
  299. client_logged=client_logged,
  300. )
  301. _topic_map: dict[str, str] = {
  302. "/pp": "34", # unknown
  303. "/ig_sub_iris": "134",
  304. "/ig_sub_iris_response": "135",
  305. "/ig_message_sync": "146",
  306. "/ig_send_message": "132",
  307. "/ig_send_message_response": "133",
  308. "/ig_realtime_sub": "149",
  309. "/pubsub": "88",
  310. "/t_fs": "102", # Foreground state
  311. "/graphql": "9",
  312. "/t_region_hint": "150",
  313. "/mqtt_health_stats": "/mqtt_health_stats",
  314. "/ls_resp": "179",
  315. "/rs_req": "244",
  316. "/rs_resp": "245",
  317. "/t_rtc_log": "274",
  318. "/ig_large_scale_fire_and_forget_sync": "279",
  319. }
  320. _reverse_topic_map: dict[str, str] = {value: key for key, value in _topic_map.items()}
  321. class RealtimeTopic(Enum):
  322. SUB_IRIS = "/ig_sub_iris"
  323. SUB_IRIS_RESPONSE = "/ig_sub_iris_response"
  324. MESSAGE_SYNC = "/ig_message_sync"
  325. SEND_MESSAGE = "/ig_send_message"
  326. SEND_MESSAGE_RESPONSE = "/ig_send_message_response"
  327. REALTIME_SUB = "/ig_realtime_sub"
  328. PUBSUB = "/pubsub"
  329. FOREGROUND_STATE = "/t_fs"
  330. GRAPHQL = "/graphql"
  331. REGION_HINT = "/t_region_hint"
  332. MQTT_HEALTH_STATS = "/mqtt_health_stats"
  333. UNKNOWN_PP = "/pp"
  334. LIGHTSPEED_RESPONSE = "/ls_resp"
  335. RS_REQ = "/rs_req"
  336. RS_RESP = "/rs_resp"
  337. T_RTC_LOG = "/t_rtc_log"
  338. LARGE_SCALE_FIRE_AND_FORGET_SYNC = "/ig_large_scale_fire_and_forget_sync"
  339. @property
  340. def encoded(self) -> str:
  341. return _topic_map[self.value]
  342. @staticmethod
  343. def decode(val: str) -> RealtimeTopic:
  344. return RealtimeTopic(_reverse_topic_map[val])