瀏覽代碼

Add support for Matrix->Instagram images

Tulir Asokan 4 年之前
父節點
當前提交
48412978bc
共有 5 個文件被更改,包括 56 次插入10 次删除
  1. 1 1
      ROADMAP.md
  2. 22 2
      mauigpapi/http/thread.py
  3. 21 1
      mauigpapi/http/upload.py
  4. 1 0
      mauigpapi/types/thread_item.py
  5. 11 6
      mautrix_instagram/portal.py

+ 1 - 1
ROADMAP.md

@@ -4,7 +4,7 @@
   * [ ] Message content
   * [ ] Message content
     * [x] Text
     * [x] Text
     * [ ] Media
     * [ ] Media
-      * [ ] Images
+      * [x] Images
       * [ ] Videos
       * [ ] Videos
       * [ ] Voice messages
       * [ ] Voice messages
       * [ ] Locations
       * [ ] Locations

+ 22 - 2
mauigpapi/http/thread.py

@@ -14,9 +14,11 @@
 # 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, AsyncIterable
 from typing import Optional, AsyncIterable
+from uuid import uuid4
 
 
 from .base import BaseAndroidAPI
 from .base import BaseAndroidAPI
-from ..types import DMInboxResponse, DMThreadResponse, Thread, ThreadItem
+from ..types import (DMInboxResponse, DMThreadResponse, Thread, ThreadItem, ThreadAction,
+                     ThreadItemType, CommandResponse)
 
 
 
 
 class ThreadAPI(BaseAndroidAPI):
 class ThreadAPI(BaseAndroidAPI):
@@ -47,7 +49,7 @@ class ThreadAPI(BaseAndroidAPI):
             for thread in resp.inbox.threads:
             for thread in resp.inbox.threads:
                 yield thread
                 yield thread
 
 
