auth.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # mautrix-twitter - A Matrix-Twitter DM 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 Tuple, TYPE_CHECKING
  17. from mautrix.bridge.commands import HelpSection, command_handler
  18. from mauigpapi.state import AndroidState
  19. from mauigpapi.http import AndroidAPI
  20. from mauigpapi.errors import (IGLoginTwoFactorRequiredError, IGLoginBadPasswordError,
  21. IGLoginInvalidUserError, IGBad2FACodeError)
  22. from mauigpapi.types import BaseResponseUser
  23. from .typehint import CommandEvent
  24. if TYPE_CHECKING:
  25. from ..user import User
  26. SECTION_AUTH = HelpSection("Authentication", 10, "")
  27. async def get_login_state(user: 'User', username: str) -> Tuple[AndroidAPI, AndroidState]:
  28. if user.command_status and user.command_status["action"] == "Login":
  29. api: AndroidAPI = user.command_status["api"]
  30. state: AndroidState = user.command_status["state"]
  31. else:
  32. state = AndroidState()
  33. state.device.generate(username)
  34. api = AndroidAPI(state)
  35. await api.simulate_pre_login_flow()
  36. user.command_status = {
  37. "action": "Login",
  38. "state": state,
  39. "api": api,
  40. }
  41. return api, state
  42. @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
  43. help_text="Log in to Instagram", help_args="<_username_> <_password_>")
  44. async def login(evt: CommandEvent) -> None:
  45. if await evt.sender.is_logged_in():
  46. await evt.reply("You're already logged in")
  47. return
  48. elif len(evt.args) < 2:
  49. await evt.reply("**Usage:** `$cmdprefix+sp login <username> <password>`")
  50. return
  51. username = evt.args[0]
  52. password = " ".join(evt.args[1:])
  53. api, state = await get_login_state(evt.sender, username)
  54. try:
  55. resp = await api.login(username, password)
  56. except IGLoginTwoFactorRequiredError as e:
  57. tfa_info = e.body.two_factor_info
  58. msg = "Username and password accepted, but you have two-factor authentication enabled.\n"
  59. if tfa_info.totp_two_factor_on:
  60. msg += "Send the code from your authenticator app here."
  61. elif tfa_info.sms_two_factor_on:
  62. msg += f"Send the code sent to {tfa_info.obfuscated_phone_number} here."
  63. else:
  64. msg += ("Unfortunately, none of your two-factor authentication methods are currently "
  65. "supported by the bridge.")
  66. return
  67. evt.sender.command_status = {
  68. **evt.sender.command_status,
  69. "next": enter_login_2fa,
  70. "username": tfa_info.username,
  71. "is_totp": tfa_info.totp_two_factor_on,
  72. "2fa_identifier": tfa_info.two_factor_identifier,
  73. }
  74. await evt.reply(msg)
  75. except IGLoginInvalidUserError:
  76. await evt.reply("Invalid username")
  77. except IGLoginBadPasswordError:
  78. await evt.reply("Incorrect password")
  79. else:
  80. await _post_login(evt, api, state, resp.logged_in_user)
  81. async def enter_login_2fa(evt: CommandEvent) -> None:
  82. api: AndroidAPI = evt.sender.command_status["api"]
  83. state: AndroidState = evt.sender.command_status["state"]
  84. identifier = evt.sender.command_status["2fa_identifier"]
  85. username = evt.sender.command_status["username"]
  86. is_totp = evt.sender.command_status["is_totp"]
  87. try:
  88. resp = await api.two_factor_login(username, code="".join(evt.args), identifier=identifier,
  89. is_totp=is_totp)
  90. except IGBad2FACodeError:
  91. await evt.reply("Invalid 2-factor authentication code. Please try again "
  92. "or use `$cmdprefix+sp cancel` to cancel.")
  93. except Exception as e:
  94. await evt.reply(f"Failed to log in: {e}")
  95. evt.log.exception("Failed to log in")
  96. evt.sender.command_status = None
  97. else:
  98. evt.sender.command_status = None
  99. await _post_login(evt, api, state, resp.logged_in_user)
  100. async def _post_login(evt: CommandEvent, api: AndroidAPI, state: AndroidState,
  101. user: BaseResponseUser) -> None:
  102. await api.simulate_post_login_flow()
  103. evt.sender.state = state
  104. pl = state.device.payload
  105. manufacturer, model = pl["manufacturer"], pl["model"]
  106. await evt.reply(f"Successfully logged in as {user.full_name} ([@{user.username}]"
  107. f"(https://instagram.com/{user.username}), user ID: {user.pk}).\n\n"
  108. f"The bridge will show up on Instagram as {manufacturer} {model}.")
  109. await evt.sender.try_connect()
  110. @command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Disconnect the bridge from"
  111. "your Instagram account")
  112. async def logout(evt: CommandEvent) -> None:
  113. await evt.sender.logout()
  114. await evt.reply("Successfully logged out")