Tulir Asokan 4 жил өмнө
parent
commit
47932ad16a

+ 1 - 1
mauigpapi/errors/__init__.py

@@ -4,5 +4,5 @@ from .state import IGUserIDNotFoundError, IGCookieNotFoundError, IGNoCheckpointE
 from .response import (IGResponseError, IGActionSpamError, IGNotFoundError, IGRateLimitError,
 from .response import (IGResponseError, IGActionSpamError, IGNotFoundError, IGRateLimitError,
                        IGCheckpointError, IGUserHasLoggedOutError, IGLoginRequiredError,
                        IGCheckpointError, IGUserHasLoggedOutError, IGLoginRequiredError,
                        IGPrivateUserError, IGSentryBlockError, IGInactiveUserError, IGLoginError,
                        IGPrivateUserError, IGSentryBlockError, IGInactiveUserError, IGLoginError,
-                       IGLoginTwoFactorRequiredError, IGLoginBadPasswordError,
+                       IGLoginTwoFactorRequiredError, IGLoginBadPasswordError, IGBad2FACodeError,
                        IGLoginInvalidUserError, IGNotLoggedInError)
                        IGLoginInvalidUserError, IGNotLoggedInError)

+ 5 - 1
mauigpapi/errors/response.py

@@ -32,7 +32,7 @@ class IGResponseError(IGError):
         if "message" in json:
         if "message" in json:
             message = json["message"]
             message = json["message"]
         type_hint = get_type_hints(type(self)).get("body", JSON)
         type_hint = get_type_hints(type(self)).get("body", JSON)
-        if type_hint is not JSON and isinstance(type_hint, Serializable):
+        if type_hint is not JSON and issubclass(type_hint, Serializable):
             self.body = type_hint.deserialize(json)
             self.body = type_hint.deserialize(json)
         super().__init__(f"{prefix}: {self._message_override or message}")
         super().__init__(f"{prefix}: {self._message_override or message}")
 
 
@@ -103,3 +103,7 @@ class IGLoginBadPasswordError(IGLoginError):
 
 
 class IGLoginInvalidUserError(IGLoginError):
 class IGLoginInvalidUserError(IGLoginError):
     pass
     pass
+
+
+class IGBad2FACodeError(IGResponseError):
+    pass

+ 3 - 2
mauigpapi/http/base.py

@@ -14,7 +14,6 @@
 # You should have received a copy of the GNU Affero General Public License
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 from typing import Optional, Dict, Any, TypeVar, Type
 from typing import Optional, Dict, Any, TypeVar, Type
-import asyncio
 import random
 import random
 import time
 import time
 import json
 import json
@@ -26,7 +25,7 @@ from mautrix.types import JSON, Serializable
 from ..state import AndroidState
 from ..state import AndroidState
 from ..errors import (IGActionSpamError, IGNotFoundError, IGRateLimitError, IGCheckpointError,
 from ..errors import (IGActionSpamError, IGNotFoundError, IGRateLimitError, IGCheckpointError,
                       IGUserHasLoggedOutError, IGLoginRequiredError, IGPrivateUserError,
                       IGUserHasLoggedOutError, IGLoginRequiredError, IGPrivateUserError,
-                      IGSentryBlockError, IGInactiveUserError, IGResponseError,
+                      IGSentryBlockError, IGInactiveUserError, IGResponseError, IGBad2FACodeError,
                       IGLoginBadPasswordError, IGLoginInvalidUserError,
                       IGLoginBadPasswordError, IGLoginInvalidUserError,
                       IGLoginTwoFactorRequiredError)
                       IGLoginTwoFactorRequiredError)
 
 
@@ -169,6 +168,8 @@ class BaseAndroidAPI:
             raise IGLoginBadPasswordError(resp, data)
             raise IGLoginBadPasswordError(resp, data)
         elif error_type == "invalid_user":
         elif error_type == "invalid_user":
             raise IGLoginInvalidUserError(resp, data)
             raise IGLoginInvalidUserError(resp, data)
+        elif error_type == "sms_code_validation_code_invalid":
+            raise IGBad2FACodeError(resp, data)
 
 
         raise IGResponseError(resp, data)
         raise IGResponseError(resp, data)
 
 

+ 4 - 4
mauigpapi/http/login.py

@@ -67,8 +67,8 @@ class LoginAPI(BaseAndroidAPI):
                                         response_type=LoginResponse)
                                         response_type=LoginResponse)
 
 
     async def two_factor_login(self, username: str, code: str, identifier: str,
     async def two_factor_login(self, username: str, code: str, identifier: str,
-                               trust_device: bool = True, method: Optional[str] = "1"
-                               ) -> LoginResponseUser:
+                               trust_device: bool = True, is_totp: bool = True,
+                               ) -> LoginResponse:
         req = {
         req = {
             "verification_code": code,
             "verification_code": code,
             "_csrftoken": self.state.cookies.csrf_token,
             "_csrftoken": self.state.cookies.csrf_token,
@@ -77,10 +77,10 @@ class LoginAPI(BaseAndroidAPI):
             "trust_this_device": "1" if trust_device else "0",
             "trust_this_device": "1" if trust_device else "0",
             "guid": self.state.device.uuid,
             "guid": self.state.device.uuid,
             "device_id": self.state.device.id,
             "device_id": self.state.device.id,
-            "verification_method": method,
+            "verification_method": "0" if is_totp else "1",
         }
         }
         return await self.std_http_post("/api/v1/accounts/two_factor_login/", data=req,
         return await self.std_http_post("/api/v1/accounts/two_factor_login/", data=req,
-                                        response_type=LoginResponseUser)
+                                        response_type=LoginResponse)
 
 
     async def logout(self, one_tap_app_login: Optional[bool] = None) -> LogoutResponse:
     async def logout(self, one_tap_app_login: Optional[bool] = None) -> LogoutResponse:
         req = {
         req = {

+ 0 - 13
mauigpapi/mqtt/__init__.py

@@ -1,16 +1,3 @@
 from .subscription import SkywalkerSubscription, GraphQLSubscription
 from .subscription import SkywalkerSubscription, GraphQLSubscription
-from .types import (RealtimeTopic, ThreadItemType, ThreadAction, ReactionStatus, TypingStatus,
-                    CommandResponse, CommandResponsePayload, Operation, IrisPayload, ImageVersions,
-                    IrisPayloadData, ViewMode, CreativeConfig, CreateModeAttribution, ImageVersion,
-                    VideoVersion, MediaType,RegularMediaItem, FriendshipStatus, MinimalUser, User,
-                    Caption, MediaShareItem, ReplayableMediaItem, VisualMedia, AudioInfo,
-                    VoiceMediaData, VoiceMediaItem, AnimatedMediaItem, AnimatedMediaImage,
-                    AnimatedMediaImages, MessageSyncEvent, MessageSyncMessage, PubsubPayloadData,
-                    PubsubBasePayload, PubsubPublishMetadata, PubsubPayload, PubsubEvent,
-                    ActivityIndicatorData, AppPresenceEventPayload, AppPresenceEvent,
-                    RealtimeZeroProvisionPayload, ZeroProductProvisioningEvent, RealtimeDirectData,
-                    RealtimeDirectEvent, ClientConfigUpdatePayload, ClientConfigUpdateEvent,
-                    LiveVideoCommentPayload, LiveVideoCommentUser, LiveVideoCommentEvent,
-                    LiveVideoComment, LiveVideoSystemComment)
 from .events import Connect, Disconnect
 from .events import Connect, Disconnect
 from .conn import AndroidMQTT
 from .conn import AndroidMQTT

+ 9 - 5
mauigpapi/types/error.py

@@ -71,7 +71,7 @@ class LoginPhoneVerificationSettings(SerializableAttrs['LoginPhoneVerificationSe
     max_sms_count: int
     max_sms_count: int
     resend_sms_delay_sec: int
     resend_sms_delay_sec: int
     robocall_count_down_time_sec: int
     robocall_count_down_time_sec: int
-    robocall_max_after_sms: bool
+    robocall_after_max_sms: bool
 
 
 
 
 @dataclass
 @dataclass
@@ -84,6 +84,10 @@ class LoginTwoFactorInfo(SerializableAttrs['LoginTwoFactorInfo']):
     show_messenger_code_option: bool
     show_messenger_code_option: bool
     show_new_login_screen: bool
     show_new_login_screen: bool
     show_trusted_device_option: bool
     show_trusted_device_option: bool
+    should_opt_in_trusted_device_option: bool
+    pending_trusted_notification: bool
+    # TODO type
+    # sms_not_allowed_reason: Any
     phone_verification_settings: Optional[LoginPhoneVerificationSettings] = None
     phone_verification_settings: Optional[LoginPhoneVerificationSettings] = None
 
 
 
 
@@ -92,9 +96,9 @@ class LoginErrorResponse(SerializableAttrs['LoginErrorResponse']):
     message: str
     message: str
     status: str
     status: str
     error_type: str
     error_type: str
-    error_title: str
-    buttons: List[LoginErrorResponseButton]
-    invalid_credentials: bool
-    two_factor_required: bool
+    error_title: Optional[str] = None
+    buttons: Optional[List[LoginErrorResponseButton]] = None
+    invalid_credentials: Optional[bool] = None
+    two_factor_required: Optional[bool] = None
     two_factor_info: Optional[LoginTwoFactorInfo] = None
     two_factor_info: Optional[LoginTwoFactorInfo] = None
     phone_verification_settings: Optional[LoginPhoneVerificationSettings] = None
     phone_verification_settings: Optional[LoginPhoneVerificationSettings] = None

+ 2 - 2
mauigpapi/types/thread.py

@@ -40,7 +40,7 @@ class ThreadUserLastSeenAt(SerializableAttrs['UserLastSeenAt']):
     shh_seen_state: Dict[str, Any]
     shh_seen_state: Dict[str, Any]
 
 
 
 
-@dataclass
+@dataclass(kw_only=True)
 class Thread(SerializableAttrs['Thread']):
 class Thread(SerializableAttrs['Thread']):
     thread_id: str
     thread_id: str
     thread_v2_id: str
     thread_v2_id: str
@@ -80,7 +80,7 @@ class Thread(SerializableAttrs['Thread']):
 
 
     newest_cursor: str
     newest_cursor: str
     oldest_cursor: str
     oldest_cursor: str
-    next_cursor: str
+    next_cursor: Optional[str] = None
     prev_cursor: str
     prev_cursor: str
     last_permanent_item: ThreadItem
     last_permanent_item: ThreadItem
     items: List[ThreadItem]
     items: List[ThreadItem]

+ 46 - 31
mautrix_instagram/commands/auth.py

@@ -13,12 +13,13 @@
 #
 #
 # You should have received a copy of the GNU Affero General Public License
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from mautrix.bridge.commands import HelpSection, command_handler
 from mauigpapi.state import AndroidState
 from mauigpapi.state import AndroidState
 from mauigpapi.http import AndroidAPI
 from mauigpapi.http import AndroidAPI
 from mauigpapi.errors import (IGLoginTwoFactorRequiredError, IGLoginBadPasswordError,
 from mauigpapi.errors import (IGLoginTwoFactorRequiredError, IGLoginBadPasswordError,
-                              IGLoginInvalidUserError, IGLoginError)
+                              IGLoginInvalidUserError, IGBad2FACodeError)
+from mauigpapi.types import BaseResponseUser
 
 
-from mautrix.bridge.commands import HelpSection, command_handler
 from .typehint import CommandEvent
 from .typehint import CommandEvent
 
 
 SECTION_AUTH = HelpSection("Authentication", 10, "")
 SECTION_AUTH = HelpSection("Authentication", 10, "")
@@ -35,47 +36,48 @@ async def login(evt: CommandEvent) -> None:
         return
         return
     username = evt.args[0]
     username = evt.args[0]
     password = " ".join(evt.args[1:])
     password = " ".join(evt.args[1:])
-    state = AndroidState()
-    state.device.generate(username)
-    api = AndroidAPI(state)
-    await api.simulate_pre_login_flow()
+    if evt.sender.command_status and evt.sender.command_status["action"] == "Login":
+        api: AndroidAPI = evt.sender.command_status["api"]
+        state: AndroidState = evt.sender.command_status["state"]
+    else:
+        evt.log.trace(f"Generating new device for {username}")
+        state = AndroidState()
+        state.device.generate(username)
+        api = AndroidAPI(state)
+        await api.simulate_pre_login_flow()
+        evt.sender.command_status = {
+            "action": "Login",
+            "room_id": evt.room_id,
+            "state": state,
+            "api": api,
+        }
     try:
     try:
         resp = await api.login(username, password)
         resp = await api.login(username, password)
     except IGLoginTwoFactorRequiredError as e:
     except IGLoginTwoFactorRequiredError as e:
         tfa_info = e.body.two_factor_info
         tfa_info = e.body.two_factor_info
         msg = "Username and password accepted, but you have two-factor authentication enabled.\n"
         msg = "Username and password accepted, but you have two-factor authentication enabled.\n"
-        if tfa_info.sms_two_factor_on:
-            if tfa_info.totp_two_factor_on:
-                msg += (f"Send the code from your authenticator app "
-                        f"or one sent to {tfa_info.obfuscated_phone_number} here.")
-            else:
-                msg += f"Send the code sent to {tfa_info.obfuscated_phone_number} here."
-        elif tfa_info.totp_two_factor_on:
+        if tfa_info.totp_two_factor_on:
             msg += "Send the code from your authenticator app here."
             msg += "Send the code from your authenticator app here."
+        elif tfa_info.sms_two_factor_on:
+            msg += f"Send the code sent to {tfa_info.obfuscated_phone_number} here."
         else:
         else:
             msg += ("Unfortunately, none of your two-factor authentication methods are currently "
             msg += ("Unfortunately, none of your two-factor authentication methods are currently "
                     "supported by the bridge.")
                     "supported by the bridge.")
             return
             return
         evt.sender.command_status = {
         evt.sender.command_status = {
-            "action": "Login",
-            "room_id": evt.room_id,
+            **evt.sender.command_status,
             "next": enter_login_2fa,
             "next": enter_login_2fa,
-
             "username": tfa_info.username,
             "username": tfa_info.username,
+            "is_totp": tfa_info.totp_two_factor_on,
             "2fa_identifier": tfa_info.two_factor_identifier,
             "2fa_identifier": tfa_info.two_factor_identifier,
-            "state": state,
-            "api": api,
         }
         }
+        await evt.reply(msg)
     except IGLoginInvalidUserError:
     except IGLoginInvalidUserError:
         await evt.reply("Invalid username")
         await evt.reply("Invalid username")
     except IGLoginBadPasswordError:
     except IGLoginBadPasswordError:
         await evt.reply("Incorrect password")
         await evt.reply("Incorrect password")
     else:
     else:
-        evt.sender.state = state
-        user = resp.logged_in_user
-        await evt.reply(f"Successfully logged in as {user.full_name} ([@{user.username}]"
-                        f"(https://instagram.com/{user.username}), user ID: {user.pk})")
-        await evt.sender.try_connect()
+        await _post_login(evt, api, state, resp.logged_in_user)
 
 
 
 
 async def enter_login_2fa(evt: CommandEvent) -> None:
 async def enter_login_2fa(evt: CommandEvent) -> None:
@@ -83,19 +85,32 @@ async def enter_login_2fa(evt: CommandEvent) -> None:
     state: AndroidState = evt.sender.command_status["state"]
     state: AndroidState = evt.sender.command_status["state"]
     identifier = evt.sender.command_status["2fa_identifier"]
     identifier = evt.sender.command_status["2fa_identifier"]
     username = evt.sender.command_status["username"]
     username = evt.sender.command_status["username"]
-    evt.sender.command_status = None
+    is_totp = evt.sender.command_status["is_totp"]
     try:
     try:
-        user = await api.two_factor_login(username, code="".join(evt.args), identifier=identifier)
-    except IGLoginError as e:
-        await evt.reply(f"Failed to log in: {e.body.message}")
+        resp = await api.two_factor_login(username, code="".join(evt.args), identifier=identifier,
+                                          is_totp=is_totp)
+    except IGBad2FACodeError:
+        await evt.reply("Invalid 2-factor authentication code. Please try again "
+                        "or use `$cmdprefix+sp cancel` to cancel.")
     except Exception as e:
     except Exception as e:
         await evt.reply(f"Failed to log in: {e}")
         await evt.reply(f"Failed to log in: {e}")
         evt.log.exception("Failed to log in")
         evt.log.exception("Failed to log in")
+        evt.sender.command_status = None
     else:
     else:
-        evt.sender.state = state
-        await evt.reply(f"Successfully logged in as {user.full_name} ([@{user.username}]"
-                        f"(https://instagram.com/{user.username}), user ID: {user.pk})")
-        await evt.sender.try_connect()
+        evt.sender.command_status = None
+        await _post_login(evt, api, state, resp.logged_in_user)
+
+
+async def _post_login(evt: CommandEvent, api: AndroidAPI, state: AndroidState,
+                      user: BaseResponseUser) -> None:
+    await api.simulate_post_login_flow()
+    evt.sender.state = state
+    pl = state.device.payload
+    manufacturer, model = pl["manufacturer"], pl["model"]
+    await evt.reply(f"Successfully logged in as {user.full_name} ([@{user.username}]"
+                    f"(https://instagram.com/{user.username}), user ID: {user.pk}).\n\n"
+                    f"The bridge will show up on Instagram as {manufacturer} {model}.")
+    await evt.sender.try_connect()
 
 
 
 
 @command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Disconnect the bridge from"
 @command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Disconnect the bridge from"

+ 8 - 5
mautrix_instagram/db/puppet.py

@@ -44,13 +44,17 @@ class Puppet:
     next_batch: Optional[SyncToken]
     next_batch: Optional[SyncToken]
     base_url: Optional[URL]
     base_url: Optional[URL]
 
 
+    @property
+    def _base_url_str(self) -> Optional[str]:
+        return str(self.base_url) if self.base_url else None
+
     async def insert(self) -> None:
     async def insert(self) -> None:
         q = ("INSERT INTO puppet (pk, name, username, photo_id, photo_mxc, name_set, avatar_set,"
         q = ("INSERT INTO puppet (pk, name, username, photo_id, photo_mxc, name_set, avatar_set,"
              "                    is_registered, custom_mxid, access_token, next_batch, base_url) "
              "                    is_registered, custom_mxid, access_token, next_batch, base_url) "
              "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)")
              "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)")
         await self.db.execute(q, self.pk, self.name, self.username, self.photo_id, self.photo_mxc,
         await self.db.execute(q, self.pk, self.name, self.username, self.photo_id, self.photo_mxc,
-                              self.is_registered, self.custom_mxid, self.access_token,
-                              self.next_batch, str(self.base_url) if self.base_url else None)
+                              self.name_set, self.avatar_set, self.is_registered, self.custom_mxid,
+                              self.access_token, self.next_batch, self._base_url_str)
 
 
     async def update(self) -> None:
     async def update(self) -> None:
         q = ("UPDATE puppet SET name=$2, username=$3, photo_id=$4, photo_mxc=$5, name_set=$6,"
         q = ("UPDATE puppet SET name=$2, username=$3, photo_id=$4, photo_mxc=$5, name_set=$6,"
@@ -59,8 +63,7 @@ class Puppet:
              "WHERE pk=$1")
              "WHERE pk=$1")
         await self.db.execute(q, self.pk, self.name, self.username, self.photo_id, self.photo_mxc,
         await self.db.execute(q, self.pk, self.name, self.username, self.photo_id, self.photo_mxc,
                               self.name_set, self.avatar_set, self.is_registered, self.custom_mxid,
                               self.name_set, self.avatar_set, self.is_registered, self.custom_mxid,
-                              self.access_token, self.next_batch,
-                              str(self.base_url) if self.base_url else None)
+                              self.access_token, self.next_batch, self._base_url_str)
 
 
     @classmethod
     @classmethod
     def _from_row(cls, row: asyncpg.Record) -> 'Puppet':
     def _from_row(cls, row: asyncpg.Record) -> 'Puppet':
@@ -73,7 +76,7 @@ class Puppet:
     async def get_by_pk(cls, pk: int) -> Optional['Puppet']:
     async def get_by_pk(cls, pk: int) -> Optional['Puppet']:
         q = ("SELECT pk, name, username, photo_id, photo_mxc, name_set, avatar_set, is_registered,"
         q = ("SELECT pk, name, username, photo_id, photo_mxc, name_set, avatar_set, is_registered,"
              "       custom_mxid, access_token, next_batch, base_url "
              "       custom_mxid, access_token, next_batch, base_url "
-             "FROM puppet WHERE igpk=$1")
+             "FROM puppet WHERE pk=$1")
         row = await cls.db.fetchrow(q, pk)
         row = await cls.db.fetchrow(q, pk)
         if not row:
         if not row:
             return None
             return None

+ 2 - 2
mautrix_instagram/db/reaction.py

@@ -40,10 +40,10 @@ class Reaction:
         await self.db.execute(q, self.mxid, self.mx_room, self.ig_item_id, self.ig_receiver,
         await self.db.execute(q, self.mxid, self.mx_room, self.ig_item_id, self.ig_receiver,
                               self.ig_sender, self.reaction)
                               self.ig_sender, self.reaction)
 
 
-    async def edit(self, mx_room: RoomID, mxid: EventID, reaction: ReactionKey) -> None:
+    async def edit(self, mx_room: RoomID, mxid: EventID, reaction: str) -> None:
         await self.db.execute("UPDATE reaction SET mxid=$1, mx_room=$2, reaction=$3 "
         await self.db.execute("UPDATE reaction SET mxid=$1, mx_room=$2, reaction=$3 "
                               "WHERE ig_item_id=$4 AND ig_receiver=$5 AND ig_sender=$6",
                               "WHERE ig_item_id=$4 AND ig_receiver=$5 AND ig_sender=$6",
-                              mxid, mx_room, reaction.value, self.ig_item_id, self.ig_receiver,
+                              mxid, mx_room, reaction, self.ig_item_id, self.ig_receiver,
                               self.ig_sender)
                               self.ig_sender)
 
 
     async def delete(self) -> None:
     async def delete(self) -> None:

+ 3 - 3
mautrix_instagram/db/user.py

@@ -38,12 +38,12 @@ class User:
         q = ('INSERT INTO "user" (mxid, igpk, state, notice_room) '
         q = ('INSERT INTO "user" (mxid, igpk, state, notice_room) '
              'VALUES ($1, $2, $3, $4)')
              'VALUES ($1, $2, $3, $4)')
         await self.db.execute(q, self.mxid, self.igpk,
         await self.db.execute(q, self.mxid, self.igpk,
-                              self.state.serialize() if self.state else None, self.notice_room)
+                              self.state.json() if self.state else None, self.notice_room)
 
 
     async def update(self) -> None:
     async def update(self) -> None:
         await self.db.execute('UPDATE "user" SET igpk=$2, state=$3, notice_room=$4 '
         await self.db.execute('UPDATE "user" SET igpk=$2, state=$3, notice_room=$4 '
                               'WHERE mxid=$1', self.mxid, self.igpk,
                               'WHERE mxid=$1', self.mxid, self.igpk,
-                              self.state.serialize() if self.state else None, self.notice_room)
+                              self.state.json() if self.state else None, self.notice_room)
 
 
     @classmethod
     @classmethod
     def _from_row(cls, row: asyncpg.Record) -> 'User':
     def _from_row(cls, row: asyncpg.Record) -> 'User':
@@ -69,7 +69,7 @@ class User:
 
 
     @classmethod
     @classmethod
     async def all_logged_in(cls) -> List['User']:
     async def all_logged_in(cls) -> List['User']:
-        q = ("SELECT mxid, igp, state, notice_room "
+        q = ("SELECT mxid, igpk, state, notice_room "
              'FROM "user" WHERE igpk IS NOT NULL AND state IS NOT NULL')
              'FROM "user" WHERE igpk IS NOT NULL AND state IS NOT NULL')
         rows = await cls.db.fetch(q)
         rows = await cls.db.fetch(q)
         return [cls._from_row(row) for row in rows]
         return [cls._from_row(row) for row in rows]

+ 4 - 3
mautrix_instagram/portal.py

@@ -117,7 +117,7 @@ class Portal(DBPortal, BasePortal):
             self.log.debug(f"_upsert_reaction redacting {existing.mxid} and inserting {mxid}"
             self.log.debug(f"_upsert_reaction redacting {existing.mxid} and inserting {mxid}"
                            f" (message: {message.mxid})")
                            f" (message: {message.mxid})")
             await intent.redact(existing.mx_room, existing.mxid)
             await intent.redact(existing.mx_room, existing.mxid)
-            await existing.edit(reaction=reaction, mxid=mxid, mx_room=message.mx_room)
+            await existing.edit(emoji=reaction, mxid=mxid, mx_room=message.mx_room)
         else:
         else:
             self.log.debug(f"_upsert_reaction inserting {mxid} (message: {message.mxid})")
             self.log.debug(f"_upsert_reaction inserting {mxid} (message: {message.mxid})")
             await DBReaction(mxid=mxid, mx_room=message.mx_room, ig_item_id=message.item_id,
             await DBReaction(mxid=mxid, mx_room=message.mx_room, ig_item_id=message.item_id,
@@ -240,8 +240,8 @@ class Portal(DBPortal, BasePortal):
             event_id = None
             event_id = None
             if item.text:
             if item.text:
                 content = TextMessageEventContent(msgtype=MessageType.TEXT, body=item.text)
                 content = TextMessageEventContent(msgtype=MessageType.TEXT, body=item.text)
-                # TODO timestamp is probably not milliseconds
-                event_id = await self._send_message(intent, content, timestamp=item.timestamp)
+                event_id = await self._send_message(intent, content,
+                                                    timestamp=item.timestamp // 1000)
             # TODO handle attachments and reactions
             # TODO handle attachments and reactions
             if event_id:
             if event_id:
                 await DBMessage(mxid=event_id, mx_room=self.mxid, item_id=item.item_id,
                 await DBMessage(mxid=event_id, mx_room=self.mxid, item_id=item.item_id,
@@ -275,6 +275,7 @@ class Portal(DBPortal, BasePortal):
         # Make sure puppets who should be here are here
         # Make sure puppets who should be here are here
         for user in users:
         for user in users:
             puppet = await p.Puppet.get_by_pk(user.pk)
             puppet = await p.Puppet.get_by_pk(user.pk)
+            await puppet.update_info(user)
             await puppet.intent_for(self).ensure_joined(self.mxid)
             await puppet.intent_for(self).ensure_joined(self.mxid)
 
 
         # Kick puppets who shouldn't be here
         # Kick puppets who shouldn't be here

+ 9 - 6
mautrix_instagram/puppet.py

@@ -45,12 +45,13 @@ class Puppet(DBPuppet, BasePuppet):
 
 
     def __init__(self, pk: int, name: Optional[str] = None, username: Optional[str] = None,
     def __init__(self, pk: int, name: Optional[str] = None, username: Optional[str] = None,
                  photo_id: Optional[str] = None, photo_mxc: Optional[ContentURI] = None,
                  photo_id: Optional[str] = None, photo_mxc: Optional[ContentURI] = None,
-                 is_registered: bool = False, custom_mxid: Optional[UserID] = None,
-                 access_token: Optional[str] = None, next_batch: Optional[SyncToken] = None,
-                 base_url: Optional[URL] = None) -> None:
-        super().__init__(pk=pk, name=name, username=username, photo_id=photo_id, photo_mxc=photo_mxc,
-                         is_registered=is_registered, custom_mxid=custom_mxid,
-                         access_token=access_token, next_batch=next_batch, base_url=base_url)
+                 name_set: bool = False, avatar_set: bool = False, is_registered: bool = False,
+                 custom_mxid: Optional[UserID] = None, access_token: Optional[str] = None,
+                 next_batch: Optional[SyncToken] = None, base_url: Optional[URL] = None) -> None:
+        super().__init__(pk=pk, name=name, username=username, photo_id=photo_id, name_set=name_set,
+                         photo_mxc=photo_mxc, avatar_set=avatar_set, is_registered=is_registered,
+                         custom_mxid=custom_mxid, access_token=access_token, next_batch=next_batch,
+                         base_url=base_url)
         self.log = self.log.getChild(str(pk))
         self.log = self.log.getChild(str(pk))
 
 
         self.default_mxid = self.get_mxid_from_id(pk)
         self.default_mxid = self.get_mxid_from_id(pk)
@@ -110,6 +111,8 @@ class Puppet(DBPuppet, BasePuppet):
         if info.profile_pic_id != self.photo_id or not self.avatar_set:
         if info.profile_pic_id != self.photo_id or not self.avatar_set:
             self.photo_id = info.profile_pic_id
             self.photo_id = info.profile_pic_id
             if info.profile_pic_id:
             if info.profile_pic_id:
+                # TODO if info.has_anonymous_profile_picture, we might need auth to get it
+                #      ...and we should probably download it with the device headers anyway
                 async with ClientSession() as sess, sess.get(info.profile_pic_url) as resp:
                 async with ClientSession() as sess, sess.get(info.profile_pic_url) as resp:
                     content_type = resp.headers["Content-Type"]
                     content_type = resp.headers["Content-Type"]
                     resp_data = await resp.read()
                     resp_data = await resp.read()

+ 5 - 0
mautrix_instagram/user.py

@@ -76,6 +76,7 @@ class User(DBUser, BaseUser):
         self._metric_value = defaultdict(lambda: False)
         self._metric_value = defaultdict(lambda: False)
         self._is_logged_in = False
         self._is_logged_in = False
         self._listen_task = None
         self._listen_task = None
+        self.command_status = None
 
 
     @classmethod
     @classmethod
     def init_cls(cls, bridge: 'InstagramBridge') -> AsyncIterable[Awaitable[None]]:
     def init_cls(cls, bridge: 'InstagramBridge') -> AsyncIterable[Awaitable[None]]:
@@ -107,6 +108,7 @@ class User(DBUser, BaseUser):
             await self.send_bridge_notice("You have been logged out of Instagram")
             await self.send_bridge_notice("You have been logged out of Instagram")
             return
             return
         self.client = client
         self.client = client
+        self._is_logged_in = True
         self.igpk = resp.user.pk
         self.igpk = resp.user.pk
         self.username = resp.user.username
         self.username = resp.user.username
         self._track_metric(METRIC_LOGGED_IN, True)
         self._track_metric(METRIC_LOGGED_IN, True)
@@ -234,6 +236,9 @@ class User(DBUser, BaseUser):
 
 
     @async_time(METRIC_MESSAGE)
     @async_time(METRIC_MESSAGE)
     async def handle_message(self, evt: MessageSyncEvent) -> None:
     async def handle_message(self, evt: MessageSyncEvent) -> None:
+        # We don't care about messages with no sender
+        if not evt.message.user_id:
+            return
         portal = await po.Portal.get_by_thread_id(evt.message.thread_id, receiver=self.igpk)
         portal = await po.Portal.get_by_thread_id(evt.message.thread_id, receiver=self.igpk)
         if not portal.mxid:
         if not portal.mxid:
             # TODO try to find the thread?
             # TODO try to find the thread?

+ 1 - 0
mautrix_instagram/util/__init__.py

@@ -0,0 +1 @@
+from .color_log import ColorFormatter

+ 30 - 0
mautrix_instagram/util/color_log.py

@@ -0,0 +1,30 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from mautrix.util.logging.color import (ColorFormatter as BaseColorFormatter, PREFIX, MXID_COLOR,
+                                        RESET)
+
+MAUIGPAPI_COLOR = PREFIX + "35;1m"  # magenta
+
+
+class ColorFormatter(BaseColorFormatter):
+    def _color_name(self, module: str) -> str:
+        if module.startswith("mauigpapi"):
+            return MAUIGPAPI_COLOR + module + RESET
+        elif module.startswith("mau.instagram"):
+            mau, instagram, subtype, user_id = module.split(".", 3)
+            return (MAUIGPAPI_COLOR + f"{mau}.{instagram}.{subtype}" + RESET
+                    + "." + MXID_COLOR + user_id + RESET)
+        return super()._color_name(module)