-    async def get_thread(self, thread_id: str,  cursor: Optional[str] = None, limit: int = 10,
+    async def get_thread(self, thread_id: str, cursor: Optional[str] = None, limit: int = 10,
                          direction: str = "older", seq_id: Optional[int] = None
                          direction: str = "older", seq_id: Optional[int] = None
                          ) -> DMThreadResponse:
                          ) -> DMThreadResponse:
         query = {
         query = {
@@ -74,3 +76,21 @@ class ThreadAPI(BaseAndroidAPI):
         await self.std_http_post(f"/api/v1/direct_v2/threads/{thread_id}/items/{item_id}/delete/",
         await self.std_http_post(f"/api/v1/direct_v2/threads/{thread_id}/items/{item_id}/delete/",
                                  data={"_csrftoken": self.state.cookies.csrf_token,
                                  data={"_csrftoken": self.state.cookies.csrf_token,
                                        "_uuid": self.state.device.uuid})
                                        "_uuid": self.state.device.uuid})
+
+    async def broadcast(self, thread_id: str, item_type: ThreadItemType, signed: bool = False,
+                        client_context: Optional[str] = None, **kwargs) -> CommandResponse:
+        client_context = client_context or str(uuid4())
+        form = {
+            "action": ThreadAction.SEND_ITEM.value,
+            "send_attribution": "inbox",
+            "thread_id": thread_id,
+            "client_context": client_context,
+            "_csrftoken": self.state.cookies.csrf_token,
+            "device_id": self.state.device.id,
+            "mutation_token": client_context,
+            "_uuid": self.state.device.uuid,
+            **kwargs,
+            "offline_threading_id": client_context,
+        }
+        return await self.std_http_post(f"/api/v1/direct_v2/threads/broadcast/{item_type.value}/",
+                                        data=form, raw=not signed, response_type=CommandResponse)

+ 21 - 1
mauigpapi/http/upload.py

@@ -27,7 +27,7 @@ class UploadAPI(BaseAndroidAPI):
     async def upload_jpeg_photo(self, data: bytes, upload_id: Optional[str] = None,
     async def upload_jpeg_photo(self, data: bytes, upload_id: Optional[str] = None,
                                 is_sidecar: bool = False, waterfall_id: Optional[str] = None,
                                 is_sidecar: bool = False, waterfall_id: Optional[str] = None,
                                 media_type: MediaType = MediaType.IMAGE) -> UploadPhotoResponse:
                                 media_type: MediaType = MediaType.IMAGE) -> UploadPhotoResponse:
-        upload_id = upload_id or str(time.time())
+        upload_id = upload_id or str(int(time.time() * 1000))
         name = f"{upload_id}_0_{random.randint(1000000000, 9999999999)}"
         name = f"{upload_id}_0_{random.randint(1000000000, 9999999999)}"
         params = {
         params = {
             "retry_context": json.dumps({
             "retry_context": json.dumps({
@@ -57,3 +57,23 @@ class UploadAPI(BaseAndroidAPI):
         }
         }
         return await self.std_http_post(f"/rupload_igphoto/{name}", headers=headers, data=data,
         return await self.std_http_post(f"/rupload_igphoto/{name}", headers=headers, data=data,
                                         raw=True, response_type=UploadPhotoResponse)
                                         raw=True, response_type=UploadPhotoResponse)
+
+    async def finish_upload(self, upload_id: str, source_type: str):
+        headers = {
+            "retry_context": json.dumps({
+                "num_step_auto_retry": 0,
+                "num_reupload": 0,
+                "num_step_manual_retry": 0,
+            }),
+        }
+        req = {
+            "timezone_offset": self.state.device.timezone_offset,
+            "_csrftoken": self.state.cookies.csrf_token,
+            "source_type": source_type,
+            "_uid": self.state.cookies.user_id,
+            "device_id": self.state.device.id,
+            "_uuid": self.state.device.uuid,
+            "upload_id": upload_id,
+            "device": self.state.device.payload,
+        }
+        return await self.std_http_post("/api/v1/media/upload_finish/", headers=headers, data=req)

+ 1 - 0
mauigpapi/types/thread_item.py

@@ -30,6 +30,7 @@ class ThreadItemType(SerializableEnum):
     HASHTAG = "hashtag"
     HASHTAG = "hashtag"
     PROFILE = "profile"
     PROFILE = "profile"
     MEDIA_SHARE = "media_share"
     MEDIA_SHARE = "media_share"
+    CONFIGURE_PHOTO = "configure_photo"
     LOCATION = "location"
     LOCATION = "location"
     ACTION_LOG = "action_log"
     ACTION_LOG = "action_log"
     TITLE = "title"
     TITLE = "title"

+ 11 - 6
mautrix_instagram/portal.py

@@ -24,7 +24,7 @@ import magic
 from yarl import URL
 from yarl import URL
 
 
 from mauigpapi.types import (Thread, ThreadUser, ThreadItem, RegularMediaItem, MediaType,
 from mauigpapi.types import (Thread, ThreadUser, ThreadItem, RegularMediaItem, MediaType,
-                             ReactionStatus, Reaction, AnimatedMediaItem)
+                             ReactionStatus, Reaction, AnimatedMediaItem, ThreadItemType)
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.bridge import BasePortal, NotificationDisabler
 from mautrix.bridge import BasePortal, NotificationDisabler
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, ImageInfo,
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, ImageInfo,
@@ -164,9 +164,12 @@ class Portal(DBPortal, BasePortal):
             mime_type = message.info.mimetype or magic.from_buffer(data, mime=True)
             mime_type = message.info.mimetype or magic.from_buffer(data, mime=True)
             if mime_type == "image/jpeg":
             if mime_type == "image/jpeg":
                 upload_resp = await sender.client.upload_jpeg_photo(data)
                 upload_resp = await sender.client.upload_jpeg_photo(data)
-                # TODO I don't think this works
-                resp = await sender.mqtt.send_media(self.thread_id, upload_resp.upload_id,
-                                                    client_context=request_id)
+                # TODO is it possible to do this with MQTT?
+                resp = await sender.client.broadcast(self.thread_id,
+                                                     ThreadItemType.CONFIGURE_PHOTO,
+                                                     client_context=request_id,
+                                                     upload_id=upload_resp.upload_id,
+                                                     allow_full_aspect_ratio="1")
             else:
             else:
                 # TODO add link to media for unsupported file types
                 # TODO add link to media for unsupported file types
                 return
                 return
@@ -278,7 +281,8 @@ class Portal(DBPortal, BasePortal):
             "image/webp": ".webp",
             "image/webp": ".webp",
             "image/jpeg": ".jpg",
             "image/jpeg": ".jpg",
             "video/mp4": ".mp4"
             "video/mp4": ".mp4"
-        }.get(info.mime_type) or mimetypes.guess_extension(info.mime_type)
+        }.get(info.mime_type)
+        extension = extension or mimetypes.guess_extension(info.mime_type) or ""
         file_name = f"{msgtype.value[2:]}{extension}"
         file_name = f"{msgtype.value[2:]}{extension}"
 
 
         upload_mime_type = info.mime_type
         upload_mime_type = info.mime_type
@@ -304,7 +308,8 @@ class Portal(DBPortal, BasePortal):
         if item.media:
         if item.media:
             reuploaded = await self._reupload_instagram_media(source, item.media, intent)
             reuploaded = await self._reupload_instagram_media(source, item.media, intent)
         elif item.animated_media:
         elif item.animated_media:
-            reuploaded = await self._reupload_instagram_animated(source, item.animated_media, intent)
+            reuploaded = await self._reupload_instagram_animated(source, item.animated_media,
+                                                                 intent)
         else:
         else:
             reuploaded = None
             reuploaded = None
         if not reuploaded:
         if not reuploaded: