account.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 typing import Optional, Type, TypeVar
  17. import base64
  18. import struct
  19. import time
  20. import json
  21. import io
  22. from ..types import CurrentUserResponse
  23. from .base import BaseAndroidAPI
  24. T = TypeVar('T')
  25. class AccountAPI(BaseAndroidAPI):
  26. async def current_user(self) -> CurrentUserResponse:
  27. url = (self.url / "api/v1/accounts/current_user/").with_query({"edit": "true"})
  28. resp = await self.http.get(url)
  29. return CurrentUserResponse.deserialize(await self.handle_response(resp))
  30. async def set_biography(self, text: str) -> CurrentUserResponse:
  31. # TODO entities?
  32. return await self.__command("set_biography", device_id=self.state.device.id, raw_text=text)
  33. async def set_profile_picture(self, upload_id: str) -> CurrentUserResponse:
  34. return await self.__command("change_profile_picture",
  35. use_fbuploader="true", upload_id=upload_id)
  36. async def remove_profile_picture(self) -> CurrentUserResponse:
  37. return await self.__command("remove_profile_picture")
  38. async def set_private(self, private: bool) -> CurrentUserResponse:
  39. return await self.__command("set_private" if private else "set_public")
  40. async def confirm_email(self, slug: str) -> CurrentUserResponse:
  41. # slug can contain slashes, but it shouldn't start or end with one
  42. return await self.__command(f"confirm_email/{slug}")
  43. async def send_recovery_flow_email(self, query: str):
  44. req = {
  45. "_csrftoken": self.state.cookies.csrf_token,
  46. "adid": "",
  47. "guid": self.state.device.uuid,
  48. "device_id": self.state.device.id,
  49. "query": query,
  50. }
  51. resp = await self.http.post(self.url / "api/v1/accounts/send_recovery_flow_email/",
  52. data=self.sign(req, filter_nulls=True))
  53. # TODO parse response content
  54. return await self.handle_response(resp)
  55. async def edit_profile(self, external_url: Optional[str] = None, gender: Optional[str] = None,
  56. phone_number: Optional[str] = None, username: Optional[str] = None,
  57. # TODO should there be a last_name?
  58. first_name: Optional[str] = None, biography: Optional[str] = None,
  59. email: Optional[str] = None) -> CurrentUserResponse:
  60. return await self.__command("edit_profile", device_id=self.state.device.id, email=email,
  61. external_url=external_url, first_name=first_name,
  62. username=username, phone_number=phone_number, gender=gender,
  63. biography=biography)
  64. async def __command(self, command: str, response_type: Type[T] = CurrentUserResponse,
  65. **kwargs: str) -> T:
  66. req = {
  67. "_csrftoken": self.state.cookies.csrf_token,
  68. "_uid": self.state.cookies.user_id,
  69. "_uuid": self.state.device.uuid,
  70. **kwargs,
  71. }
  72. resp = await self.http.post(self.url / f"api/v1/accounts/{command}",
  73. data=self.sign(req, filter_nulls=True))
  74. return response_type.deserialize(await self.handle_response(resp))
  75. async def read_msisdn_header(self, usage: str = "default"):
  76. req = {
  77. "mobile_subno_usage": usage,
  78. "device_id": self.state.device.uuid,
  79. }
  80. headers = {
  81. "X-DEVICE-ID": self.state.device.uuid,
  82. }
  83. resp = await self.http.post(self.url / "api/v1/accounts/read_msisdn_header/",
  84. data=self.sign(req), headers=headers)
  85. # TODO parse response content
  86. return await self.handle_response(resp)
  87. async def msisdn_header_bootstrap(self, usage: str = "default"):
  88. req = {
  89. "mobile_subno_usage": usage,
  90. "device_id": self.state.device.uuid,
  91. }
  92. resp = await self.http.post(self.url / "api/v1/accounts/msisdn_header_bootstrap/",
  93. data=self.sign(req))
  94. # TODO parse response content
  95. return await self.handle_response(resp)
  96. async def contact_point_prefill(self, usage: str = "default"):
  97. req = {
  98. "mobile_subno_usage": usage,
  99. "device_id": self.state.device.uuid,
  100. }
  101. resp = await self.http.post(self.url / "api/v1/accounts/contact_point_prefill/",
  102. data=self.sign(req))
  103. # TODO parse response content
  104. return await self.handle_response(resp)
  105. async def get_prefill_candidates(self):
  106. req = {
  107. "android_device_id": self.state.device.id,
  108. "usages": json.dumps(["account_recovery_omnibox"]),
  109. "device_id": self.state.device.uuid,
  110. }
  111. resp = await self.http.post(self.url / "api/v1/accounts/contact_point_prefill/",
  112. data=self.sign(req))
  113. # TODO parse response content
  114. return await self.handle_response(resp)
  115. async def process_contact_point_signals(self):
  116. req = {
  117. "phone_id": self.state.device.phone_id,
  118. "_csrftoken": self.state.cookies.csrf_token,
  119. "_uid": self.state.cookies.user_id,
  120. "device_id": self.state.device.uuid,
  121. "_uuid": self.state.device.uuid,
  122. "google_tokens": json.dumps([]),
  123. }
  124. resp = await self.http.post(self.url / "api/v1/accounts/process_contact_point_signals/",
  125. data=self.sign(req))
  126. # TODO parse response content
  127. return await self.handle_response(resp)