auth.py 5.3 KB

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