Repository: ohld/igbot (language: python)

Issue:
Add missing user_tags references
Add tags support for album uploading, it was deleted due to merge conflicts in previous PR (sorry).


Discussion / hints:

Fix patch:
diff --git a/.github/issue-close-app.yml b/.github/issue-close-app.yml
index 38833fdc9..1a8f9928d 100644
--- a/.github/issue-close-app.yml
+++ b/.github/issue-close-app.yml
@@ -28,12 +28,6 @@ issueConfigs:
 # template 3: feature_request
    - "Is your feature request related to a problem? Please describe"
    - "Describe the solution you'd like"
-   - "Version"
-   - "To Reproduce"
-# - content:
-# template 2: feature request
-  # - "Have you looked for this feature in other issues and in the docs"
-  # - "Describe the solution you'd like"
 
 # Optional configuration:
 #
diff --git a/Pipfile.lock b/Pipfile.lock
index 678ca634d..0a735d4d6 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,10 +18,10 @@
     "default": {
         "certifi": {
             "hashes": [
-                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
-                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
+                "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
+                "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
             ],
-            "version": "==2019.11.28"
+            "version": "==2020.4.5.1"
         },
         "chardet": {
             "hashes": [
@@ -118,10 +118,10 @@
         },
         "certifi": {
             "hashes": [
-                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
-                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
+                "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
+                "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
             ],
-            "version": "==2019.11.28"
+            "version": "==2020.4.5.1"
         },
         "chardet": {
             "hashes": [
@@ -140,39 +140,40 @@
         },
         "coverage": {
             "hashes": [
-                "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0",
-                "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30",
-                "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b",
-                "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0",
-                "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823",
-                "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe",
-                "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037",
-                "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6",
-                "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31",
-                "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd",
-                "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892",
-                "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1",
-                "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78",
-                "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac",
-                "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006",
-                "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014",
-                "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2",
-                "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7",
-                "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8",
-                "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7",
-                "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9",
-                "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1",
-                "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307",
-                "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a",
-                "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435",
-                "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0",
-                "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5",
-                "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441",
-                "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732",
-                "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de",
-                "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"
-            ],
-            "version": "==5.0.4"
+                "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
+                "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
+                "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
+                "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
+                "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
+                "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
+                "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
+                "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
+                "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
+                "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
+                "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
+                "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
+                "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
+                "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
+                "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
+                "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
+                "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
+                "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
+                "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
+                "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
+                "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
+                "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
+                "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
+                "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
+                "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
+                "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
+                "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
+                "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
+                "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
+                "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
+                "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
+            ],
+            "index": "pypi",
+            "version": "==5.1"
         },
         "idna": {
             "hashes": [
@@ -219,10 +220,10 @@
         },
         "pyparsing": {
             "hashes": [
-                "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
-                "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
+                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
-            "version": "==2.4.6"
+            "version": "==2.4.7"
         },
         "pysocks": {
             "hashes": [
diff --git a/examples/photos/pics/01.jpg b/examples/photos/pics/01.jpg
deleted file mode 100644
index 00c4d7050..000000000
Binary files a/examples/photos/pics/01.jpg and /dev/null differ
diff --git a/examples/photos/pics/02.jpg b/examples/photos/pics/02.jpg
deleted file mode 100644
index 61ee42694..000000000
Binary files a/examples/photos/pics/02.jpg and /dev/null differ
diff --git a/examples/photos/upload_photos.py b/examples/photos/upload_photos.py
index a66dd8727..9aa63f806 100644
--- a/examples/photos/upload_photos.py
+++ b/examples/photos/upload_photos.py
@@ -18,6 +18,8 @@
 parser.add_argument("-proxy", type=str, help="proxy")
 parser.add_argument("-photo", type=str, help="photo name")
 parser.add_argument("-caption", type=str, help="caption for photo")
+parser.add_argument('-tag', action='append', help='taged user id')
+
 args = parser.parse_args()
 
 bot = Bot()
@@ -77,9 +79,14 @@
                 pic=pic, caption=caption
             )
         )
+
+        # prepare tagged user_id
+        users_to_tag = [{'user_id': u, 'x': 0.5, 'y': 0.5} for u in args.tag]
+
         if not bot.upload_photo(
             os.path.dirname(os.path.realpath(__file__)) + "/media/" + pic,
             caption=caption,
+            user_tags=users_to_tag
         ):
             bot.logger.error("Something went wrong...")
             break
diff --git a/examples/video/media/02.mov b/examples/video/media/02.mov
deleted file mode 100644
index 2fd82ef4c..000000000
Binary files a/examples/video/media/02.mov and /dev/null differ
diff --git a/examples/video/media/60s.mp4 b/examples/video/media/60s.mp4
deleted file mode 100644
index 578a88d22..000000000
Binary files a/examples/video/media/60s.mp4 and /dev/null differ
diff --git a/instabot/api/api.py b/instabot/api/api.py
index 3c25c8b32..fd46ce32d 100644
--- a/instabot/api/api.py
+++ b/instabot/api/api.py
@@ -41,7 +41,7 @@
     creatives_ar_class,
     set_contact_point_prefill,
 )
-from .api_photo import configure_photo, download_photo, upload_photo
+from .api_photo import configure_photo, download_photo, upload_photo, upload_album
 from .api_story import configure_story, download_story, upload_story_photo
 from .api_video import configure_video, download_video, upload_video
 from .prepare import delete_credentials, get_credentials
@@ -106,17 +106,29 @@ def __init__(
             fh.setLevel(loglevel_file)
             fh.setFormatter(
                 logging.Formatter(
-                    "%(asctime)s - %(name)s (%(module)s) - %(levelname)s - %(message)s"
+                    "%(asctime)s - %(name)s (%(module)s %(pathname)s:%(lineno)s) - %(levelname)s - %(message)s"
                 )
             )
 
-            self.logger.addHandler(fh)
+            handler_existed = False
+            for handler in self.logger.handlers:
+                if isinstance(handler, logging.FileHandler):
+                    handler_existed = True
+                    break
+            if not handler_existed:
+                self.logger.addHandler(fh)
 
         ch = logging.StreamHandler()
         ch.setLevel(loglevel_stream)
         ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
 
-        self.logger.addHandler(ch)
+        handler_existed = False
+        for handler in self.logger.handlers:
+            if isinstance(handler, logging.StreamHandler):
+                handler_existed = True
+                break
+        if not handler_existed:
+            self.logger.addHandler(ch)
         self.logger.setLevel(logging.DEBUG)
 
         self.last_json = None
@@ -250,7 +262,9 @@ def login(
         is_threaded=False,
     ):
         if password is None:
-            username, password = get_credentials(username=username)
+            username, password = get_credentials(
+                base_path=self.base_path, username=username
+            )
 
         set_device = generate_all_uuids = True
         self.set_user(username, password)
@@ -352,7 +366,7 @@ def login(
                     "Failed to login go to instagram and change your password"
                 )
                 self.save_failed_login()
-                delete_credentials()
+                delete_credentials(self.base_path)
                 return False
 
     def two_factor_auth(self):
@@ -402,7 +416,7 @@ def save_successful_login(self):
 
     def save_failed_login(self):
         self.logger.info("Username or password is incorrect.")
-        delete_credentials()
+        delete_credentials(self.base_path)
         sys.exit()
 
     def solve_challenge(self):
@@ -508,6 +522,7 @@ def send_request(
             raise Exception(msg)
         if headers:
             self.session.headers.update(headers)
+
         try:
             self.total_requests += 1
             if post is not None:  # POST
@@ -535,6 +550,7 @@ def send_request(
             self.logger.debug(
                 "GET to endpoint: {} returned response: {}".format(endpoint, response)
             )
+
         if response.status_code == 200:
             try:
                 self.last_json = json.loads(response.text)
@@ -588,7 +604,7 @@ def send_request(
                     self.logger.error(
                         "Since we hit 15 minutes of time outs, we have to restart. Removing session and cookies. Please relogin."
                     )
-                    delete_credentials()
+                    delete_credentials(self.base_path)
                     sys.exit()
                 timeout_minutes += 5
                 self.logger.warning(
@@ -612,7 +628,7 @@ def send_request(
                     self.logger.error(
                         "Failed to login go to instagram and change your password"
                     )
-                    delete_credentials()
+                    delete_credentials(self.base_path)
                 # PERFORM Interactive Two-Factor Authentication
                 if response_data.get("two_factor_required"):
                     try:
@@ -761,6 +777,8 @@ def upload_photo(
         from_video=False,
         force_resize=False,
         options={},
+        user_tags=None,
+        is_sidecar=False
     ):
         """Upload photo to Instagram
 
@@ -776,18 +794,55 @@ def upload_photo(
                              configure_timeout, rename (Dict)
                              Designed to reduce the number of function
                              arguments! This is the simplest request object.
+        @param user_tags     Tag other users (List)
+                             usertags = [
+                                {"user_id": user_id, "position": [x, y]}
+                             ]
+        @param is_sidecar    An album element (Boolean)
 
         @return Boolean
         """
         return upload_photo(
-            self, photo, caption, upload_id, from_video, force_resize, options
+            self, photo, caption, upload_id, from_video, force_resize, options, user_tags, is_sidecar
+        )
+
+    def upload_album(
+        self,
+        photos,
+        caption=None,
+        upload_id=None,
+        from_video=False,
+        force_resize=False,
+        options={},
+        user_tags=None
+    ):
+        """Upload album to Instagram
+
+        @param photos        List of paths to photo files (List of strings)
+        @param caption       Media description (String)
+        @param upload_id     Unique upload_id (String). When None, then
+                             generate automatically
+        @param from_video    A flag that signals whether the photo is loaded
+                             from the video or by itself
+                             (Boolean, DEPRECATED: not used)
+        @param force_resize  Force photo resize (Boolean)
+        @param options       Object with difference options, e.g.
+                             configure_timeout, rename (Dict)
+                             Designed to reduce the number of function
+                             arguments! This is the simplest request object.
+        @param user_tags
+
+        @return Boolean
+        """
+        return upload_album(
+            self, photos, caption, upload_id, from_video, force_resize, options, user_tags
         )
 
     def download_photo(self, media_id, filename, media=False, folder="photos"):
         return download_photo(self, media_id, filename, media, folder)
 
-    def configure_photo(self, upload_id, photo, caption=""):
-        return configure_photo(self, upload_id, photo, caption)
+    def configure_photo(self, upload_id, photo, caption="", user_tags=None, is_sidecar=False):
+        return configure_photo(self, upload_id, photo, caption, user_tags, is_sidecar)
 
     # ====== STORY METHODS ====== #
     def download_story(self, filename, story_url, username):
diff --git a/instabot/api/api_photo.py b/instabot/api/api_photo.py
index 904eb7553..57588a272 100644
--- a/instabot/api/api_photo.py
+++ b/instabot/api/api_photo.py
@@ -78,10 +78,9 @@ def compatible_aspect_ratio(size):
     return min_ratio <= ratio <= max_ratio
 
 
-def configure_photo(self, upload_id, photo, caption=""):
+def configure_photo(self, upload_id, photo, caption="", user_tags=None, is_sidecar=False):
     width, height = get_image_size(photo)
-    data = self.json_data(
-        {
+    data = {
             "media_folder": "Instagram",
             "source_type": 4,
             "caption": caption,
@@ -94,7 +93,13 @@ def configure_photo(self, upload_id, photo, caption=""):
             },
             "extra": {"source_width": width, "source_height": height},
         }
-    )
+    if user_tags:
+        data['usertags'] = user_tags
+
+    if is_sidecar:
+        return data
+
+    data = self.json_data(data)
     return self.send_request("media/configure/?", data)
 
 
@@ -106,6 +111,8 @@ def upload_photo(
     from_video=False,
     force_resize=False,
     options={},
+    user_tags=None,
+    is_sidecar=False
 ):
     """Upload photo to Instagram
 
@@ -121,12 +128,23 @@ def upload_photo(
                          configure_timeout, rename (Dict)
                          Designed to reduce the number of function arguments!
                          This is the simplest request object.
+    @param user_tags     Tag other users (List)
+                         usertags = [
+                            {"user_id": user_id, "position": [x, y]}
+                         ]
+    @param is_sidecar    An album element (Boolean)
 
-    @return Boolean
+    @return Object with state of uploading to Instagram (or False), Dict for is_sidecar
     """
+    if user_tags is None:
+        usertags = None
+    else:
+        tags = {'in': [{'user_id': user['user_id'], 'position': [user['x'], user['y']]} for user in user_tags]}
+        usertags = json.dumps(tags, separators=(',', ':'))
+
     options = dict({"configure_timeout": 15, "rename": True}, **(options or {}))
     if upload_id is None:
-        upload_id = str(int(time.time() * 1000))
+        upload_id = int(time.time() * 1000)
     if not photo:
         return False
     if not compatible_aspect_ratio(get_image_size(photo)):
@@ -137,9 +155,8 @@ def upload_photo(
             return False
     waterfall_id = str(uuid4())
     # upload_name example: '1576102477530_0_7823256191'
-    upload_name = "{upload_id}_0_{rand}".format(
-        upload_id=upload_id, rand=random.randint(1000000000, 9999999999)
-    )
+    # upload_name example:  'fb_uploader_1585807380927'
+    upload_name = "fb_uploader_{upload_id}".format(upload_id=upload_id)
     rupload_params = {
         "retry_context": '{"num_step_auto_retry":0,"num_reupload":0,"num_step_manual_retry":0}',
         "media_type": "1",
@@ -149,6 +166,8 @@ def upload_photo(
             {"lib_name": "moz", "lib_version": "3.1.m", "quality": "80"}
         ),
     }
+    if is_sidecar:
+        rupload_params["is_sidecar"] = "1"
     photo_data = open(photo, "rb").read()
     photo_len = str(len(photo_data))
     self.session.headers.update(
@@ -171,11 +190,14 @@ def upload_photo(
         ),
         data=photo_data,
     )
+
     if response.status_code != 200:
         self.logger.error(
             "Photo Upload failed with the following response: {}".format(response)
         )
         return False
+    # update the upload id
+    upload_id = int(response.json()['upload_id'])
     if from_video:
         # Not configure when from_video is True
         return True
@@ -184,7 +206,12 @@ def upload_photo(
     for attempt in range(4):
         if configure_timeout:
             time.sleep(configure_timeout)
-        if self.configure_photo(upload_id, photo, caption):
+        if is_sidecar:
+            configuration = self.configure_photo(upload_id, photo, caption, usertags, is_sidecar=True)
+            if options.get("rename"):
+                os.rename(photo, "{fname}.REMOVE_ME".format(fname=photo))
+            return configuration
+        elif self.configure_photo(upload_id, photo, caption, usertags, is_sidecar=False):
             media = self.last_json.get("media")
             self.expose()
             if options.get("rename"):
@@ -193,6 +220,53 @@ def upload_photo(
     return False
 
 
+def upload_album(
+    self,
+    photos,
+    caption=None,
+    upload_id=None,
+    from_video=False,
+    force_resize=False,
+    options={},
+    user_tags=None
+):
+    """Upload album to Instagram
+
+    @param photos        List of paths to photo files (List of strings)
+    @param caption       Media description (String)
+    @param upload_id     Unique upload_id (String). When None, then generate
+                         automatically
+    @param from_video    A flag that signals whether the photo is loaded from
+                         the video or by itself
+                         (Boolean, DEPRECATED: not used)
+    @param force_resize  Force photo resize (Boolean)
+    @param options       Object with difference options, e.g.
+                         configure_timeout, rename (Dict)
+                         Designed to reduce the number of function arguments!
+                         This is the simplest request object.
+    @param user_tags
+
+    @return Boolean
+    """
+    if not photos:
+        return False
+    photo_metas = []
+    for photo in photos:
+        result = self.upload_photo(photo, caption, None, from_video, force_resize, options, user_tags, is_sidecar=True)
+        if not result:
+            self.logger.error("Could not upload photo {photo} for the album!".format(photo=photo))
+            return False
+        photo_metas.append(result)
+    if upload_id is None:
+        upload_id = int(time.time() * 1000)
+    data = self.json_data({
+        "caption": caption,
+        "client_sidecar_id": upload_id,
+        "children_metadata": photo_metas
+    })
+    return self.send_request("media/configure_sidecar/?", post=data)
+
+
 def get_image_size(fname):
     with open(fname, "rb") as fhandle:
         head = fhandle.read(24)
diff --git a/instabot/api/api_video.py b/instabot/api/api_video.py
index 4a793ff90..06e273ab8 100644
--- a/instabot/api/api_video.py
+++ b/instabot/api/api_video.py
@@ -286,7 +286,6 @@ def resize_video(fname, thumbnail=None):
             print("Resizing video")
             vid = vid.resize(width=1080)
     (w, h) = vid.size
-    return fname, thumbnail, w, h, vid.duration
     if vid.duration > d_lim:
         print("Cutting video to {lim} sec from start".format(lim=d_lim))
         vid = vid.subclip(0, d_lim)
diff --git a/instabot/api/config.py b/instabot/api/config.py
index ea3231697..4b342bfc4 100644
--- a/instabot/api/config.py
+++ b/instabot/api/config.py
@@ -3,7 +3,7 @@
 
 API_DOMAIN = "i.instagram.com"
 API_URL = "https://{domain}/api/v1/".format(domain=API_DOMAIN)
-APP_VERSION = "130.0.0.31.121"
+APP_VERSION = "136.0.0.34.124"
 USER_AGENT_BASE = (
     "Instagram {app_version} "
     "Android ({android_version}/{android_release}; "
@@ -11,7 +11,7 @@
     "{device}; {model}; {cpu}; en_US; {version_code})"
 )
 SIG_KEY_VERSION = "4"
-IG_SIG_KEY = "f0bdfd5332d66a64d5e04965e6a7ade67c4e2cfc57ea38f0083c0400640a5e20"
+IG_SIG_KEY = "46024e8f31e295869a0e861eaed42cb1dd8454b55232d85f6c6764365079374b"
 
 REQUEST_HEADERS = {
     "X-IG-App-Locale": "en_US",
diff --git a/instabot/api/devices.py b/instabot/api/devices.py
index a9271acc0..e7e8f806b 100644
--- a/instabot/api/devices.py
+++ b/instabot/api/devices.py
@@ -1,5 +1,7 @@
-DEFAULT_DEVICE = "one_plus_7"
-APP_VERSION = "130.0.0.31.121"
+import random
+
+APP_VERSION = "136.0.0.34.124"
+VERSION_CODE = "208061712"
 DEVICES = {
     "one_plus_7": {
         "app_version": APP_VERSION,
@@ -11,7 +13,7 @@
         "device": "GM1903",
         "model": "OnePlus7",
         "cpu": "qcom",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     "one_plus_3": {
         "app_version": APP_VERSION,
@@ -23,7 +25,7 @@
         "device": "ONEPLUS A3003",
         "model": "OnePlus3",
         "cpu": "qcom",
-        "version_code": "180322800",
+        "version_code": VERSION_CODE,
     },
     # Released on March 2016
     "samsung_galaxy_s7": {
@@ -36,7 +38,7 @@
         "device": "SM-G930F",
         "model": "herolte",
         "cpu": "samsungexynos8890",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on January 2017
     "huawei_mate_9_pro": {
@@ -49,7 +51,7 @@
         "device": "LON-L29",
         "model": "HWLON",
         "cpu": "hi3660",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on February 2018
     "samsung_galaxy_s9_plus": {
@@ -62,7 +64,7 @@
         "device": "SM-G965F",
         "model": "star2qltecs",
         "cpu": "samsungexynos9810",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on November 2016
     "one_plus_3t": {
@@ -75,7 +77,7 @@
         "device": "ONEPLUS A3010",
         "model": "OnePlus3T",
         "cpu": "qcom",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on April 2016
     "lg_g5": {
@@ -88,7 +90,7 @@
         "device": "RS988",
         "model": "h1",
         "cpu": "h1",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on June 2016
     "zte_axon_7": {
@@ -101,7 +103,7 @@
         "device": "ZTE A2017U",
         "model": "ailsa_ii",
         "cpu": "qcom",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
     # Released on March 2016
     "samsung_galaxy_s7_edge": {
@@ -114,6 +116,7 @@
         "device": "SM-G935",
         "model": "hero2lte",
         "cpu": "samsungexynos8890",
-        "version_code": "168361634",
+        "version_code": VERSION_CODE,
     },
 }
+DEFAULT_DEVICE = random.choice(list(DEVICES.keys()))
diff --git a/instabot/api/prepare.py b/instabot/api/prepare.py
index 4124ef3c7..3105c9c17 100644
--- a/instabot/api/prepare.py
+++ b/instabot/api/prepare.py
@@ -4,11 +4,15 @@
 import os
 import sys
 
-current_path = os.path.abspath(os.getcwd())
-SECRET_FILE = current_path + "/config/secret.txt"
+DEFAULT_SECRET_DIR = os.path.abspath(os.getcwd())
 
 
-def add_credentials():
+def get_credential_file(base_path=DEFAULT_SECRET_DIR):
+    return base_path + "/config/secret.txt"
+
+
+def add_credentials(base_path):
+    SECRET_FILE = get_credential_file(base_path)
     with open(SECRET_FILE, "a") as f:
         print("Enter your login: ")
         f.write(str(sys.stdin.readline().strip()) + ":")
@@ -19,7 +23,8 @@ def add_credentials():
         f.write(getpass.getpass() + "\n")
 
 
-def get_credentials(username=None):
+def get_credentials(base_path, username=None):
+    SECRET_FILE = get_credential_file(base_path)
     """Returns login and password stored in `secret.txt`."""
     while not check_secret():
         pass
@@ -42,11 +47,11 @@ def get_credentials(username=None):
         try:
             ind = int(sys.stdin.readline())
             if ind == 0:
-                add_credentials()
+                add_credentials(base_path)
                 continue
             elif ind == -1:
-                delete_credentials()
-                check_secret()
+                delete_credentials(base_path)
+                check_secret(base_path)
                 continue
             elif 0 <= ind - 1 < len(lines):
                 return lines[ind - 1]
@@ -54,7 +59,8 @@ def get_credentials(username=None):
             print("Wrong input, enter the number of the account to use.")
 
 
-def check_secret():
+def check_secret(base_path):
+    SECRET_FILE = get_credential_file(base_path)
     while True:
         if os.path.exists(SECRET_FILE):
             with open(SECRET_FILE, "r") as f:
@@ -80,13 +86,14 @@ def check_secret():
             )
             print("Don't worry. It will be stored locally.")
             while True:
-                add_credentials()
+                add_credentials(base_path)
                 print("Do you want to add another account? (y/n)")
                 if "y" not in sys.stdin.readline():
                     break
 
 
-def delete_credentials():
+def delete_credentials(base_path):
+    SECRET_FILE = get_credential_file(base_path)
     if os.path.exists(SECRET_FILE):
         os.remove(SECRET_FILE)
 
diff --git a/instabot/bot/bot.py b/instabot/bot/bot.py
index af4fc83fd..46071c2d4 100644
--- a/instabot/bot/bot.py
+++ b/instabot/bot/bot.py
@@ -104,7 +104,7 @@
     like_user,
     like_users,
 )
-from .bot_photo import download_photo, download_photos, upload_photo
+from .bot_photo import download_photo, download_photos, upload_photo, upload_album
 from .bot_stats import save_user_stats
 from .bot_story import download_stories, upload_story_photo, watch_users_reels
 from .bot_support import (
@@ -785,8 +785,7 @@ def download_photos(self, medias, folder="photos", save_description=False):
         return download_photos(self, medias, folder, save_description)
 
     def upload_photo(
-        self, photo, caption=None, upload_id=None, from_video=False, options={}
-    ):
+        self, photo, caption=None, upload_id=None, from_video=False, options={}, user_tags=None, is_sidecar=False):
         """Upload photo to Instagram
         @param photo        Path to photo file (String)
         @param caption      Media description (String)
@@ -799,11 +798,38 @@ def upload_photo(
                             e.g. configure_timeout, rename (Dict)
                             Designed to reduce the number of function
                             arguments! This is the simplest request object.
+        @param user_tags    Tag other users (List)
+                            usertags = [
+                                {"user_id": user_id, "position": [x, y]}
+                            ]
+        @param is_sidecar   An album element (Boolean)
+
+        @return             Object with state of uploading to
+                            Instagram (or False)
+        """
+        return upload_photo(self, photo, caption, upload_id, from_video, options, user_tags, is_sidecar)
+
+    def upload_album(
+        self, photos, caption=None, upload_id=None, from_video=False, options={}, user_tags=None
+    ):
+        """Upload album to Instagram
+        @param photos       List of paths to photo files (List of strings)
+        @param caption      Media description (String)
+        @param upload_id    Unique upload_id (String). When None, then
+                            generate automatically
+        @param from_video   A flag that signals whether the photo is loaded
+                            from the video or by itself
+                            (Boolean, DEPRECATED: not used)
+        @param options      Object with difference options,
+                            e.g. configure_timeout, rename (Dict)
+                            Designed to reduce the number of function
+                            arguments! This is the simplest request object.
+        @param user_tags
 
         @return             Object with state of uploading to
                             Instagram (or False)
         """
-        return upload_photo(self, photo, caption, upload_id, from_video, options)
+        return upload_album(self, photos, caption, upload_id, from_video, options, user_tags)
 
     # video
     def upload_video(self, video, caption="", thumbnail=None, options={}):
diff --git a/instabot/bot/bot_checkpoint.py b/instabot/bot/bot_checkpoint.py
index cfd2c6704..27d447688 100644
--- a/instabot/bot/bot_checkpoint.py
+++ b/instabot/bot/bot_checkpoint.py
@@ -6,8 +6,7 @@
 import pickle
 from datetime import datetime
 
-current_path = os.path.abspath(os.getcwd())
-CHECKPOINT_PATH = current_path + "/config/{fname}.checkpoint"
+CHECKPOINT_PATH = "{fname}.checkpoint"
 
 
 class Checkpoint(object):
diff --git a/instabot/bot/bot_photo.py b/instabot/bot/bot_photo.py
index dd92351c6..8e596218e 100644
--- a/instabot/bot/bot_photo.py
+++ b/instabot/bot/bot_photo.py
@@ -5,7 +5,7 @@
 
 
 def upload_photo(
-    self, photo, caption=None, upload_id=None, from_video=False, options={}
+    self, photo, caption=None, upload_id=None, from_video=False, options={}, user_tags=None, is_sidecar=False
 ):
     """Upload photo to Instagram
 
@@ -19,12 +19,17 @@ def upload_photo(
                        configure_timeout, rename (Dict)
                        Designed to reduce the number of function arguments!
                        This is the simplest request object.
+    @param user_tags   Tag other users (List)
+                       usertags = [
+                         {"user_id": user_id, "position": [x, y]}
+                       ]
+    @param is_sidecar  An album element (Boolean)
 
-    @return            Object with state of uploading to Instagram (or False)
+    @return            Object with state of uploading to Instagram (or False), Dict for is_sidecar
     """
     self.small_delay()
     result = self.api.upload_photo(
-        photo, caption, upload_id, from_video, options=options
+        photo, caption, upload_id, from_video, options=options, user_tags=user_tags, is_sidecar=is_sidecar
     )
     if not result:
         self.logger.info("Photo '{}' is not uploaded.".format(photo))
@@ -33,6 +38,36 @@ def upload_photo(
     return result
 
 
+def upload_album(
+    self, photos, caption=None, upload_id=None, from_video=False, options={}, user_tags=None
+):
+    """Upload album to Instagram
+
+    @param photos      List of paths to photo files (List of strings)
+    @param caption     Media description (String)
+    @param upload_id   Unique upload_id (String). When None, then
+                       generate automatically
+    @param from_video  A flag that signals whether the photo is loaded from
+                       the video or by itself (Boolean, DEPRECATED: not used)
+    @param options     Object with difference options, e.g.
+                       configure_timeout, rename (Dict)
+                       Designed to reduce the number of function arguments!
+                       This is the simplest request object.
+    @param user_tags
+
+    @return            Boolean
+    """
+    self.small_delay()
+    result = self.api.upload_album(
+        photos, caption, upload_id, from_video, options=options, user_tags=user_tags
+    )
+    if not result:
+        self.logger.info("Photos are not uploaded.")
+        return False
+    self.logger.info("Photo are uploaded.")
+    return result
+
+
 def download_photo(
     self, media_id, folder="photos", filename=None, save_description=False
 ):
diff --git a/requirements.txt b/requirements.txt
index 79384ddd1..606e6eaee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,4 +16,6 @@ mock>=3.0.5
 moviepy>=1.0.1
 Pillow>=6.2.2
 pytest>=4.6.9
+rsa>=4.0
 pycryptodome>=3.9.7
+pycryptodomex>=3.9.7


Test patch:
diff --git a/examples/autopost/pics/1 First test.jpg b/examples/autopost/pics/1 First test.jpg
deleted file mode 100644
index eac3734fd..000000000
Binary files a/examples/autopost/pics/1 First test.jpg and /dev/null differ
diff --git a/examples/autopost/pics/2 Second test.jpg b/examples/autopost/pics/2 Second test.jpg
deleted file mode 100644
index eac3734fd..000000000
Binary files a/examples/autopost/pics/2 Second test.jpg and /dev/null differ
diff --git a/examples/autopost/pics/3 Third test.jpg b/examples/autopost/pics/3 Third test.jpg
deleted file mode 100644
index eac3734fd..000000000
Binary files a/examples/autopost/pics/3 Third test.jpg and /dev/null differ