├── gradle.properties ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ └── kotlin │ ├── Credentials.kt │ ├── util │ ├── LoginException.kt │ ├── Device.kt │ ├── Devices.kt │ ├── CookiePersistor.kt │ ├── Crypto.kt │ └── Config.kt │ ├── jsonpath │ ├── extension │ │ ├── JSONArray.kt │ │ └── JSONObject.kt │ ├── JsonResult.kt │ ├── cache │ │ ├── Cache.kt │ │ ├── LRUCache.kt │ │ └── CacheProvider.kt │ ├── JsonPath.kt │ ├── PathCompiler.kt │ └── Token.kt │ ├── samples │ ├── follow │ │ ├── UnfollowNonFollowers.kt │ │ ├── FollowUsersFollowers.kt │ │ └── FollowUserByLocation.kt │ ├── stories │ │ ├── WatchStoriesFromExploreTab.kt │ │ ├── WatchStoriesByLocation.kt │ │ └── WatchStoriesOfUserFollowers.kt │ ├── like │ │ ├── LikeExploreTabMedias.kt │ │ ├── LikeHashTagMedias.kt │ │ └── LikeLocationBasedMedias.kt │ ├── download │ │ ├── DownloadStoriesOfUser.kt │ │ └── DownloadUserMedias.kt │ ├── dm │ │ ├── SendLikeToAllYourFollowers.kt │ │ └── SendHashTagToTimeLineUsers.kt │ └── comment │ │ ├── CommentExploreTabMedias.kt │ │ ├── CommentHashTagMedias.kt │ │ └── CommentLocationBasedMedias.kt │ ├── BotTest.kt │ └── api │ ├── Request.kt │ └── InstagramAPI.kt ├── settings.gradle.kts ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /.idea/ 4 | /local.properties 5 | /data/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadiyarajesh/insta-bot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/Credentials.kt: -------------------------------------------------------------------------------- 1 | object Credentials { 2 | const val USERNAME = "enter_your_instagram_username_here" 3 | const val PASSWORD = "enter_your_instagram_password_here" 4 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "InstagramAPI" 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | jcenter() 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/util/LoginException.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | class LoginException(message: String) : Exception() { 4 | val msg = message 5 | override fun toString(): String { 6 | return msg 7 | } 8 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 24 00:37:30 CST 2019 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /src/main/kotlin/util/Device.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | data class Device( 4 | val instagramVersion: String, 5 | val androidVersion: Int, 6 | val androidRelease: String, 7 | val dpi: String, 8 | val resolution: String, 9 | val manufacturer: String, 10 | val device: String, 11 | val model: String, 12 | val cpu: String 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/extension/JSONArray.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite.extension 2 | 3 | import com.nfeld.jsonpathlite.JsonPath 4 | import org.json.JSONArray 5 | 6 | fun JSONArray.read(jsonpath: String): T? { 7 | return JsonPath(jsonpath).readFromJson(this) 8 | } 9 | 10 | fun JSONArray.read(jsonpath: JsonPath): T? { 11 | return jsonpath.readFromJson(this) 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/extension/JSONObject.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite.extension 2 | 3 | import com.nfeld.jsonpathlite.JsonPath 4 | import org.json.JSONObject 5 | 6 | fun JSONObject.read(jsonpath: String): T? { 7 | return JsonPath(jsonpath).readFromJson(this) 8 | } 9 | 10 | fun JSONObject.read(jsonpath: JsonPath): T? { 11 | return jsonpath.readFromJson(this) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/JsonResult.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONObject 5 | 6 | data class JsonObject(val underlying: JSONObject) : JsonResult() { 7 | override fun read(path: String): T? = JsonPath(path).readFromJson(underlying) 8 | } 9 | 10 | data class JsonArray(val underlying: JSONArray): JsonResult() { 11 | override fun read(path: String): T? = JsonPath(path).readFromJson(underlying) 12 | } 13 | 14 | sealed class JsonResult { 15 | abstract fun read(path: String): T? 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/samples/follow/UnfollowNonFollowers.kt: -------------------------------------------------------------------------------- 1 | package samples.follow 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | bot.unfollowNonFollowers().collect { println(it) } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/stories/WatchStoriesFromExploreTab.kt: -------------------------------------------------------------------------------- 1 | package samples.stories 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | bot.watchExploreTabUsersStories().collect { println(it) } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/like/LikeExploreTabMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.like 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val howManyMediasYouWantToLike = 10 20 | 21 | bot.likeMediasByExplorePage(howManyMediasYouWantToLike).collect { println(it) } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/download/DownloadStoriesOfUser.kt: -------------------------------------------------------------------------------- 1 | package samples.download 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val user = "enter_username_here_whose_media_you_want_to_download" 20 | 21 | bot.downloadUserStories(user).collect { println(it) } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/cache/Cache.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite.cache 2 | 3 | import com.nfeld.jsonpathlite.JsonPath 4 | 5 | interface Cache { 6 | /** 7 | * Retrieve an instance of [JsonPath] containing the compiled path. 8 | * 9 | * @param path path string key for cache 10 | * @return cached [JsonPath] instance or null if not cached 11 | */ 12 | fun get(path: String): JsonPath? 13 | 14 | /** 15 | * Insert the given path and [JsonPath] as key/value pair into cache. 16 | * 17 | * @param path path string key for cache 18 | * @param jsonPath instance of [JsonPath] containing compiled path 19 | */ 20 | fun put(path: String, jsonPath: JsonPath) 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/util/Devices.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | object Devices { 4 | private const val INSTAGRAM_VERSION_OLD: String = "105.0.0.18.119" 5 | private const val INSTAGRAM_VERSION_NEW: String = "126.0.0.25.121" 6 | 7 | // Released on August 2019 8 | val onePlus7 = Device( 9 | INSTAGRAM_VERSION_NEW, 29, "10.0", "420dpi", 10 | "1080x2260", "OnePlus", "GM1903", 11 | "OnePlus7", "qcom" 12 | ) 13 | 14 | // Released on February 2018 15 | val samsungGalaxyS9Plus = Device( 16 | INSTAGRAM_VERSION_OLD, 24, "7.0", "640dpi", 17 | "1440x2560", "samsung", "SM-G965F", 18 | "star2qltecs", "samsungexynos9810" 19 | ) 20 | 21 | val DEFAULT_DEVICE: Device = onePlus7 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/like/LikeHashTagMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.like 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val hashTagName = "enter_hashtag_name_here" 20 | val howManyMediasYouWantToLike = 10 21 | 22 | bot.likeMediasByHashTag(hashTagName, howManyMediasYouWantToLike).collect { println(it) } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/dm/SendLikeToAllYourFollowers.kt: -------------------------------------------------------------------------------- 1 | package samples.dm 2 | 3 | import bot.InstagramBot 4 | import com.nfeld.jsonpathlite.extension.read 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.runBlocking 9 | 10 | @ExperimentalCoroutinesApi 11 | fun main() = runBlocking { 12 | 13 | val username = Credentials.USERNAME 14 | val password = Credentials.PASSWORD 15 | 16 | val bot = InstagramBot() 17 | bot.prepare(username) 18 | bot.login(username, password) 19 | 20 | val yourFollowers = bot.getSelfFollowers().toList() 21 | bot.sendDirectLikeToUsers(yourFollowers).collect { println(it) } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/like/LikeLocationBasedMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.like 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val locationName = "enter_location_name_here" 20 | val howManyMediasYouWantToLike = 10 21 | 22 | bot.likeMediasByLocation(locationName, howManyMediasYouWantToLike).collect { println(it) } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/comment/CommentExploreTabMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.comment 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val commentList = listOf("Comment 1", "Comment 2") 20 | val howManyMediasYouWantToComment = 10 21 | 22 | bot.commentMediasByExplorePage(commentList, howManyMediasYouWantToComment).collect { println(it) } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/follow/FollowUsersFollowers.kt: -------------------------------------------------------------------------------- 1 | package samples.follow 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val user = "enter_username_whose_followers_you_want_to_follow_here" 20 | val howManyFollowersYouWantToFollow = 10 21 | 22 | bot.followUserFollowers(user, howManyFollowersYouWantToFollow).collect { println(it) } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/stories/WatchStoriesByLocation.kt: -------------------------------------------------------------------------------- 1 | package samples.stories 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val locationName = "enter_location_name_here" 20 | val howManyUsersYouWantToWatchStories = 10 21 | 22 | bot.watchLocationUsersStories( 23 | locationName, 24 | howManyUsersYouWantToWatchStories 25 | ).collect { println(it) } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/comment/CommentHashTagMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.comment 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val hashTagName = "enter_hashtag_name_here" 20 | val commentList = listOf("Comment 1", "Comment 2") 21 | val howManyMediasYouWantToComment = 10 22 | 23 | bot.commentMediasByHashTag(hashTagName, commentList, howManyMediasYouWantToComment).collect { println(it) } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/comment/CommentLocationBasedMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.comment 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val locationName = "enter_location_name_here" 20 | val commentList = listOf("Comment 1", "Comment 2") 21 | val howManyMediasYouWantToComment = 10 22 | 23 | bot.commentMediasByLocation(locationName, commentList, howManyMediasYouWantToComment).collect { println(it) } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/stories/WatchStoriesOfUserFollowers.kt: -------------------------------------------------------------------------------- 1 | package samples.stories 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.runBlocking 9 | 10 | @ExperimentalCoroutinesApi 11 | fun main() = runBlocking { 12 | 13 | val username = Credentials.USERNAME 14 | val password = Credentials.PASSWORD 15 | 16 | val bot = InstagramBot() 17 | bot.prepare(username) 18 | bot.login(username, password) 19 | 20 | 21 | val user = "enter_username_whose_followers_stories_you_want_to_watch" 22 | val howManyFollowersYouWantWatchStories = 10 23 | 24 | bot.watchUsersStories( 25 | bot.getUserFollowers(user, howManyFollowersYouWantWatchStories).toList() 26 | ).collect { println(it) } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/download/DownloadUserMedias.kt: -------------------------------------------------------------------------------- 1 | package samples.download 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.runBlocking 8 | 9 | @ExperimentalCoroutinesApi 10 | fun main() = runBlocking { 11 | 12 | val username = Credentials.USERNAME 13 | val password = Credentials.PASSWORD 14 | 15 | val bot = InstagramBot() 16 | bot.prepare(username) 17 | bot.login(username, password) 18 | 19 | val user = "enter_username_here_whose_media_you_want_to_download" 20 | val howManyMediasYouWantToDownload = 10 21 | val doYouWantToSaveDescriptionOfMedias = false 22 | 23 | bot.downloadUserMedias( 24 | user, 25 | howManyMediasYouWantToDownload, 26 | doYouWantToSaveDescriptionOfMedias 27 | ).collect { println(it) } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/follow/FollowUserByLocation.kt: -------------------------------------------------------------------------------- 1 | package samples.follow 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.map 8 | import kotlinx.coroutines.flow.toList 9 | import kotlinx.coroutines.runBlocking 10 | 11 | @ExperimentalCoroutinesApi 12 | fun main() = runBlocking { 13 | 14 | val username = Credentials.USERNAME 15 | val password = Credentials.PASSWORD 16 | 17 | val bot = InstagramBot() 18 | bot.prepare(username) 19 | bot.login(username, password) 20 | 21 | val locationName = "enter_location_name_here" 22 | val howManyFollowersYouWantToFollow = 10 23 | 24 | bot.followUsers( 25 | bot.getUsersByLocation(locationName, howManyFollowersYouWantToFollow) 26 | .map { it.get("pk").toString() }.toList() 27 | ).collect { println(it) } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/samples/dm/SendHashTagToTimeLineUsers.kt: -------------------------------------------------------------------------------- 1 | package samples.dm 2 | 3 | import Credentials 4 | import bot.InstagramBot 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.map 8 | import kotlinx.coroutines.flow.toList 9 | import kotlinx.coroutines.runBlocking 10 | 11 | @ExperimentalCoroutinesApi 12 | fun main() = runBlocking { 13 | 14 | val username = Credentials.USERNAME 15 | val password = Credentials.PASSWORD 16 | 17 | val bot = InstagramBot() 18 | bot.prepare(username) 19 | bot.login(username, password) 20 | 21 | val hashTagName = "enter_hashtag_name_here_without_#" 22 | val textName = "enter_text_name_to_send_along_with_hashtag" 23 | 24 | bot.sendDirectHashTagToUsers( 25 | bot.getUsersByTimeline().map { it.get("pk").toString() }.toList(), 26 | hashTagName, textName 27 | ).collect { println(it) } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/cache/LRUCache.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite.cache 2 | 3 | import com.nfeld.jsonpathlite.JsonPath 4 | import org.jetbrains.annotations.TestOnly 5 | import java.util.* 6 | 7 | internal class LRUCache(private val maxCacheSize: Int): Cache { 8 | private val map = LRUMap() 9 | 10 | @Synchronized 11 | override fun get(path: String): JsonPath? = map.get(path) 12 | 13 | @Synchronized 14 | override fun put(path: String, jsonPath: JsonPath) { 15 | map.put(path, jsonPath) 16 | } 17 | 18 | @TestOnly 19 | internal fun toList(): List> = map.toList() 20 | 21 | private inner class LRUMap : LinkedHashMap(INITIAL_CAPACITY, LOAD_FACTOR, true) { 22 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxCacheSize 23 | } 24 | 25 | companion object { 26 | private const val INITIAL_CAPACITY = 16 27 | private const val LOAD_FACTOR = 0.75f 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/cache/CacheProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite.cache 2 | 3 | object CacheProvider { 4 | 5 | private var cache: Cache? = null 6 | private var useDefault = true 7 | 8 | /** 9 | * Consumer can set this to preferred max cache size. 10 | */ 11 | @JvmStatic 12 | var maxCacheSize = 100 13 | 14 | /** 15 | * Set cache to custom implementation of [Cache]. 16 | * 17 | * @param newCache cache implementation to use, or null if no cache desired. 18 | */ 19 | @JvmStatic 20 | fun setCache(newCache: Cache?) { 21 | useDefault = false 22 | cache = newCache 23 | } 24 | 25 | internal fun getCache(): Cache? { 26 | if (cache == null && useDefault) { 27 | synchronized(this) { 28 | if (cache == null) { 29 | cache = createDefaultCache() 30 | } 31 | } 32 | } 33 | return cache 34 | } 35 | 36 | private fun createDefaultCache(): Cache = LRUCache(maxCacheSize) 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rajesh Hadiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/util/CookiePersistor.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import khttp.structures.cookie.Cookie 4 | import khttp.structures.cookie.CookieJar 5 | import java.io.File 6 | 7 | class CookiePersistor(private val resource: String) { 8 | // Check if persisted cookie is exists 9 | fun exist(): Boolean { 10 | return File(resource).exists() 11 | } 12 | 13 | // Save account and cookies to storage 14 | fun save(account: String, cookieJar: CookieJar) { 15 | val cksList = arrayListOf() 16 | cookieJar.entries.forEach { 17 | cksList.add("$it") 18 | } 19 | val cookiesString = cksList.toList().joinToString("#") 20 | File(resource).printWriter().use { out -> 21 | out.print("account->$account\ncookies->$cookiesString") 22 | } 23 | } 24 | 25 | // Load cookies and account from storage 26 | fun load(): CookieDisk { 27 | val jar = CookieJar() 28 | val split = File(resource).readText().split("\n") 29 | val account = split[0].split("->")[1] 30 | val cookiesString = split[1].split("->")[1].split("#") 31 | cookiesString.forEach { it -> 32 | val cks = Cookie(it) 33 | jar.setCookie(cks) 34 | } 35 | return CookieDisk(account, jar) 36 | } 37 | 38 | // Delete cookies 39 | fun destroy() { 40 | if (exist()) { 41 | File(resource).delete() 42 | } 43 | } 44 | } 45 | 46 | data class CookieDisk(val account: String, val cookieJar: CookieJar) 47 | -------------------------------------------------------------------------------- /src/main/kotlin/BotTest.kt: -------------------------------------------------------------------------------- 1 | import bot.InstagramBot 2 | import kotlinx.coroutines.ExperimentalCoroutinesApi 3 | import kotlinx.coroutines.InternalCoroutinesApi 4 | import kotlinx.coroutines.flow.collect 5 | import kotlinx.coroutines.runBlocking 6 | 7 | @InternalCoroutinesApi 8 | @ExperimentalCoroutinesApi 9 | fun main() = runBlocking { 10 | 11 | val username = Credentials.USERNAME 12 | val password = Credentials.PASSWORD 13 | 14 | val bot = InstagramBot() 15 | bot.prepare(username) 16 | bot.login(username, password) 17 | 18 | /** 19 | * Minimum and maximum time (in seconds) to wait before performing next action. 20 | * Actual time will be randomly generated each time between min and max time 21 | */ 22 | 23 | bot.minSleepTime = 60 24 | bot.maxSleepTime = 120 25 | 26 | // Provide your comment list here 27 | val commentsList = listOf("Sample comment 1", "Sample comment 2") 28 | // Provide your username list here 29 | val userList = listOf("user_name_1", "user_name_2") 30 | 31 | // Get all following of you 32 | bot.getSelfFollowing(Int.MAX_VALUE).collect { println(it) } 33 | // Get 100 followers of given username 34 | bot.getUserFollowers("enter_user_name_here", 100, isUsername = true).collect { println(it) } 35 | // Like 5 medias from explore page 36 | bot.likeMediasByExplorePage(5).collect { println(it) } 37 | // Comment 5 medias having given hashtag and given comment list 38 | bot.commentMediasByHashTag("enter_hashtag_name_here", commentsList, 5).collect { println(it) } 39 | // Follow given list of users 40 | bot.followUsers(userList).collect { println(it) } 41 | // Approve all pending follow requests 42 | bot.approveAllPendingFollowRequests().collect { println(it) } 43 | // Watch stories of 200 users based on given location 44 | bot.watchLocationUsersStories("enter_location_name_here", 200).collect { println(it) } 45 | // Download latest 5 medias of given user along with caption 46 | bot.downloadUserMedias("enter_user_name_here", 5, true).collect { println(it) } 47 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/util/Crypto.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import java.math.BigInteger 4 | import java.util.* 5 | import kotlin.math.roundToInt 6 | 7 | object Crypto { 8 | 9 | // Signature class 10 | data class Signature(val signed: String, val appVersion: String, val sigKeyVersion: String, val payload: String) 11 | 12 | // Signature function 13 | fun signData(payload: String): Signature { 14 | val signed = generateHMAC(KEY.SIG_KEY, payload) 15 | return Signature( 16 | signed, 17 | KEY.APP_VERSION, 18 | KEY.SIG_KEY_VERSION, 19 | payload 20 | ) 21 | } 22 | 23 | // Generate MD5 Hash of given string 24 | private fun generateMD5(s: String): String { 25 | return try { 26 | val messageDigest = java.security.MessageDigest.getInstance("MD5") 27 | messageDigest.update(s.toByteArray(), 0, s.length) 28 | BigInteger(1, messageDigest.digest()).toString(16) 29 | } catch (e: Exception) { 30 | System.err.println("Error occurred while generating MD5 $e") 31 | "" 32 | } 33 | } 34 | 35 | // Generate hash-based message authentication code of given data 36 | fun generateHMAC(key: String, data: String): String { 37 | return try { 38 | val sha256HMAC = javax.crypto.Mac.getInstance("HmacSHA256") 39 | val secretKey = javax.crypto.spec.SecretKeySpec(key.toByteArray(charset("UTF-8")), "HmacSHA256") 40 | sha256HMAC.init(secretKey) 41 | val bytes = sha256HMAC.doFinal(data.toByteArray(charset("UTF-8"))) 42 | java.lang.String.format("%040x", BigInteger(1, bytes)) 43 | } catch (e: Exception) { 44 | System.err.println("Error occurred while generating HMAC $e") 45 | "" 46 | } 47 | } 48 | 49 | // Random UUID Generator function 50 | fun generateUUID(type: Boolean): String { 51 | var uuid = UUID.randomUUID().toString() 52 | if (!type) { 53 | uuid = uuid.replace("-", "") 54 | } 55 | return uuid 56 | } 57 | 58 | // Generate temporary GUID 59 | fun generateTemporaryGUID(name: String, uuid: String, duration: Float): String { 60 | return UUID.nameUUIDFromBytes("$name$uuid${(System.currentTimeMillis() / duration).roundToInt()}".toByteArray()) 61 | .toString() 62 | } 63 | 64 | // Generate Device ID 65 | fun generateDeviceId(username: String): String { 66 | val seed = 11111 + (Math.random() * ((99999 - 11111) + 1)) 67 | val hash = generateMD5("$username$seed") 68 | return "android-${hash.substring(0, 16)}" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InstaBot 2 | Instagram bot implemented in Kotlin to perform all major operations supported by Instagram app. 3 | 4 | ## Features 5 | - Like medias 6 | - Comment medias 7 | - Direct messages 8 | - Watch stories 9 | - Download medias 10 | - Hashtag targeting 11 | - Location targeting 12 | - And more... 13 | 14 | ## Built with 15 | [Kotlin](https://kotlinlang.org/) - A modern programming language for Android/JVM that makes developers happier. 16 | 17 | [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - For asynchronous programming 18 | 19 | [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) - A cold asynchronous data stream that sequentially emits values and completes normally or with an exception. 20 | 21 | [JsonPathKt](https://github.com/codeniko/JsonPathKt) - A lighter and more efficient implementation of JsonPath in Kotlin 22 | 23 | ## Installation 24 | 25 | Add JitPack to your build.gradle.kts file 26 | ``` 27 | repositories { 28 | ... 29 | maven(url = "https://jitpack.io") 30 | } 31 | ``` 32 | 33 | Add Gradle dependency to your build.gradle.kts file 34 | ``` 35 | dependencies { 36 | implementation("com.github.hadiyarajesh:insta-bot:Tag") 37 | } 38 | ``` 39 | 40 | ## Quick start 41 | Set your Instagram username and password in [Credentials.Kt](https://github.com/hadiyarajesh/insta-bot/blob/master/src/main/kotlin/Credentials.kt) file 42 | ```kotlin 43 | object Credentials { 44 | const val USERNAME = "your_instagram_username" 45 | const val PASSWORD = "your_instagram_password" 46 | } 47 | ``` 48 | Initialize InstagramBot class with credential value and call prepare method. Then, call login method to login into instagram. (Prepare method must be called before login method) 49 | ```kotlin 50 | val username = Credentials.USERNAME 51 | val password = Credentials.PASSWORD 52 | 53 | val bot = InstagramBot() 54 | bot.prepare(username) 55 | bot.login(username, password) 56 | ``` 57 | 58 | Now you can perform any operations of your choice like. 59 | ```kotlin 60 | // Get 100 followers of you 61 | bot.getSelfFollowers(100).collect { println(it) } 62 | // Like 5 medias from explore page 63 | bot.likeMediasByExplorePage(5).collect { println(it) } 64 | // Approve all pending follow requests 65 | bot.approveAllPendingFollowRequests().collect { println(it) } 66 | // Watch stories of 200 users based on given location 67 | bot.watchLocationUsersStories("enter_location_name_here", 200).collect { println(it) } 68 | ``` 69 | 70 | For more details, refer [BotTest](https://github.com/hadiarajesh/insta-bot/blob/master/src/main/kotlin/BotTest.kt) file. 71 | 72 | ## Samples 73 | [You can find ready to use sample scripts here](https://github.com/hadiyarajesh/insta-bot/tree/master/src/main/kotlin/samples) 74 | 75 | ## Documentation 76 | [You can find documentation here](https://hadiyarajesh.github.io/docs/instagram-api/index.html) 77 | 78 | ## Contributing 79 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 80 | 81 | ## Terms and conditions 82 | - You will NOT use this API for marketing purposes (spam, botting, harassment). 83 | - We do NOT give support to anyone who wants to use this API to send spam or commit other crimes. 84 | - We reserve the right to block any user of this repository that does not meet these conditions. 85 | 86 | ## Legal 87 | This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by Instagram, Facebook inc. or any of its affiliates or subsidiaries. This is an independent and unofficial API. Use it at your own risk. 88 | 89 | ## License 90 | [MIT License](https://github.com/hadiyarajesh/insta-bot/blob/master/LICENSE) 91 | -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/JsonPath.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite 2 | 3 | import com.nfeld.jsonpathlite.cache.CacheProvider 4 | import org.json.JSONArray 5 | import org.json.JSONException 6 | import org.json.JSONObject 7 | 8 | class JsonPath(path: String) { 9 | 10 | private val path: String 11 | internal val tokens: List 12 | 13 | /** 14 | * Trim given path string and compile it on initialization 15 | */ 16 | init { 17 | this.path = path.trim() 18 | 19 | val cache = CacheProvider.getCache() 20 | val cachedJsonPath = cache?.get(this.path) 21 | if (cachedJsonPath != null) { 22 | tokens = cachedJsonPath.tokens 23 | } else { 24 | tokens = PathCompiler.compile(this.path) 25 | cache?.put(this.path, this) 26 | } 27 | } 28 | 29 | /** 30 | * Read the value at path in given JSON string 31 | * 32 | * @return Given type if value in path exists, null otherwise 33 | */ 34 | fun readFromJson(jsonString: String): T? { 35 | /* 36 | We don't need to parse this string into own JsonResult wrapper as we don't need those convenience methods at this point. 37 | Use org.json directly based on first character of given string. Also pass it to private readFromJson method directly to skip a stack frame 38 | */ 39 | val trimmedJson = jsonString.trim() 40 | return when (trimmedJson.firstOrNull()) { 41 | '{' -> _readFromJson(JSONObject(trimmedJson)) 42 | '[' -> _readFromJson(JSONArray(trimmedJson)) 43 | else -> null 44 | } 45 | } 46 | 47 | /** 48 | * Read the value at path in given JSON Object 49 | * 50 | * @return Given type if value in path exists, null otherwise 51 | */ 52 | fun readFromJson(jsonObject: JSONObject): T? = _readFromJson(jsonObject) 53 | 54 | /** 55 | * Read the value at path in given JSON Array 56 | * 57 | * @return Given type if value in path exists, null otherwise 58 | */ 59 | fun readFromJson(jsonArray: JSONArray): T? = _readFromJson(jsonArray) 60 | 61 | @Suppress("UNCHECKED_CAST") 62 | private fun _readFromJson(json: Any): T? { 63 | var valueAtPath: Any? = json 64 | tokens.forEach { token -> 65 | valueAtPath?.let { valueAtPath = token.read(it) } 66 | } 67 | val lastValue = valueAtPath 68 | if (lastValue is JSONArray && containsOnlyPrimitives(lastValue)) { 69 | valueAtPath = lastValue.toList().toList() // return immutable list 70 | } else if (lastValue == JSONObject.NULL) { 71 | return null 72 | } 73 | return valueAtPath as? T 74 | } 75 | 76 | /** 77 | * Check if a JSONArray contains only primitive values (in this case, non-JSONObject/JSONArray). 78 | */ 79 | private fun containsOnlyPrimitives(jsonArray: JSONArray) : Boolean { 80 | val it = jsonArray.iterator() 81 | if(!it.hasNext()) { 82 | return false 83 | } 84 | while (it.hasNext()) { 85 | val item = it.next() 86 | if (item is JSONObject || item is JSONArray) { 87 | return false 88 | } 89 | } 90 | return true 91 | } 92 | 93 | // private fun isSpecialChar(c: Char): Boolean { 94 | // return c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || c == 'r' || c == 't' 95 | // } 96 | 97 | companion object { 98 | /** 99 | * Parse JSON string and return successful [JsonResult] or throw [JSONException] on parsing error 100 | * 101 | * @param jsonString JSON string to parse 102 | * @return instance of parsed [JsonResult] object 103 | * @throws JSONException 104 | */ 105 | @Throws(JSONException::class) 106 | @JvmStatic 107 | fun parse(jsonString: String): JsonResult = when { 108 | jsonString.isEmpty() -> throw JSONException("JSON string is empty") 109 | jsonString.first() == '{' -> JsonObject(JSONObject(jsonString)) 110 | else -> JsonArray(JSONArray(jsonString)) 111 | } 112 | 113 | /** 114 | * Parse JSON string and return successful [JsonResult] or null otherwise 115 | * 116 | * @param jsonString JSON string to parse 117 | * @return instance of parsed [JsonResult] object or null 118 | */ 119 | @JvmStatic 120 | fun parseOrNull(jsonString: String): JsonResult? { 121 | return jsonString.firstOrNull()?.run { 122 | try { 123 | if (this == '{') { 124 | JsonObject(JSONObject(jsonString)) 125 | } else { 126 | JsonArray(JSONArray(jsonString)) 127 | } 128 | } catch (e: JSONException) { 129 | null 130 | } 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/main/kotlin/api/Request.kt: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import util.LoginException 4 | import com.nfeld.jsonpathlite.JsonPath 5 | import khttp.get 6 | import khttp.post 7 | import khttp.structures.cookie.CookieJar 8 | import util.Crypto 9 | import util.HTTP 10 | import java.io.File 11 | import kotlin.random.Random 12 | 13 | // Generic class to send GET/POST request 14 | class Request { 15 | private var url: String = "" 16 | private var data: String = "" 17 | private var isGet = true 18 | var persistedCookies: CookieJar? = null 19 | var headers = HTTP.HEADERS 20 | private var extraSignature: MutableMap? = null 21 | 22 | fun prepare( 23 | endpoint: String?, 24 | payload: String = "", 25 | header: Map? = null, 26 | extraSig: Map? = null, 27 | API_URL: String = HTTP.API_URL 28 | ): Request { 29 | url = "$API_URL$endpoint" 30 | data = payload 31 | isGet = data.isEmpty() 32 | extraSig?.let { extraSignature?.putAll(it) } 33 | header?.let { headers.putAll(it) } 34 | val extraHeaders = mapOf( 35 | "X-IG-Connection-Speed" to "-1kbps", 36 | "X-IG-Bandwidth-Speed-KBPS" to Random.Default.nextInt(7000, 10000).toString(), 37 | "X-IG-Bandwidth-TotalBytes-B" to Random.Default.nextInt(500000, 900000).toString(), 38 | "X-IG-Bandwidth-TotalTime-MS" to Random.Default.nextInt(50, 150).toString() 39 | ) 40 | headers.putAll(extraHeaders) 41 | 42 | return this 43 | } 44 | 45 | fun send(isLogin: Boolean = false): Boolean { 46 | 47 | if (!InstagramAPI.isLoggedIn && !isLogin) { 48 | throw LoginException("Please login first") 49 | } 50 | 51 | val response = if (isGet) { 52 | get(url = url, headers = headers, cookies = persistedCookies) 53 | } else { 54 | val signature = data.let { Crypto.signData(it) } 55 | val payload = mutableMapOf( 56 | "signed_body" to "${signature.signed}.${signature.payload}", 57 | "ig_sig_key_version" to signature.sigKeyVersion 58 | ) 59 | 60 | extraSignature?.let { payload.putAll(it) } 61 | post(url = url, headers = headers, data = payload, cookies = persistedCookies) 62 | } 63 | 64 | if (persistedCookies == null) { 65 | persistedCookies = response.cookies 66 | } else { 67 | persistedCookies?.putAll(response.cookies) 68 | } 69 | 70 | InstagramAPI.totalRequests += 1 71 | InstagramAPI.lastResponse = response 72 | InstagramAPI.statusCode = response.statusCode 73 | 74 | if (response.statusCode == 200) { 75 | InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) 76 | return true 77 | } else { 78 | if (response.statusCode != 404) { 79 | InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) 80 | 81 | if (InstagramAPI.lastJSON?.read("$.message") == "feedback_required") { 82 | println("ATTENTION! feedback required") 83 | } 84 | } 85 | 86 | when (response.statusCode) { 87 | 429 -> { 88 | val sleepMinutes = 5L 89 | println("Request return 429, it means too many request. I will go to sleep for $sleepMinutes minutes") 90 | Thread.sleep(sleepMinutes * 60 * 1000) 91 | } 92 | 400 -> { 93 | InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) 94 | when { 95 | InstagramAPI.lastJSON?.read("$.two_factor_required") == true -> { 96 | // Perform interactive two factor authentication 97 | return InstagramAPI.performTwoFactorAuth() 98 | } 99 | 100 | InstagramAPI.lastJSON?.read("$.message") == "challenge_required" -> { 101 | // Perform interactive challenge solving 102 | return InstagramAPI.solveChallenge() 103 | } 104 | 105 | else -> { 106 | println("Instagram's error message: ${InstagramAPI.lastJSON?.read("$.message")}, STATUS_CODE: ${response.statusCode}") 107 | return false 108 | } 109 | } 110 | } 111 | 403 -> { 112 | InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) 113 | 114 | if (InstagramAPI.lastJSON?.read("$.message") == "login_required") { 115 | println("Re-login required. Clearing cookie file") 116 | val cookieFile = File(InstagramAPI.username) 117 | if (cookieFile.exists()) { 118 | if (cookieFile.delete()) { 119 | println("Cookie file cleared successfully") 120 | } 121 | } 122 | println("Cookie file does not found") 123 | } else { 124 | println("Something went wrong. ${response.text}") 125 | } 126 | return false 127 | } 128 | 405 -> { 129 | println("This method is not allowed") 130 | return false 131 | } 132 | } 133 | } 134 | 135 | return false 136 | } 137 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/PathCompiler.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite 2 | 3 | internal object PathCompiler { 4 | 5 | /** 6 | * @param path Path string to compile 7 | * @return List of [Token] to read against a JSON 8 | */ 9 | @Throws(IllegalArgumentException::class) 10 | internal fun compile(path: String): List { 11 | if (path.firstOrNull() != '$') { 12 | throw IllegalArgumentException("First character in path must be '$' root token") 13 | } 14 | 15 | val tokens = mutableListOf() 16 | var isDeepScan = false 17 | val keyBuilder = StringBuilder() 18 | 19 | fun resetForNextToken() { 20 | isDeepScan = false 21 | keyBuilder.clear() 22 | } 23 | 24 | fun addObjectAccessorToken() { 25 | val key = keyBuilder.toString() 26 | if (isDeepScan) { 27 | tokens.add(DeepScanObjectAccessorToken(listOf(key))) 28 | } else { 29 | tokens.add(ObjectAccessorToken(key)) 30 | } 31 | } 32 | 33 | val len = path.length 34 | var i = 1 35 | while (i < len) { 36 | val c = path[i] 37 | val next = path.getOrNull(i + 1) 38 | when { 39 | c == '.' -> { 40 | if (keyBuilder.isNotEmpty()) { 41 | addObjectAccessorToken() 42 | resetForNextToken() 43 | } 44 | // check if it's followed by another dot. This means the following key will be used in deep scan 45 | if (next == '.') { 46 | isDeepScan = true 47 | ++i 48 | } else if (next == null) { 49 | throw IllegalArgumentException("Unexpected ending with dot") 50 | } 51 | } 52 | c == '[' -> { 53 | if (keyBuilder.isNotEmpty()) { 54 | addObjectAccessorToken() 55 | resetForNextToken() 56 | } 57 | val closingBracketIndex = findMatchingClosingBracket(path, i) 58 | if (closingBracketIndex > i + 1) { // i+1 checks to make sure atleast one char in the brackets 59 | val token = compileBracket(path, i, closingBracketIndex) 60 | if (isDeepScan) { 61 | val deepScanToken: Token? = when (token) { 62 | is ObjectAccessorToken -> DeepScanObjectAccessorToken(listOf(token.key)) 63 | is MultiObjectAccessorToken -> DeepScanObjectAccessorToken(token.keys) 64 | is ArrayAccessorToken -> DeepScanArrayAccessorToken(listOf(token.index)) 65 | is MultiArrayAccessorToken -> DeepScanArrayAccessorToken(token.indices) 66 | is ArrayLengthBasedRangeAccessorToken -> DeepScanLengthBasedArrayAccessorToken(token.startIndex, token.endIndex, token.offsetFromEnd) 67 | else -> null 68 | } 69 | deepScanToken?.let { tokens.add(it) } 70 | resetForNextToken() 71 | } else { 72 | tokens.add(token) 73 | } 74 | i = closingBracketIndex 75 | } else { 76 | throw IllegalArgumentException("Expecting closing array bracket with a value inside") 77 | } 78 | } 79 | else -> keyBuilder.append(c) 80 | } 81 | ++i 82 | } 83 | 84 | if (keyBuilder.isNotEmpty()) { 85 | addObjectAccessorToken() 86 | } 87 | 88 | return tokens.toList() 89 | } 90 | 91 | /** 92 | * @param path original path 93 | * @param openingIndex opening bracket index we are to search matching closing bracket for 94 | * @return closing bracket index, or -1 if not found 95 | */ 96 | internal fun findMatchingClosingBracket(path: String, openingIndex: Int): Int { 97 | var expectingClosingQuote = false 98 | var i = openingIndex + 1 99 | val len = path.length 100 | 101 | while (i < len) { 102 | val c = path[i] 103 | val next = path.getOrNull(i + 1) 104 | when { 105 | c == '\'' -> expectingClosingQuote = !expectingClosingQuote 106 | c == ']' && !expectingClosingQuote -> return i 107 | c == '\\' && expectingClosingQuote -> { 108 | if (next == '\'') { 109 | ++i // skip this char so we don't process escaped quote 110 | } else if (next == null) { 111 | throw IllegalArgumentException("Unexpected char at end of path") 112 | } 113 | } 114 | } 115 | ++i 116 | } 117 | 118 | return -1 119 | } 120 | 121 | /** 122 | * Compile path expression inside of brackets 123 | * 124 | * @param path original path 125 | * @param openingIndex index of opening bracket 126 | * @param closingIndex index of closing bracket 127 | * @return Compiled [Token] 128 | */ 129 | internal fun compileBracket(path: String, openingIndex: Int, closingIndex: Int): Token { 130 | var isObjectAccessor = false 131 | var isNegativeArrayAccessor = false // supplements isArrayAccessor 132 | var expectingClosingQuote = false 133 | var hasStartColon = false // found colon in beginning 134 | var hasEndColon = false // found colon in end 135 | var isRange = false // has starting and ending range. There will be two keys containing indices of each 136 | 137 | var i = openingIndex + 1 138 | val keys = mutableListOf() 139 | val keyBuilder = StringBuilder() 140 | 141 | fun buildAndAddKey() { 142 | var key = keyBuilder.toString() 143 | if (!isObjectAccessor && isNegativeArrayAccessor) { 144 | key = "-$key" 145 | isNegativeArrayAccessor = false 146 | } 147 | keys.add(key) 148 | keyBuilder.clear() 149 | } 150 | 151 | //TODO handle escaped chars 152 | while (i < closingIndex) { 153 | val c = path[i] 154 | 155 | when { 156 | c == ' ' && !expectingClosingQuote -> { 157 | // skip empty space that's not enclosed in quotes 158 | } 159 | 160 | c == ':' && !expectingClosingQuote -> { 161 | if (openingIndex == i - 1) { 162 | hasStartColon = true 163 | } else if (i == closingIndex - 1) { 164 | hasEndColon = true 165 | // keybuilder should have a key... 166 | buildAndAddKey() 167 | } else if (keyBuilder.isNotEmpty()) { 168 | buildAndAddKey() // becomes starting index of range 169 | isRange = true 170 | } 171 | } 172 | 173 | c == '-' && !isObjectAccessor -> { 174 | isNegativeArrayAccessor = true 175 | } 176 | 177 | c == ',' && !expectingClosingQuote -> { 178 | // object accessor would have added key on closing quote 179 | if (!isObjectAccessor && keyBuilder.isNotEmpty()) { 180 | buildAndAddKey() 181 | } 182 | } 183 | 184 | c == '\'' && expectingClosingQuote -> { // only valid inside array bracket and ending 185 | if (keyBuilder.isEmpty()) { 186 | throw IllegalArgumentException("Key is empty string") 187 | } 188 | buildAndAddKey() 189 | expectingClosingQuote = false 190 | } 191 | 192 | c == '\'' -> { 193 | expectingClosingQuote = true 194 | isObjectAccessor = true 195 | } 196 | 197 | c.isDigit() || isObjectAccessor -> keyBuilder.append(c) 198 | else -> throw IllegalArgumentException("Unexpected char, char=$c, index=$i") 199 | } 200 | 201 | ++i 202 | } 203 | 204 | if (keyBuilder.isNotEmpty()) { 205 | buildAndAddKey() 206 | } 207 | 208 | var token: Token? = null 209 | if (isObjectAccessor) { 210 | if (keys.size > 1) { 211 | token = MultiObjectAccessorToken(keys) 212 | } else { 213 | keys.firstOrNull()?.let { 214 | token = ObjectAccessorToken(it) 215 | } 216 | } 217 | } else { 218 | when { 219 | isRange -> { 220 | val start = keys[0].toInt(10) 221 | val end = keys[1].toInt(10) // exclusive 222 | val isEndNegative = end < 0 223 | token = if (start < 0 || isEndNegative) { 224 | val offsetFromEnd = if (isEndNegative) end else 0 225 | val endIndex = if (!isEndNegative) end else null 226 | ArrayLengthBasedRangeAccessorToken(start, endIndex, offsetFromEnd) 227 | } else { 228 | MultiArrayAccessorToken(IntRange(start, end - 1).toList()) 229 | } 230 | } 231 | hasStartColon -> { 232 | val end = keys[0].toInt(10) // exclusive 233 | token = if (end < 0) { 234 | ArrayLengthBasedRangeAccessorToken(0, null, end) 235 | } else { 236 | MultiArrayAccessorToken(IntRange(0, end - 1).toList()) 237 | } 238 | } 239 | hasEndColon -> { 240 | val start = keys[0].toInt(10) 241 | token = ArrayLengthBasedRangeAccessorToken(start) 242 | } 243 | keys.size == 1 -> token = ArrayAccessorToken(keys[0].toInt(10)) 244 | keys.size > 1 -> token = MultiArrayAccessorToken(keys.map { it.toInt(10) }) 245 | } 246 | } 247 | 248 | token?.let { 249 | return it 250 | } 251 | 252 | throw IllegalArgumentException("Not a valid path") 253 | } 254 | } -------------------------------------------------------------------------------- /src/main/kotlin/jsonpath/Token.kt: -------------------------------------------------------------------------------- 1 | package com.nfeld.jsonpathlite 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONObject 5 | 6 | /** 7 | * Accesses value at [index] from [JSONArray] 8 | * 9 | * @param index index to access, can be negative which means to access from end 10 | */ 11 | internal data class ArrayAccessorToken(val index: Int) : Token { 12 | override fun read(json: Any): Any? { 13 | if (json is JSONArray) { 14 | if (index < 0) { 15 | // optimized to get array length only if we're accessing from last 16 | val indexFromLast = json.length() + index 17 | if (indexFromLast >= 0) { 18 | return json.opt(indexFromLast) 19 | } 20 | } 21 | return json.opt(index) 22 | } 23 | return null 24 | } 25 | } 26 | 27 | /** 28 | * Accesses values at [indices] from [JSONArray]. When read, value returned will be [JSONArray] of values 29 | * at requested indices in given order. 30 | * 31 | * @param indices indices to access, can be negative which means to access from end 32 | */ 33 | internal data class MultiArrayAccessorToken(val indices: List) : Token { 34 | override fun read(json: Any): Any? { 35 | val result = JSONArray() 36 | 37 | if (json is JSONArray) { 38 | val jsonLength = json.length() 39 | indices.forEach { index -> 40 | if (index < 0) { 41 | val indexFromLast = jsonLength + index 42 | if (indexFromLast >= 0) { 43 | json.opt(indexFromLast)?.let { result.put(it) } 44 | } 45 | } else { 46 | json.opt(index)?.let { result.put(it) } 47 | } 48 | } 49 | return result 50 | } 51 | return null 52 | } 53 | } 54 | 55 | /** 56 | * Accesses values from [JSONArray] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. 57 | * When read, value returned will be JSONArray of values at requested indices in order of values in range. 58 | * 59 | * @param startIndex starting index of range, inclusive. Can be negative. 60 | * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] 61 | * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] 62 | */ 63 | internal data class ArrayLengthBasedRangeAccessorToken(val startIndex: Int, 64 | val endIndex: Int? = null, 65 | val offsetFromEnd: Int = 0) : Token { 66 | override fun read(json: Any): Any? { 67 | val token = if (json is JSONArray) { 68 | toMultiArrayAccessorToken(json) 69 | } else null 70 | return token?.read(json) 71 | } 72 | 73 | fun toMultiArrayAccessorToken(json: JSONArray): MultiArrayAccessorToken? { 74 | val len = json.length() 75 | val start = if (startIndex < 0) { 76 | len + startIndex 77 | } else startIndex 78 | 79 | // use endIndex if we have it, otherwise calculate from json array length 80 | val endInclusive = if (endIndex != null) { 81 | endIndex - 1 82 | } else len + offsetFromEnd - 1 83 | 84 | if (start >= 0 && endInclusive >= start) { 85 | return MultiArrayAccessorToken(IntRange(start, endInclusive).toList()) 86 | } 87 | return MultiArrayAccessorToken(emptyList()) 88 | } 89 | } 90 | 91 | /** 92 | * Accesses value at [key] from [JSONObject] 93 | * 94 | * @param index index to access, can be negative which means to access from end 95 | */ 96 | internal data class ObjectAccessorToken(val key: String) : Token { 97 | override fun read(json: Any): Any? { 98 | return if (json is JSONObject) { 99 | json.opt(key) 100 | } else null 101 | } 102 | } 103 | 104 | /** 105 | * Accesses values at [keys] from [JSONObject]. When read, value returned will be [JSONObject] 106 | * containing key/value pairs requested. Keys that are null or don't exist won't be added in Object 107 | * 108 | * @param keys keys to access for which key/values to return 109 | */ 110 | internal data class MultiObjectAccessorToken(val keys: List) : Token { 111 | override fun read(json: Any): Any? { 112 | val result = JSONObject() 113 | 114 | return if (json is JSONObject) { 115 | keys.forEach { key -> 116 | json.opt(key)?.let { 117 | result.put(key, it) 118 | } 119 | } 120 | result 121 | } else null 122 | } 123 | } 124 | 125 | /** 126 | * Recursive scan for values with keys in [targetKeys] list. Returns a [JSONArray] containing values found. 127 | * 128 | * @param targetKeys keys to find values for 129 | */ 130 | internal data class DeepScanObjectAccessorToken(val targetKeys: List) : Token { 131 | private fun scan(jsonValue: Any, result: JSONArray) { 132 | when (jsonValue) { 133 | is JSONObject -> { 134 | // first add all values from keys requested to result 135 | if (targetKeys.size > 1) { 136 | val resultToAdd = JSONObject() 137 | targetKeys.forEach { targetKey -> 138 | jsonValue.opt(targetKey)?.let { resultToAdd.put(targetKey, it) } 139 | } 140 | if (!resultToAdd.isEmpty) { 141 | result.put(resultToAdd) 142 | } 143 | } else { 144 | targetKeys.firstOrNull()?.let { key -> 145 | jsonValue.opt(key)?.let { result.put(it) } 146 | } 147 | } 148 | 149 | // recursively scan all underlying objects/arrays 150 | jsonValue.keySet().forEach { objKey -> 151 | val objValue = jsonValue.opt(objKey) 152 | if (objValue is JSONObject || objValue is JSONArray) { 153 | scan(objValue, result) 154 | } 155 | } 156 | } 157 | is JSONArray -> { 158 | jsonValue.forEach { 159 | if (it is JSONObject || it is JSONArray) { 160 | scan(it, result) 161 | } 162 | } 163 | } 164 | else -> {} 165 | } 166 | } 167 | 168 | override fun read(json: Any): Any? { 169 | val result = JSONArray() 170 | scan(json, result) 171 | return result 172 | } 173 | } 174 | 175 | /** 176 | * Recursive scan for values/objects/arrays found for all [indices] specified. Returns a [JSONArray] containing results found. 177 | * 178 | * @param indices indices to retrieve values/objects for 179 | */ 180 | internal data class DeepScanArrayAccessorToken(val indices: List) : Token { 181 | private fun scan(jsonValue: Any, result: JSONArray) { 182 | when (jsonValue) { 183 | is JSONObject -> { 184 | // traverse all key/value pairs and recursively scan underlying objects/arrays 185 | jsonValue.keySet().forEach { objKey -> 186 | val objValue = jsonValue.opt(objKey) 187 | if (objValue is JSONObject || objValue is JSONArray) { 188 | scan(objValue, result) 189 | } 190 | } 191 | } 192 | is JSONArray -> { 193 | // first add all requested indices to our results 194 | indices.forEach { index -> 195 | ArrayAccessorToken(index).read(jsonValue)?.let { result.put(it) } 196 | } 197 | 198 | // now recursively scan underlying objects/arrays 199 | jsonValue.forEach { 200 | if (it is JSONObject || it is JSONArray) { 201 | scan(it, result) 202 | } 203 | } 204 | } 205 | else -> {} 206 | } 207 | } 208 | 209 | override fun read(json: Any): Any? { 210 | val result = JSONArray() 211 | scan(json, result) 212 | return result 213 | } 214 | } 215 | 216 | 217 | /** 218 | * Recursive scan for values/objects/arrays from [JSONArray] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. 219 | * When read, value returned will be JSONArray of values at requested indices in order of values in range. Returns a [JSONArray] containing results found. 220 | * 221 | * @param startIndex starting index of range, inclusive. Can be negative. 222 | * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] 223 | * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] 224 | */ 225 | internal data class DeepScanLengthBasedArrayAccessorToken(val startIndex: Int, 226 | val endIndex: Int? = null, 227 | val offsetFromEnd: Int = 0) : Token { 228 | private fun scan(jsonValue: Any, result: JSONArray) { 229 | when (jsonValue) { 230 | is JSONObject -> { 231 | // traverse all key/value pairs and recursively scan underlying objects/arrays 232 | jsonValue.keySet().forEach { objKey -> 233 | val objValue = jsonValue.opt(objKey) 234 | if (objValue is JSONObject || objValue is JSONArray) { 235 | scan(objValue, result) 236 | } 237 | } 238 | } 239 | is JSONArray -> { 240 | ArrayLengthBasedRangeAccessorToken(startIndex, endIndex, offsetFromEnd) 241 | .toMultiArrayAccessorToken(jsonValue) 242 | ?.read(jsonValue) 243 | ?.let { resultAny -> 244 | val resultArray = resultAny as? JSONArray 245 | resultArray?.forEach { result.put(it) } 246 | } 247 | 248 | // now recursively scan underlying objects/arrays 249 | jsonValue.forEach { 250 | if (it is JSONObject || it is JSONArray) { 251 | scan(it, result) 252 | } 253 | } 254 | } 255 | else -> {} 256 | } 257 | } 258 | 259 | override fun read(json: Any): Any? { 260 | val result = JSONArray() 261 | scan(json, result) 262 | return result 263 | } 264 | } 265 | 266 | internal interface Token { 267 | /** 268 | * Takes in JSONObject/JSONArray and outputs next JSONObject/JSONArray or value by evaluating token against current object/array in path 269 | * Unfortunately needs to be done with Any since [org.json.JSONObject] and [org.json.JSONArray] do not implement a common interface :( 270 | * 271 | * @param json [JSONObject] or [JSONArray] 272 | * @return [JSONObject], [JSONArray], or any JSON primitive value 273 | */ 274 | fun read(json: Any): Any? 275 | } -------------------------------------------------------------------------------- /src/main/kotlin/util/Config.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import api.InstagramAPI 4 | 5 | /* 6 | Configuration file of the project 7 | */ 8 | 9 | // Instagram Secret Key 10 | object KEY { 11 | const val SIG_KEY: String = "5f3e50f435583c9ae626302a71f7340044087a7e2c60adacfc254205a993e305" 12 | const val SIG_KEY_VERSION: String = "4" 13 | const val APP_VERSION: String = "105.0.0.18.119" 14 | } 15 | 16 | // HTTP HOST and HEADERS 17 | object HTTP { 18 | private const val HOST_NAME = "i.instagram.com" 19 | private const val HOST = "https://$HOST_NAME/" 20 | const val API_URL = "${HOST}api/v1/" 21 | 22 | val HEADERS = mutableMapOf( 23 | "User-Agent" to USER_AGENT, 24 | "Connection" to "Keep-Alive", 25 | "X-Pigeon-Session-Id" to Crypto.generateTemporaryGUID( 26 | "pigeonSessionId", 27 | InstagramAPI.uuid, 28 | 1200000f 29 | ), 30 | "X-Pigeon-Rawclienttime" to "%.3f".format(System.currentTimeMillis() / 1000f), 31 | "X-IG-Capabilities" to "IT7nCQ==", 32 | "X-IG-App-ID" to "567067343352427", 33 | "X-IG-Connection-Type" to "WIFI", 34 | "X-IG-Prefetch-Request" to "foreground", 35 | "X-IG-VP9-Capable" to "false", 36 | "X-FB-HTTP-Engine" to "Liger", 37 | "Accept" to "*/*", 38 | "Accept-Encoding" to "gzip,deflate", 39 | "Accept-Language" to "en-US", 40 | "Content-type" to "application/x-www-form-urlencoded; charset=UTF-8", 41 | "Cookie2" to "\$Version=1" 42 | ) 43 | } 44 | 45 | val USER_AGENT: String = "Instagram ${Devices.DEFAULT_DEVICE.instagramVersion} " + 46 | "Android (${Devices.DEFAULT_DEVICE.androidVersion}/${Devices.DEFAULT_DEVICE.androidRelease}; " + 47 | "${Devices.DEFAULT_DEVICE.dpi}; ${Devices.DEFAULT_DEVICE.resolution}; ${Devices.DEFAULT_DEVICE.manufacturer}; " + 48 | "${Devices.DEFAULT_DEVICE.device}; ${Devices.DEFAULT_DEVICE.model}; ${Devices.DEFAULT_DEVICE.cpu}; en_US)" 49 | 50 | 51 | // File names 52 | object FILES { 53 | const val DATA_DIRECTORY_NAME = "data" 54 | const val MAX_ACTIONS_FILE = "max_actions.txt" 55 | const val TOTAL_ACTIONS_FILE = "total_actions.txt" 56 | const val BLOCK_ACTION_FILE = "block_action.txt" 57 | const val LIKES_FILE = "likes.txt" 58 | const val UNLIKES_FILE = "unlikes.txt" 59 | const val COMMENTS_FILE = "comments.txt" 60 | const val FOLLOWED_FILE = "followed.txt" 61 | const val UNFOLLOWED_FILE = "unfollowed.txt" 62 | const val BLOCKS_FILE = "blocks.txt" 63 | const val UNBLOCKS_FILE = "unblocks.txt" 64 | const val ARCHIVED_FILE = "archived.txt" 65 | const val UNARCHIVED_FILE = "unarchived.txt" 66 | const val MESSAGES_FILE = "messages.txt" 67 | const val STORIES_FILE = "stories.txt" 68 | } 69 | 70 | // Action names 71 | object ACTIONS { 72 | const val LIKES = "likes" 73 | const val UNLIKES = "unlikes" 74 | const val COMMENTS = "comments" 75 | const val FOLLOWS = "follows" 76 | const val UNFOLLOWS = "unfollows" 77 | const val BLOCKS = "blocks" 78 | const val UNBLOCKS = "unblocks" 79 | const val MESSAGES = "messages" 80 | const val ARCHIVED = "archived" 81 | const val UNARCHIVED = "unarchived" 82 | const val STORIES_VIEWED = "stories_viewed" 83 | } 84 | 85 | // Enum class for direct messaging items 86 | enum class ITEMTYPE { 87 | TEXT, 88 | MEDIA, 89 | HASHTAG, 90 | PROFILE, 91 | LIKE, 92 | PHOTO 93 | } 94 | 95 | object EXPERIMENTS { 96 | const val EXPERIMENTS: String = 97 | "ig_android_sticker_search_explorations,android_ig_camera_ar_asset_manager_improvements_universe,ig_android_stories_seen_state_serialization,ig_stories_photo_time_duration_universe,ig_android_bitmap_cache_executor_size,ig_android_stories_music_search_typeahead,ig_android_delayed_comments,ig_android_switch_back_option,ig_android_video_profiler_loom_traces,ig_android_paid_branded_content_rendering,ig_android_direct_app_reel_grid_search,ig_android_stories_no_inflation_on_app_start,ig_android_camera_sdk_check_gl_surface_r2,ig_promote_review_screen_title_universe,ig_android_direct_newer_single_line_composer_universe,ig_direct_holdout_h1_2019,ig_explore_2019_h1_destination_cover,ig_android_direct_stories_in_direct_inbox,ig_fb_graph_differentiation_no_fb_data,ig_android_recyclerview_binder_group_enabled_universe,ig_android_direct_share_sheet_custom_fast_scroller,ig_android_video_exoplayer_2,ig_android_shopping_channel_in_explore,ig_android_stories_music_filters,ig_android_2018_h1_hashtag_report_universe,ig_android_live_replay_highlights_universe,ig_android_hashtag_page_reduced_related_items,ig_android_live_titles_broadcaster_side_create_title_universe,ig_android_fbns_preload_direct_universe,ig_android_prefetch_carousels_on_swipe_universe,ig_camera_network_activity_logger,ig_camera_remove_display_rotation_cb_universe,ig_android_interactions_migrate_inline_composer_to_viewpoint_universe,ig_android_realtime_always_start_connection_on_condition_universe,ig_android_ad_leadgen_single_screen_universe,ig_android_enable_zero_rating,ig_android_import_page_post_after_biz_conversion,ig_camera_ar_effect_attribution_position,ig_android_vc_call_ended_cleanup_universe,ig_stories_engagement_holdout_2019_h1_universe,ig_android_story_import_intent,ig_direct_report_conversation_universe,ig_biz_graph_connection_universe,ig_android_codec_high_profile,ig_android_nametag,ig_android_sso_family_key_universe,ig_android_parse_direct_messages_bytes_universe,ig_hashtag_creation_universe,ig_android_gallery_order_by_date_taken,ig_android_igtv_reshare,ig_end_of_feed_universe,ig_android_share_others_post_reorder,ig_android_additional_contact_in_nux,ig_android_live_use_all_preview_sizes,ig_android_clarify_invite_options,ig_android_live_align_by_2_universe,ig_android_separate_network_executor,ig_android_realtime_manager_optimization,ig_android_auto_advance_su_unit_when_scrolled_off_screen,ig_android_network_cancellation,ig_android_media_as_sticker,ig_android_stories_video_prefetch_kb,ig_android_maintabfragment,ig_inventory_connections,ig_stories_injection_tool_enabled_universe,ig_android_stories_disable_highlights_media_preloading,ig_android_live_start_broadcast_optimized_universe,ig_android_stories_question_response_mutation_universe,ig_android_onetap_upsell_change_pwd,ig_nametag_data_collection,ig_android_disable_scroll_listeners,ig_android_persistent_nux,ig_android_igtv_audio_always_on,ig_android_enable_liger_preconnect_universe,ig_android_persistent_duplicate_notif_checker_user_based,ig_android_rate_limit_mediafeedviewablehelper,ig_android_search_remove_null_state_sections,ig_android_stories_viewer_drawable_cache_universe,ig_direct_android_reply_modal_universe,ig_android_biz_qp_suggest_page,ig_shopping_indicator_content_variations_android,ig_android_stories_reel_media_item_automatic_retry,ig_fb_notification_universe,ig_android_live_disable_speed_test_ui_timeout_universe,ig_android_direct_thread_scroll_perf_oncreate_universe,ig_android_low_data_mode_backup_2,ig_android_invite_xout_universe,ig_android_low_data_mode_backup_3,ig_android_low_data_mode_backup_4,ig_android_low_data_mode_backup_5,ig_android_video_abr_universe,ig_android_low_data_mode_backup_1,ig_android_signup_refactor_santity,ig_challenge_general_v2,ig_android_place_signature_universe,ig_android_hide_button_for_invite_facebook_friends,ig_android_business_promote_tooltip,ig_android_follow_requests_ui_improvements,ig_android_shopping_post_tagging_nux_universe,ig_android_stories_sensitivity_screen,ig_android_camera_arengine_shader_caching_universe,ig_android_insta_video_broadcaster_infra_perf,ig_android_direct_view_more_qe,ig_android_direct_visual_message_prefetch_count_universe,ig_camera_android_ar_effect_stories_deeplink,ig_android_client_side_delivery_universe,ig_android_stories_send_client_reels_on_tray_fetch_universe,ig_android_direct_inbox_background_view_models,ig_android_startup_thread_priority,ig_android_stories_viewer_responsiveness_universe,ig_android_live_use_rtc_upload_universe,ig_android_live_ama_viewer_universe,ig_android_business_id_conversion_universe,ig_smb_ads_holdout_2018_h2_universe,ig_android_modal_activity_no_animation_fix_universe,ig_android_camera_post_smile_low_end_universe,ig_android_live_realtime_comments_universe,ig_android_vc_in_app_notification_universe,ig_eof_caboose_universe,ig_android_new_one_tap_nux_universe,ig_android_igds_edit_profile_fields,ig_android_downgrade_viewport_exit_behavior,ig_android_mi_batch_upload_universe,ig_camera_android_segmentation_async_universe,ig_android_use_recyclerview_for_direct_search_universe,ig_android_live_comment_fetch_frequency_universe,ig_android_create_page_on_top_universe,ig_android_direct_log_badge_count_inconsistent,ig_android_stories_text_format_emphasis,ig_android_question_sticker_replied_state,ig_android_ad_connection_manager_universe,ig_android_image_upload_skip_queue_only_on_wifi,ig_android_ad_watchbrowse_carousel_universe,ig_android_interactions_show_verified_badge_for_preview_comments_universe,ig_stories_question_sticker_music_format_prompt,ig_android_activity_feed_row_click,ig_android_hide_crashing_newsfeed_story_t38131972,ig_android_video_upload_quality_qe1,ig_android_save_collaborative_collections,ig_android_location_attribution_text,ig_camera_android_profile_ar_notification_universe,coupon_price_test_boost_instagram_media_acquisition_universe,ig_android_video_outputsurface_handlerthread_universe,ig_android_country_code_fix_universe,ig_perf_android_holdout_2018_h1,ig_android_stories_music_overlay,ig_android_enable_lean_crash_reporting_universe,ig_android_resumable_downloads_logging_universe,ig_android_stories_default_rear_camera_universe,ig_android_low_latency_consumption_universe,ig_android_offline_mode_holdout,ig_android_foreground_location_collection,ig_android_stories_close_friends_disable_first_time_badge,ig_android_react_native_universe_kill_switch,ig_android_video_ta_universe,ig_android_media_rows_async_inflate,ig_android_stories_gallery_video_segmentation,ig_android_stories_in_feed_preview_notify_fix_universe,ig_android_video_rebind_force_keep_playing_fix,ig_android_direct_business_holdout,ig_android_xposting_upsell_directly_after_sharing_to_story,ig_android_gallery_high_quality_photo_thumbnails,ig_android_interactions_new_comment_like_pos_universe,ig_feed_core_experience_universe,ig_android_friends_sticker,ig_android_business_ix_universe,ig_android_suggested_highlights,ig_android_stories_posting_offline_ui,ig_android_stories_close_friends_rings_remove_green_universe,ig_android_canvas_tilt_to_pan_universe,ig_android_vc_background_call_toast_universe,ig_android_concurrent_cold_start_universe,ig_promote_default_destination_universe,mi_viewpoint_viewability_universe,ig_android_location_page_info_page_upsell,igds_android_listrow_migration_universe,ig_direct_reshare_sharesheet_ranking,ig_android_fb_sync_options_universe,ig_android_drawable_usage_logging_universe,ig_android_recommend_accounts_destination_routing_fix,ig_android_fix_prepare_direct_push,ig_direct_android_larger_media_reshare_style,ig_android_video_feed_universe,ig_android_building_aymf_universe,ig_android_internal_sticker_universe,ig_traffic_routing_universe,ig_android_search_normalization,ig_android_ad_watchmore_entry_point_universe,ig_camera_android_segmentation_enabled_universe,ig_android_igtv_always_show_browse_ui,ig_android_page_claim_deeplink_qe,ig_explore_2018_h2_account_rec_deduplication_android,ig_android_story_accidentally_click_investigation_universe,ig_android_shopping_pdp_hero_carousel,ig_android_clear_inflight_image_request,ig_android_show_su_in_other_users_follow_list,ig_android_stories_infeed_lower_threshold_launch,ig_android_main_feed_video_countdown_timer,instagram_interests_holdout,ig_android_continuous_video_capture,ig_android_category_search_edit_profile,ig_android_contact_invites_nux_universe,ig_android_settings_search_v2_universe,ig_android_video_upload_iframe_interval,ig_business_new_value_prop_universe,ig_android_power_metrics,ig_android_stories_collapse_seen_segments,ig_android_live_follow_from_comments_universe,ig_android_hashtag_discover_tab,ig_android_live_skip_live_encoder_pts_correction,ig_android_reel_zoom_universe,enable_creator_account_conversion_v0_universe,ig_android_test_not_signing_address_book_unlink_endpoint,ig_android_direct_tabbed_media_picker,ig_android_direct_mutation_manager_job_scheduler,ig_ei_option_setting_universe,ig_android_hashtag_related_items_over_logging,ig_android_livewith_liveswap_optimization_universe,ig_android_direct_new_intro_card,ig_camera_android_supported_capabilities_api_universe,ig_android_video_webrtc_textureview,ig_android_share_claim_page_universe,ig_direct_android_mentions_sender,ig_android_whats_app_contact_invite_universe,ig_android_video_scrubber_thumbnail_universe,ig_camera_ar_image_transform_library,ig_android_insights_creation_growth_universe,ig_android_igtv_refresh_tv_guide_interval,ig_android_stories_gif_sticker,ig_android_stories_music_broadcast_receiver,ig_android_fb_profile_integration_fbnc_universe,ig_android_low_data_mode,ig_fb_graph_differentiation_control,ig_android_show_create_content_pages_universe,ig_android_igsystrace_universe,ig_android_new_contact_invites_entry_points_universe,ig_android_ccu_jobscheduler_inner,ig_android_netego_scroll_perf,ig_android_fb_connect_follow_invite_flow,ig_android_invite_list_button_redesign_universe,ig_android_react_native_email_sms_settings_universe,ig_android_igtv_aspect_ratio_limits,ig_hero_player,ig_android_save_auto_sharing_to_fb_option_on_server,ig_android_live_presence_universe,ig_android_whitehat_options_universe,android_cameracore_preview_frame_listener2_ig_universe,ig_android_memory_manager,ig_account_recs_in_chaining,ig_explore_2018_finite_chain_android_universe,ig_android_tagging_video_preview,ig_android_feed_survey_viewpoint,ig_android_hashtag_search_suggestions,ig_android_profile_neue_infra_rollout_universe,ig_android_instacrash_detection,ig_android_interactions_add_search_bar_to_likes_list_universe,ig_android_vc_capture_universe,ig_nametag_local_ocr_universe,ig_branded_content_share_to_facebook,ig_android_direct_segmented_video,ig_android_search_page_v2,ig_android_stories_recently_captured_universe,ig_business_integrity_ipc_universe,ig_android_share_product_universe,ig_fb_graph_differentiation_top_k_fb_coefficients,ig_shopping_viewer_share_action,ig_android_direct_share_story_to_facebook,ig_android_business_attribute_sync,ig_android_video_time_to_live_cache_eviction,ig_android_location_feed_related_business,ig_android_view_and_likes_cta_universe,ig_live_holdout_h2_2018,ig_android_profile_memories_universe,ig_promote_budget_warning_view_universe,ig_android_redirect_to_web_on_oembed_fail_universe,ig_android_optic_new_focus_controller,ig_android_shortcuts,ig_android_search_hashtag_badges,ig_android_navigation_latency_logger,ig_android_direct_composer_avoid_hiding_thread_camera,ig_android_direct_remix_visual_messages,ig_android_custom_story_import_intent,ig_android_biz_new_choose_category,ig_android_view_info_universe,ig_android_camera_upsell_dialog,ig_android_business_ix_self_serve,ig_android_dead_code_detection,ig_android_ad_watchbrowse_universe,ig_android_pbia_proxy_profile_universe,ig_android_qp_kill_switch,ig_android_gap_rule_enforcer_universe,ig_android_direct_delete_or_block_from_message_requests,ig_android_direct_left_aligned_navigation_bar,ig_android_feed_load_more_viewpoint_universe,ig_android_stories_reshare_reply_msg,ig_android_one_tap_sharesheet_fb_extensions,ig_android_stories_feeback_message_composer_entry_point,ig_direct_holdout_h2_2018,ig_camera_android_facetracker_v12_universe,ig_android_camera_ar_effects_low_storage_universe,ig_camera_android_black_feed_sticker_fix_universe,ig_android_direct_media_forwarding,ig_android_camera_attribution_in_direct,ig_android_audience_control,ig_android_stories_cross_sharing_to_fb_holdout_universe,ig_android_enable_main_feed_reel_tray_preloading,ig_android_profile_neue_universe,ig_company_profile_holdout,ig_camera_android_areffect_photo_capture_universe,ig_rti_inapp_notifications_universe,ig_android_vc_join_timeout_universe,ig_android_feed_core_ads_2019_h1_holdout_universe,ig_android_interactions_composer_mention_search_universe,ig_android_igtv_save,ig_android_follower_following_whatsapp_invite_universe,ig_android_claim_location_page,ig_android_story_ads_2019_h1_holdout_universe,ig_android_3pspp,ig_android_cache_timespan_objects,ig_timestamp_public_test,ig_android_histogram_reporter,ig_android_feed_auto_share_to_facebook_dialog,ig_android_arengine_separate_prepare,ig_android_skip_button_content_on_connect_fb_universe,ig_android_igtv_profile_tab,ig_android_show_fb_name_universe,ig_android_interactions_inline_composer_extensions_universe,ig_camera_async_space_validation_for_ar,ig_android_pigeon_sampling,ig_story_camera_reverse_video_experiment,ig_android_live_use_timestamp_normalizer,ig_android_profile_lazy_load_carousel_media,ig_android_stories_question_sticker_music_format,ig_business_profile_18h1_holdout_universe,ig_pacing_overriding_universe,ig_android_direct_allow_multiline_composition,ig_android_interactions_emoji_extension_followup_universe,ig_android_story_ads_direct_cta_universe,ig_android_q3lc_transparency_control_settings,ig_stories_selfie_sticker,ig_android_sso_use_trustedapp_universe,ig_android_ad_increase_story_adpreload_priority_universe,ig_android_interests_netego_dismiss,ig_direct_giphy_gifs_rating,ig_android_shopping_catalogsearch,ig_android_stories_music_awareness_universe,ig_android_qcc_perf,ig_android_stories_reels_tray_media_count_check,ig_android_new_fb_page_selection,ig_android_facebook_crosspost,ig_android_internal_collab_save,ig_video_holdout_h2_2017,ig_android_story_sharing_universe,ig_promote_post_insights_entry_universe,ig_android_direct_thread_store_rewrite,ig_android_qp_clash_management_enabled_v4_universe,ig_branded_content_paid_branded_content,ig_android_large_heap_override,ig_android_live_subscribe_user_level_universe,ig_android_igtv_creation_flow,ig_android_video_call_finish_universe,ig_android_direct_mqtt_send,ig_android_do_not_fetch_follow_requests_on_success,ig_android_remove_push_notifications,ig_android_vc_directapp_integration_universe,ig_android_explore_discover_people_entry_point_universe,ig_android_sonar_prober_universe,ig_android_live_bg_download_face_filter_assets_universe,ig_android_gif_framerate_throttling,ig_android_live_webrtc_livewith_params,ig_android_vc_always_start_connection_on_condition_universe,ig_camera_worldtracking_set_scale_by_arclass,ig_android_direct_inbox_typing_indicator,ig_android_stories_music_lyrics_scrubber,ig_feed_experience,ig_android_direct_new_thread_local_search_fix_universe,ig_android_appstate_logger,ig_promote_insights_video_views_universe,ig_android_dismiss_recent_searches,ig_android_downloadable_igrtc_module,ig_android_fb_link_ui_polish_universe,ig_stories_music_sticker,ig_android_device_capability_framework,ig_scroll_by_two_cards_for_suggested_invite_universe,ig_android_stories_helium_balloon_badging_universe,ig_android_business_remove_unowned_fb_pages,ig_android_stories_combined_asset_search,ig_stories_allow_camera_actions_while_recording,ig_android_analytics_mark_events_as_offscreen,ig_android_optic_feature_testing,ig_android_camera_universe,ig_android_optic_photo_cropping_fixes,ig_camera_regiontracking_use_similarity_tracker_for_scaling,ig_android_refreshable_list_view_check_spring,felix_android_video_quality,ig_android_biz_endpoint_switch,ig_android_direct_continuous_capture,ig_android_comments_direct_reply_to_author,ig_android_vc_webrtc_params,ig_android_claim_or_connect_page_on_xpost,ig_android_anr,ig_android_optic_new_architecture,ig_android_stories_viewer_as_modal_high_end_launch,ig_android_hashtag_follow_chaining_over_logging,ig_new_eof_demarcator_universe,ig_android_push_notifications_settings_redesign_universe,ig_hashtag_display_universe,ig_fbns_push,coupon_price_test_ad4ad_instagram_resurrection_universe,ig_android_live_rendering_looper_universe,ig_android_mqtt_cookie_auth_memcache_universe,ig_android_live_end_redirect_universe,ig_android_direct_mutation_manager_media_2,ig_android_ccu_jobscheduler_outer,ig_smb_ads_holdout_2019_h1_universe,ig_fb_graph_differentiation,ig_android_stories_share_extension_video_segmentation,ig_android_interactions_realtime_typing_indicator_and_live_comments,ig_android_stories_create_flow_favorites_tooltip,ig_android_live_nerd_stats_universe,ig_android_universe_video_production,ig_android_hide_reset_with_fb_universe,ig_android_reactive_feed_like_count,ig_android_stories_music_precapture,ig_android_vc_service_crash_fix_universe,ig_android_shopping_product_overlay,ig_android_direct_double_tap_to_like_hearts,ig_camera_android_api_rewrite_universe,ig_android_growth_fci_team_holdout_universe,ig_android_stories_gallery_recyclerview_kit_universe,ig_android_story_ads_instant_sub_impression_universe,ig_business_signup_biz_id_universe,ig_android_save_all,ig_android_main_feed_fragment_scroll_timing_histogram_uni,ig_android_ttcp_improvements,ig_android_camera_ar_platform_profile_universe,ig_explore_2018_topic_channel_navigation_android_universe,ig_android_live_fault_tolerance_universe,ig_android_stories_viewer_tall_android_cap_media_universe,native_contact_invites_universe,ig_android_dash_script,ig_android_insights_media_hashtag_insight_universe,ig_camera_fast_tti_universe,ig_android_stories_whatsapp_share,ig_android_inappnotification_rootactivity_tweak,ig_android_render_thread_memory_leak_holdout,ig_android_private_highlights_universe,ig_android_rate_limit_feed_video_module,ig_android_one_tap_fbshare,ig_share_to_story_toggle_include_shopping_product,ig_android_direct_speed_cam_univ,ig_payments_billing_address,ig_android_ufiv3_holdout,ig_android_new_camera_design_container_animations_universe,ig_android_livewith_guest_adaptive_camera_universe,ig_android_direct_fix_playing_invalid_visual_message,ig_shopping_viewer_intent_actions,ig_promote_add_payment_navigation_universe,ig_android_optic_disable_post_capture_preview_restart,ig_android_main_feed_refresh_style_universe,ig_android_live_analytics,ig_android_story_ads_performance_universe_1,ig_android_stories_viewer_modal_activity,ig_android_story_ads_performance_universe_3,ig_android_story_ads_performance_universe_4,ig_android_feed_seen_state_with_view_info,ig_android_ads_profile_cta_feed_universe,ig_android_vc_cowatch_universe,ig_android_optic_thread_priorities,ig_android_igtv_chaining,ig_android_live_qa_viewer_v1_universe,ig_android_stories_show_story_not_available_error_msg,ig_android_inline_notifications_recommended_user,ig_shopping_post_insights,ig_android_webrtc_streamid_salt_universe,ig_android_wellbeing_timeinapp_v1_universe,ig_android_profile_cta_v3,ig_android_video_qp_logger_universe,ig_android_cache_video_autoplay_checker,ig_android_live_suggested_live_expansion,ig_android_vc_start_from_direct_inbox_universe,ig_perf_android_holdout,ig_fb_graph_differentiation_only_fb_candidates,ig_android_expired_build_lockout,ig_promote_lotus_universe,ig_android_video_streaming_upload_universe,ig_android_optic_fast_preview_restart_listener,ig_interactions_h1_2019_team_holdout_universe,ig_android_ad_async_ads_universe,ig_camera_android_effect_info_bottom_sheet_universe,ig_android_stories_feedback_badging_universe,ig_android_sorting_on_self_following_universe,ig_android_edit_location_page_info,ig_promote_are_you_sure_universe,ig_android_interactions_feed_label_below_comments_refactor_universe,ig_android_camera_platform_effect_share_universe,ig_stories_engagement_swipe_animation_simple_universe,ig_login_activity,ig_android_direct_quick_replies,ig_android_fbns_optimization_universe,ig_android_stories_alignment_guides_universe,ig_android_rn_ads_manager_universe,ig_explore_2018_post_chaining_account_recs_dedupe_universe,ig_android_click_to_direct_story_reaction_universe,ig_internal_research_settings,ig_android_stories_video_seeking_audio_bug_fix,ig_android_insights_holdout,ig_android_swipe_up_area_universe,ig_android_rendering_controls,ig_android_feed_post_sticker,ig_android_inline_editing_local_prefill,ig_android_hybrid_bitmap_v3_prenougat,ig_android_cronet_stack,ig_android_enable_igrtc_module,ig_android_scroll_audio_priority,ig_android_shopping_product_appeals_universe,ig_android_fb_follow_server_linkage_universe,ig_android_fblocation_universe,ig_android_direct_updated_story_reference_ui,ig_camera_holdout_h1_2018_product,live_with_request_to_join_button_universe,ig_android_music_continuous_capture,ig_android_churned_find_friends_redirect_to_discover_people,ig_android_main_feed_new_posts_indicator_universe,ig_vp9_hd_blacklist,ig_ios_queue_time_qpl_universe,ig_android_split_contacts_list,ig_android_connect_owned_page_universe,ig_android_felix_prefetch_thumbnail_sprite_sheet,ig_android_multi_dex_class_loader_v2,ig_android_watch_and_more_redesign,igtv_feed_previews,ig_android_qp_batch_fetch_caching_enabled_v1_universe,ig_android_profile_edit_phone_universe,ig_android_vc_renderer_type_universe,ig_android_local_2018_h2_holdout,ig_android_purx_native_checkout_universe,ig_android_vc_disable_lock_screen_content_access_universe,ig_android_business_transaction_in_stories_creator,android_cameracore_ard_ig_integration,ig_video_experimental_encoding_consumption_universe,ig_android_iab_autofill,ig_android_location_page_intent_survey,ig_camera_android_segmentation_qe2_universe,ig_android_image_mem_cache_strong_ref_universe,ig_android_business_promote_refresh_fb_access_token_universe,ig_android_stories_samsung_sharing_integration,ig_android_hashtag_header_display,ig_discovery_holdout_2019_h1_universe,ig_android_user_url_deeplink_fbpage_endpoint,ig_android_direct_mutation_manager_handler_thread_universe,ig_branded_content_show_settings_universe,ig_android_ad_holdout_watchandmore_universe,ig_android_direct_thread_green_dot_presence_universe,ig_android_camera_new_post_smile_universe,ig_android_shopping_signup_redesign_universe,ig_android_vc_missed_call_notification_action_reply,allow_publish_page_universe,ig_android_experimental_onetap_dialogs_universe,ig_promote_ppe_v2_universe,android_cameracore_ig_gl_oom_fixes_universe,ig_android_multi_capture_camera,ig_android_fb_family_navigation_badging_user,ig_android_follow_requests_copy_improvements,ig_media_geo_gating,ig_android_comments_notifications_universe,ig_android_render_output_surface_timeout_universe,ig_android_drop_frame_check_paused,ig_direct_raven_sharesheet_ranking,ig_android_realtime_mqtt_logging,ig_family_bridges_holdout_universe,ig_android_rainbow_hashtags,ig_android_ad_watchinstall_universe,ig_android_ad_account_top_followers_universe,ig_android_betamap_universe,ig_android_video_ssim_report_universe,ig_android_cache_network_util,ig_android_leak_detector_upload_universe,ig_android_carousel_prefetch_bumping,ig_fbns_preload_default,ig_android_inline_appeal_show_new_content,ig_fbns_kill_switch,ig_hashtag_following_holdout_universe,ig_android_show_weekly_ci_upsell_limit,ig_android_direct_reel_options_entry_point_2_universe,enable_creator_account_conversion_v0_animation,ig_android_http_service_same_thread,ig_camera_holdout_h1_2018_performance,ig_android_direct_mutation_manager_cancel_fix_universe,ig_music_dash,ig_android_fb_url_universe,ig_android_reel_raven_video_segmented_upload_universe,ig_android_promote_native_migration_universe,ig_camera_android_badge_face_effects_universe,ig_android_hybrid_bitmap_v3_nougat,ig_android_multi_author_story_reshare_universe,ig_android_vc_camera_zoom_universe,ig_android_enable_request_compression_ccu,ig_android_video_controls_universe,ig_android_logging_metric_universe_v2,ig_android_xposting_newly_fbc_people,ig_android_visualcomposer_inapp_notification_universe,ig_android_contact_point_upload_rate_limit_killswitch,ig_android_webrtc_encoder_factory_universe,ig_android_search_impression_logging,ig_android_handle_username_in_media_urls_universe,ig_android_sso_kototoro_app_universe,ig_android_mi_holdout_h1_2019,ig_android_igtv_autoplay_on_prepare,ig_file_based_session_handler_2_universe,ig_branded_content_tagging_upsell,ig_shopping_insights_parity_universe_android,ig_android_live_ama_universe,ig_android_external_gallery_import_affordance,ig_android_updatelistview_on_loadmore,ig_android_optic_new_zoom_controller,ig_android_hide_type_mode_camera_button,ig_android_photos_qpl,ig_android_reel_impresssion_cache_key_qe_universe,ig_android_show_profile_picture_upsell_in_reel_universe,ig_android_live_viewer_tap_to_hide_chrome_universe,ig_discovery_holdout_universe,ig_android_direct_import_google_photos2,ig_android_stories_tray_in_viewer,ig_android_request_verification_badge,ig_android_direct_unlimited_raven_replays_inthreadsession_fix,ig_android_netgo_cta,ig_android_viewpoint_netego_universe,ig_android_stories_separate_overlay_creation,ig_android_iris_improvements,ig_android_biz_conversion_naming_test,ig_android_fci_empty_feed_friend_search,ig_android_hashtag_page_support_places_tab,ig_camera_android_ar_platform_universe,ig_android_stories_viewer_prefetch_improvements,ig_android_optic_camera_warmup,ig_android_place_search_profile_image,ig_android_interactions_in_feed_comment_view_universe,ig_android_fb_sharing_shortcut,ig_android_oreo_hardware_bitmap,ig_android_analytics_diagnostics_universe,ig_android_insights_creative_tutorials_universe,ig_android_vc_universe,ig_android_profile_unified_follow_view,ig_android_collect_os_usage_events_universe,ig_android_shopping_nux_timing_universe,ig_android_fbpage_on_profile_side_tray,ig_android_native_logcat_interceptor,ig_android_direct_thread_content_picker,ig_android_notif_improvement_universe,ig_face_effect_ranking,ig_android_shopping_more_from_business,ig_feed_content_universe,ig_android_hacked_account_reporting,ig_android_disk_usage_logging_universe,ig_android_ad_redesign_iab_universe,ig_android_banyan_migration,ig_android_profile_event_leak_holdout,ig_android_stories_loading_automatic_retry,ig_android_gqls_typing_indicator,ag_family_bridges_2018_h2_holdout,ig_promote_net_promoter_score_universe,ig_android_direct_last_seen_message_indicator,ig_android_biz_conversion_suggest_biz_nux,ig_android_log_mediacodec_info,ig_android_vc_participant_state_callee_universe,ig_camera_android_boomerang_attribution_universe,ig_android_stories_weblink_creation,ig_android_horizontal_swipe_lfd_logging,ig_profile_company_holdout_h2_2018,ig_android_ads_manager_pause_resume_ads_universe,ig_promote_fix_expired_fb_accesstoken_android_universe,ig_android_stories_media_seen_batching_universe,ig_android_interactions_nav_to_permalink_followup_universe,ig_android_live_titles_viewer_side_view_title_universe,ig_android_direct_mark_as_read_notif_action,ig_android_edit_highlight_redesign,ig_android_direct_mutation_manager_backoff_universe,ig_android_interactions_comment_like_for_all_feed_universe,ig_android_mi_skip_analytic_event_pool_universe,ig_android_fbc_upsell_on_dp_first_load,ig_android_audio_ingestion_params,ig_android_video_call_participant_state_caller_universe,ig_fbns_shared,ig_feed_engagement_holdout_2018_h1,ig_camera_android_bg_processor,ig_android_optic_new_features_implementation,ig_android_stories_reel_interactive_tap_target_size,ig_android_video_live_trace_universe,ig_android_igtv_browse_with_pip_v2,ig_android_interactive_listview_during_refresh,ig_android_igtv_feed_banner_universe,ig_android_unfollow_from_main_feed_v2,ig_android_self_story_setting_option_in_menu,ig_android_ad_watchlead_universe,ufi_share,ig_android_live_special_codec_size_list,ig_android_live_qa_broadcaster_v1_universe,ig_android_hide_stories_viewer_list_universe,ig_android_direct_albums,ig_android_business_transaction_in_stories_consumer,ig_android_scroll_stories_tray_to_front_when_stories_ready,ig_android_direct_thread_composer,instagram_android_stories_sticker_tray_redesign,ig_camera_android_superzoom_icon_position_universe,ig_android_business_cross_post_with_biz_id_infra,ig_android_photo_invites,ig_android_reel_tray_item_impression_logging_viewpoint,ig_account_identity_2018_h2_lockdown_phone_global_holdout,ig_android_high_res_gif_stickers,ig_close_friends_v4,ig_fb_cross_posting_sender_side_holdout,ig_android_ads_history_universe,ig_android_comments_composer_newline_universe,ig_rtc_use_dtls_srtp,ig_promote_media_picker_universe,ig_android_live_start_live_button_universe,ig_android_vc_ongoing_call_notification_universe,ig_android_rate_limit_feed_item_viewable_helper,ig_android_bitmap_attribution_check,ig_android_ig_to_fb_sync_universe,ig_android_reel_viewer_data_buffer_size,ig_two_fac_totp_enable,ig_android_vc_missed_call_notification_action_call_back,ig_android_stories_landscape_mode,ig_android_ad_view_ads_native_universe,ig_android_igtv_whitelisted_for_web,ig_android_global_prefetch_scheduler,ig_android_live_thread_delay_for_mute_universe,ig_close_friends_v4_global,ig_android_share_publish_page_universe,ig_android_new_camera_design_universe,ig_direct_max_participants,ig_promote_hide_local_awareness_universe,ig_android_graphql_survey_new_proxy_universe,ig_android_fs_creation_flow_tweaks,ig_android_ad_watchbrowse_cta_universe,ig_android_camera_new_tray_behavior_universe,ig_android_direct_expiring_media_loading_errors,ig_android_show_fbunlink_button_based_on_server_data,ig_android_downloadable_vp8_module,ig_android_igtv_feed_trailer,ig_android_fb_profile_integration_universe,ig_android_profile_private_banner,ig_camera_android_focus_attribution_universe,ig_android_rage_shake_whitelist,ig_android_su_follow_back,ig_android_prefetch_notification_data,ig_android_webrtc_icerestart_on_failure_universe,ig_android_vpvd_impressions_universe,ig_android_payload_based_scheduling,ig_android_grid_cell_count,ig_android_new_highlight_button_text,ig_android_direct_search_bar_redesign,ig_android_hashtag_row_preparer,ig_android_ad_pbia_header_click_universe,ig_android_direct_visual_viewer_ppr_fix,ig_background_prefetch,ig_camera_android_focus_in_post_universe,ig_android_time_spent_dashboard,ig_android_direct_vm_activity_sheet,ig_promote_political_ads_universe,ig_android_stories_auto_retry_reels_media_and_segments,ig_android_recommend_accounts_killswitch,ig_shopping_video_half_sheet,ig_android_ad_iab_qpl_kill_switch_universe,ig_android_interactions_direct_share_comment_universe,ig_android_vc_sounds_universe,ig_camera_android_cache_format_picker_children,ig_android_post_live_expanded_comments_view_universe,ig_android_always_use_server_recents,ig_android_qp_slot_cooldown_enabled_universe,ig_android_asset_picker_improvements,ig_android_direct_activator_cards,ig_android_pending_media_manager_init_fix_universe,ig_android_facebook_global_state_sync_frequency_universe,ig_android_network_trace_migration,ig_android_creation_new_post_title,ig_android_reverse_audio,ig_android_camera_gallery_upload_we_universe,ig_android_direct_inbox_async_diffing_universe,ig_android_live_save_to_camera_roll_limit_by_screen_size_universe,ig_android_profile_phone_autoconfirm_universe,ig_direct_stories_questions,ig_android_optic_surface_texture_cleanup,ig_android_vc_use_timestamp_normalizer,ig_android_post_recs_show_more_button_universe,ig_shopping_checkout_mvp_experiment,ig_android_direct_pending_media,ig_android_scroll_main_feed,ig_android_intialization_chunk_410,ig_android_story_ads_default_long_video_duration,ig_android_interactions_mention_search_presence_dot_universe,ig_android_stories_music_sticker_position,ig_android_direct_character_limit,ig_stories_music_themes,ig_android_nametag_save_experiment_universe,ig_android_media_rows_prepare_10_31,ig_android_fs_new_gallery,ig_android_stories_hide_retry_button_during_loading_launch,ig_android_remove_follow_all_fb_list,ig_android_biz_conversion_editable_profile_review_universe,ig_android_shopping_checkout_mvp,ig_android_local_info_page,ig_android_direct_log_badge_count" 98 | 99 | const val LOGIN_EXPERIMENTS: String = 100 | "ig_android_fci_onboarding_friend_search,ig_android_device_detection_info_upload,ig_android_autosubmit_password_recovery_universe,ig_growth_android_profile_pic_prefill_with_fb_pic_2,ig_account_identity_logged_out_signals_global_holdout_universe,ig_android_background_voice_phone_confirmation_prefilled_phone_number_only,ig_android_login_identifier_fuzzy_match,ig_android_one_tap_aymh_redesign_universe,ig_android_keyboard_detector_fix,ig_android_suma_landing_page,ig_android_direct_main_tab_universe,ig_android_aymh_signal_collecting_kill_switch,ig_android_login_forgot_password_universe,ig_android_smartlock_hints_universe,ig_android_smart_prefill_killswitch,ig_android_account_switch_infra_universe,ig_android_multi_tap_login_new,ig_android_email_one_tap_auto_login_during_reg,ig_android_category_search_in_sign_up,ig_android_report_nux_completed_device,ig_android_reg_login_profile_photo_universe,ig_android_caption_typeahead_fix_on_o_universe,ig_android_nux_add_email_device,ig_android_ci_opt_in_placement,ig_android_remember_password_at_login,ig_type_ahead_recover_account,ig_android_analytics_accessibility_event,ig_sem_resurrection_logging,ig_android_abandoned_reg_flow,ig_android_editable_username_in_reg,ig_android_account_recovery_auto_login,ig_android_sim_info_upload,ig_android_skip_signup_from_one_tap_if_no_fb_sso,ig_android_hide_fb_flow_in_add_account_flow,ig_android_mobile_http_flow_device_universe,ig_account_recovery_via_whatsapp_universe,ig_android_hide_fb_button_when_not_installed_universe,ig_prioritize_user_input_on_switch_to_signup,ig_android_gmail_oauth_in_reg,ig_android_login_safetynet,ig_android_gmail_autocomplete_account_over_one_tap,ig_android_background_voice_phone_confirmation,ig_android_phone_auto_login_during_reg,ig_android_hide_typeahead_for_logged_users,ig_android_hindi,ig_android_reg_modularization_universe,ig_android_bottom_sheet,ig_android_snack_bar_hiding,ig_android_one_tap_fallback_auto_login,ig_android_device_verification_separate_endpoint,ig_account_recovery_with_code_android_universe,ig_android_onboarding_skip_fb_connect,ig_android_phone_reg_redesign_universe,ig_android_universe_noticiation_channels,ig_android_media_cache_cleared_universe,ig_android_account_linking_universe,ig_android_hsite_prefill_new_carrier,ig_android_retry_create_account_universe,ig_android_family_apps_user_values_provider_universe,ig_android_reg_nux_headers_cleanup_universe,ig_android_dialog_email_reg_error_universe,ig_android_ci_fb_reg,ig_android_device_info_foreground_reporting,ig_fb_invite_entry_points,ig_android_device_verification_fb_signup,ig_android_suma_biz_account,ig_android_onetaplogin_optimization,ig_video_debug_overlay,ig_android_ask_for_permissions_on_reg,ig_android_display_full_country_name_in_reg_universe,ig_android_exoplayer_settings,ig_android_persistent_duplicate_notif_checker,ig_android_security_intent_switchoff,ig_android_background_voice_confirmation_block_argentinian_numbers,ig_android_do_not_show_back_button_in_nux_user_list,ig_android_passwordless_auth,ig_android_direct_main_tab_account_switch,ig_android_modularized_dynamic_nux_universe,ig_android_icon_perf2,ig_android_email_suggestions_universe,ig_android_fb_account_linking_sampling_freq_universe,ig_android_prefill_full_name_from_fb,ig_android_access_flow_prefill" 101 | 102 | const val LAUNCHER_CONFIGS: String = 103 | "ig_android_felix_release_players,ig_user_mismatch_soft_error,ig_android_os_version_blocking_config,ig_android_carrier_signals_killswitch,fizz_ig_android,ig_mi_block_expired_events,ig_android_killswitch_perm_direct_ssim,ig_fbns_blocked" 104 | 105 | const val SURFACES_TO_TRIGGERS = 106 | """{"5734":["instagram_feed_prompt"],"4715":["instagram_feed_header"],"5858":["instagram_feed_tool_tip"]}""" 107 | 108 | const val SURFACES_TO_QUERIES: String = 109 | """{"5734":"viewer() {eligible_promotions.trigger_context_v2().ig_parameters().trigger_name().surface_nux_id().external_gating_permitted_qps().supports_client_filters(true).include_holdouts(true) {edges {client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range{start,end},node {id,promotion_id,logging_data,max_impressions,triggers,contextual_filters {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template {name,parameters {name,required,bool_value,string_value,color_value,}},creatives {title {text},content {text},footer {text},social_context {text},social_context_images,primary_action{title {text},url,limit,dismiss_promotion},secondary_action{title {text},url,limit,dismiss_promotion},dismiss_action{title {text},url,limit,dismiss_promotion},image.scale() {uri,width,height}}}}}}","4715":"viewer(){eligible_promotions.trigger_context_v2().ig_parameters().trigger_name().surface_nux_id().external_gating_permitted_qps().supports_client_filters(true).include_holdouts(true) {edges {client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range {start,end},node {id,promotion_id,logging_data,max_impressions,triggers,contextual_filters {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas{name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template {name,parameters {name,required,bool_value,string_value,color_value,}},creatives {title {text},content {text},footer{text},social_context {text},social_context_images,primary_action{title {text},url,limit,dismiss_promotion},secondary_action{title {text},url,limit,dismiss_promotion},dismiss_action{title {text},url,limit,dismiss_promotion},image.scale(){uri,width,height}}}}}}","5858":"viewer() {eligible_promotions.trigger_context_v2().ig_parameters().trigger_name().surface_nux_id().external_gating_permitted_qps().supports_client_filters(true).include_holdouts(true) {edges {client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range {start,end},node {id,promotion_id,logging_data,max_impressions,triggers,contextual_filters {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters{filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}},clauses {clause_type,filters {filter_type,unknown_action,value {name,required,bool_value,int_value,string_value},extra_datas {name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template {name,parameters {name,required,bool_value,string_value,color_value,}},creatives {title {text},content {text},footer {text},social_context {text},social_context_images,primary_action{title {text},url,limit,dismiss_promotion},secondary_action{title {text},url,limit,dismiss_promotion},dismiss_action{title {text},url,limit,dismiss_promotion},image.scale() {uri,width,height}}}}}}"}""" 110 | 111 | val SUPPORTED_CAPABILITIES: List> = listOf( 112 | mapOf( 113 | "name" to "SUPPORTED_SDK_VERSIONS", 114 | "value" to "13.0,14.0,15.0,16.0,17.0,18.0,19.0,20.0,21.0,22.0,23.0,24.0,25.0,26.0,27.0,28.0,29.0,30.0,31.0,32.0,33.0,34.0,35.0,36.0,37.0,38.0,39.0,40.0,41.0,42.0,43.0,44.0,45.0,46.0,47.0,48.0,49.0,50.0,51.0,52.0,53.0" 115 | ), 116 | mapOf( 117 | "name" to "FACE_TRACKER_VERSION", 118 | "value" to "12" 119 | ), 120 | mapOf( 121 | "name" to "segmentation", 122 | "value" to "segmentation_enabled" 123 | ), 124 | mapOf( 125 | "name" to "WORLD_TRACKER", 126 | "value" to "WORLD_TRACKER_ENABLED" 127 | ) 128 | ) 129 | } 130 | 131 | 132 | // API Endpoints 133 | object Routes { 134 | fun msisdnHeader() = "accounts/read_msisdn_header/" 135 | 136 | fun logAttribution() = "attribution/log_attribution/" 137 | 138 | fun contactPointPrefill() = "accounts/contact_point_prefill/" 139 | 140 | fun login(): String = "accounts/login/" 141 | 142 | fun logout(): String = "accounts/logout/" 143 | 144 | fun reelsTrayFeed(): String = "feed/reels_tray/" 145 | 146 | fun suggestedSearches(): String = "fbsearch/suggested_searches/" 147 | 148 | fun rankedRecipients(): String = "direct_v2/ranked_recipients/" 149 | 150 | fun loomFetchConfig(): String = "loom/fetch_config/" 151 | 152 | fun profileNotice(): String = "users/profile_notice/" 153 | 154 | fun batchFetch(): String = "qp/batch_fetch/" 155 | 156 | fun twoFactorAuth(): String = "accounts/two_factor_login/" 157 | 158 | fun userTags(userId: String, rankToken: String): String = 159 | "usertags/${userId}/feed/?rank_token=${rankToken}&ranked_content=true" 160 | 161 | fun geoMedia(userId: String): String = "maps/user/${userId}/" 162 | 163 | fun follow(userId: String): String = "friendships/create/${userId}/" 164 | 165 | fun unfollow(userId: String): String = "friendships/destroy/${userId}/" 166 | 167 | fun removeFollower(userId: String): String = "friendships/remove_follower/${userId}" 168 | 169 | fun expose(): String = "qe/expose/" 170 | 171 | fun explore(): String = "discover/explore/" 172 | 173 | fun saveMedia(mediaId: String): String = "media/${mediaId}/save/" 174 | 175 | fun unsaveMedia(mediaId: String): String = "media/${mediaId}/unsave/" 176 | 177 | fun getSavedMedia(): String = "feed/saved/" 178 | 179 | fun igtvSuggestions(): String = "igtv/tv_guide/" 180 | 181 | fun setAccountPrivate(): String = "accounts/set_private/" 182 | 183 | fun setAccountPublic(): String = "accounts/set_public/" 184 | 185 | fun editAccount(): String = "accounts/edit_profile/" 186 | 187 | fun profileData(): String = "accounts/current_user/?edit=true" 188 | 189 | fun setNameAndPhone(): String = "accounts/set_phone_and_name/" 190 | 191 | fun comment(mediaId: String): String = "media/${mediaId}/comment/" 192 | 193 | fun deleteComment(mediaId: String, commentId: String) = "media/${mediaId}/comment/${commentId}/delete/" 194 | 195 | fun commentLikers(commentId: String): String = "media/${commentId}/comment_likers/?" 196 | 197 | fun mediaLikers(mediaId: String): String = "media/${mediaId}/likers/?" 198 | 199 | fun likeComment(commentId: String): String = "media/${commentId}/comment_like/" 200 | 201 | fun unlikeComment(commentId: String): String = "media/${commentId}/comment_unlike/" 202 | 203 | fun like(mediaId: String): String = "media/${mediaId}/like/" 204 | 205 | fun unlike(mediaId: String): String = "media/${mediaId}/unlike/" 206 | 207 | fun userFriendship(userId: String): String = "friendships/show/${userId}/" 208 | 209 | fun userInfoById(userId: String): String = "users/${userId}/info/" 210 | 211 | fun userInfoByName(username: String): String = "users/${username}/usernameinfo/" 212 | 213 | fun userFeed(userId: String, maxId: String, minTimeStamp: String, rankToken: String): String = 214 | "feed/user/${userId}/?max_id=${maxId}&min_timestamp=${minTimeStamp}&rank_token=${rankToken}&ranked_content=true" 215 | 216 | fun userStories(userId: String): String = "feed/user/${userId}/story/" 217 | 218 | fun timeline(maxId: String = ""): String = "feed/timeline/?max_id=${maxId}" 219 | 220 | fun hashTagFeed(hashTag: String, maxId: String = "", rankToken: String): String = 221 | "feed/tag/${hashTag}/?max_id=${maxId}&rank_token=${rankToken}&ranked_content=true" 222 | 223 | fun likedFeed(maxId: String): String = "feed/liked/?max_id=${maxId}" 224 | 225 | fun locationFeed(locationId: String, maxId: String, rankToken: String): String = 226 | "feed/location/${locationId}/?max_id=${maxId}&rank_token=${rankToken}&ranked_content=true" 227 | 228 | fun popularFeed(rankToken: String): String = 229 | "feed/popular/?people_teaser_supported=1&rank_token=${rankToken}&ranked_content=true" 230 | 231 | fun userFollowings(userId: String, maxId: String = "", rankToken: String): String = 232 | "friendships/${userId}/following/?max_id=${maxId}&ig_sig_key_version=${KEY.SIG_KEY_VERSION}&rank_token=${rankToken}" 233 | 234 | fun userFollowers(userId: String, maxId: String = "", rankToken: String): String = 235 | "friendships/${userId}/followers/?max_id=${maxId}&ig_sig_key_version=${KEY.SIG_KEY_VERSION}&rank_token=${rankToken}" 236 | 237 | fun changePassword(): String = "accounts/change_password/" 238 | 239 | fun removeProfilePicture(): String = "accounts/remove_profile_picture/" 240 | 241 | fun searchUser(userName: String, rankToken: String): String = 242 | "users/search/?ig_sig_key_version=${KEY.SIG_KEY_VERSION}&is_typehead=true&query=${userName}&rank_token=${rankToken}" 243 | 244 | fun searchHashTag(hashTagName: String, amount: Int, rankToken: String): String = 245 | "tags/search/?count=${amount}&is_typeahead=true&q=${hashTagName}&rank_token=${rankToken}" 246 | 247 | fun hashTagStories(hashTag: String): String = "tags/${hashTag}/story/" 248 | 249 | fun hashTagSelection(hashTag: String): String = "tags/${hashTag}/sections/" 250 | 251 | fun mediaInsight(mediaId: String): String = 252 | "insights/media_organic_insights/${mediaId}/?ig_sig_key_version=${KEY.SIG_KEY_VERSION}" 253 | 254 | fun selfInsight(): String = "insights/account_organic_insights/?show_promotions_in_landing_page=true&first=" 255 | 256 | fun followHashTag(hashTag: String): String = "tags/follow/${hashTag}/" 257 | 258 | fun unfollowHashTag(hashTag: String): String = "tags/unfollow/${hashTag}/" 259 | 260 | fun tagsFollowedByUser(userId: String): String = "users/${userId}/following_tags_info/" 261 | 262 | fun searchLocation(locationName: String, amount: Int, rankToken: String): String = 263 | "fbsearch/places/?count=${amount}&query=${locationName}&rank_token=${rankToken}" 264 | 265 | fun mediaInfo(mediaId: String): String = "media/${mediaId}/info/" 266 | 267 | fun editMedia(mediaId: String): String = "media/${mediaId}/edit_media/" 268 | 269 | fun deleteMedia(mediaId: String): String = "media/${mediaId}/delete/" 270 | 271 | fun removeSelfTagFromMedia(mediaId: String): String = "media/${mediaId}/remove/" 272 | 273 | fun archiveMedia(mediaId: String, action: String, mediaType: Int): String = 274 | "media/${mediaId}/${action}/?media_type=${mediaType}" 275 | 276 | fun mediaComments(mediaId: String, maxId: String = ""): String = "media/${mediaId}/comments/?max_id=${maxId}" 277 | 278 | fun qeSync(): String = "qe/sync/" 279 | 280 | fun launcherSync(): String = "launcher/sync/" 281 | 282 | fun inboxV2(): String = "direct_v2/inbox/?" 283 | 284 | fun presence(): String = "direct_v2/get_presence/" 285 | 286 | fun userReel(userId: String): String = "feed/user/${userId}/reel_media/" 287 | 288 | fun selfStoryViewers(storyId: String): String = 289 | "media/${storyId}/list_reel_media_viewer/?supported_capabilities_new=${EXPERIMENTS.SUPPORTED_CAPABILITIES}" 290 | 291 | fun watchReels(): String = "media/seen/" 292 | 293 | fun multipleUsersReel(): String = "feed/reels_media/" 294 | 295 | fun pendingInbox(): String = "direct_v2/pending_inbox/?persistentBadging=true&use_unified_inbox=true" 296 | 297 | fun directItem(itemType: String): String = "direct_v2/threads/broadcast/${itemType}/" 298 | 299 | fun directPhoto(): String = "direct_v2/threads/broadcast/upload_photo/" 300 | 301 | fun approvePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/approve/" 302 | 303 | fun hidePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/hide/" 304 | 305 | fun declinePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/decline/" 306 | 307 | fun autoCompleteUserList(): String = "friendships/autocomplete_user_list/?version=2&followinfo=True" 308 | 309 | fun megaphoneLog(): String = "megaphone/log/" 310 | 311 | fun block(userId: String): String = "friendships/block/${userId}/" 312 | 313 | fun unblock(userId: String): String = "friendships/unblock/${userId}/" 314 | 315 | fun recentActivity(): String = "news/inbox/" 316 | 317 | fun muteUser(): String = "friendships/mute_posts_or_story_from_follow/" 318 | 319 | fun getMutedUser(): String = "friendships/muted_reels" 320 | 321 | fun unmuteUser(): String = "friendships/unmute_posts_or_story_from_follow/" 322 | 323 | fun pendingFriendRequests(): String = "friendships/pending/" 324 | 325 | fun approvePendingFollowRequest(userId: String): String = "friendships/approve/${userId}/" 326 | 327 | fun rejectPendingFollowRequest(userId: String): String = "friendships/ignore/${userId}/" 328 | 329 | fun directShare(): String = "direct_share/inbox/" 330 | } -------------------------------------------------------------------------------- /src/main/kotlin/api/InstagramAPI.kt: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import util.LoginException 4 | import com.nfeld.jsonpathlite.JsonPath 5 | import com.nfeld.jsonpathlite.JsonResult 6 | import com.nfeld.jsonpathlite.extension.read 7 | import khttp.get 8 | import khttp.post 9 | import khttp.responses.Response 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.withContext 15 | import org.json.JSONArray 16 | import org.json.JSONObject 17 | import util.* 18 | import java.io.File 19 | import java.time.LocalDateTime 20 | import java.time.ZoneId 21 | import java.util.* 22 | import kotlin.math.max 23 | import kotlin.math.min 24 | import kotlin.random.Random 25 | import kotlin.system.exitProcess 26 | 27 | object InstagramAPI { 28 | var username: String = "username" 29 | var password: String = "password" 30 | var deviceId: String = "xxxx" 31 | var uuid: String = "xxxx" 32 | var userId: String = "" 33 | var token: String = "-" 34 | var rankToken: String = "-" 35 | var isLoggedIn: Boolean = false 36 | var lastJSON: JsonResult? = null 37 | lateinit var lastResponse: Response 38 | var statusCode: Int = 0 39 | var totalRequests: Int = 0 40 | private var request: Request = Request() 41 | private var cookiePersistor: CookiePersistor = CookiePersistor("") 42 | 43 | 44 | // Prepare Instagram API 45 | fun prepare() { 46 | deviceId = Crypto.generateDeviceId(username) 47 | uuid = Crypto.generateUUID(true) 48 | cookiePersistor = CookiePersistor(username) 49 | if (cookiePersistor.exist()) { 50 | val cookieDisk = cookiePersistor.load() 51 | val account = JSONObject(cookieDisk.account) 52 | if (account.getString("status").toLowerCase() == "ok") { 53 | println("Already logged in to Instagram") 54 | val jar = cookieDisk.cookieJar 55 | request.persistedCookies = jar 56 | isLoggedIn = true 57 | userId = jar.getCookie("ds_user_id")?.value.toString() 58 | token = jar.getCookie("csrftoken")?.value.toString() 59 | rankToken = "${userId}_$uuid" 60 | } 61 | } else { 62 | println("Cookie file does not exist, need to login first") 63 | } 64 | } 65 | 66 | private fun preLoginFlow() { 67 | println("Initiating pre login flow") 68 | readMSISDNHeader() 69 | syncLauncher(isLogin = true) 70 | syncDeviceFeatures() 71 | logAttribution() 72 | setContactPointPrefill() 73 | } 74 | 75 | private fun readMSISDNHeader(usage: String = "default"): Boolean { 76 | val payload = JSONObject() 77 | .put("device_id", this.uuid) 78 | .put("mobile_subno_usage", usage) 79 | 80 | val header = mapOf("X-DEVICE-ID" to uuid) 81 | 82 | return request.prepare(endpoint = Routes.msisdnHeader(), payload = payload.toString(), header = header) 83 | .send(true) 84 | } 85 | 86 | private fun syncLauncher(isLogin: Boolean = false): Boolean { 87 | val payload = JSONObject() 88 | .put("id", uuid) 89 | .put("server_config_retrieval", "1") 90 | .put("experiments", EXPERIMENTS.LAUNCHER_CONFIGS) 91 | 92 | if (!isLogin) { 93 | payload 94 | .put("_csrftoken", token) 95 | .put("_uid", userId) 96 | .put("_uuid", uuid) 97 | } 98 | 99 | return request.prepare(endpoint = Routes.launcherSync(), payload = payload.toString()).send(true) 100 | } 101 | 102 | private fun syncDeviceFeatures(): Boolean { 103 | val payload = JSONObject() 104 | .put("id", uuid) 105 | .put("server_config_retrieval", "1") 106 | .put("experiments", EXPERIMENTS.LOGIN_EXPERIMENTS) 107 | 108 | val header = mapOf("X-DEVICE-ID" to uuid) 109 | 110 | return request.prepare(endpoint = Routes.qeSync(), payload = payload.toString(), header = header).send(true) 111 | } 112 | 113 | private fun logAttribution(usage: String = "default"): Boolean { 114 | val payload = JSONObject() 115 | .put("adid", Crypto.generateUUID(true)) 116 | 117 | return request.prepare(endpoint = Routes.logAttribution(), payload = payload.toString()).send(true) 118 | } 119 | 120 | private fun setContactPointPrefill(usage: String = "prefill"): Boolean { 121 | val payload = JSONObject() 122 | .put("id", this.uuid) 123 | .put("phone_id", Crypto.generateUUID(true)) 124 | .put("_csrftoken", this.token) 125 | .put("usage", usage) 126 | 127 | return request.prepare(endpoint = Routes.contactPointPrefill(), payload = payload.toString()).send(true) 128 | } 129 | 130 | // Login to Instagram 131 | fun login(username: String, password: String, forceLogin: Boolean = false): Boolean { 132 | if (!isLoggedIn || forceLogin) { 133 | preLoginFlow() 134 | 135 | val payload = JSONObject() 136 | .put("_csrftoken", "missing") 137 | .put("device_id", deviceId) 138 | .put("_uuid", uuid) 139 | .put("username", username) 140 | .put("password", password) 141 | .put("login_attempt_count", "0") 142 | 143 | if (request.prepare(endpoint = Routes.login(), payload = payload.toString()).send(true)) { 144 | saveSuccessfulLogin() 145 | return true 146 | } else { 147 | println("Username or password is incorrect.") 148 | exitProcess(1) 149 | } 150 | } 151 | return false 152 | } 153 | 154 | private fun saveSuccessfulLogin() { 155 | cookiePersistor.save(lastResponse.text, lastResponse.cookies) 156 | val account = lastResponse.jsonObject 157 | if (account.getString("status").toLowerCase() == "ok") { 158 | val jar = lastResponse.cookies 159 | isLoggedIn = true 160 | userId = jar.getCookie("ds_user_id")?.value.toString() 161 | token = jar.getCookie("csrftoken")?.value.toString() 162 | rankToken = "${userId}_$uuid" 163 | 164 | println("Logged in successfully") 165 | postLoginFlow() 166 | } 167 | } 168 | 169 | // Sync features after successful login 170 | private fun postLoginFlow() { 171 | println("Initiating post login flow") 172 | syncLauncher(isLogin = false) 173 | syncUserFeatures() 174 | // Update feed and timeline 175 | getTimeline() 176 | getReelsTrayFeed(reason = "cold_start") 177 | getSuggestedSearches("users") 178 | getSuggestedSearches("blended") 179 | // DM update 180 | getRankedRecipients("reshare", true) 181 | getRankedRecipients("save", true) 182 | getInboxV2() 183 | getPresence() 184 | getRecentActivity() 185 | // Config and other stuffs 186 | getLoomFetchConfig() 187 | getProfileNotice() 188 | getBatchFetch() 189 | getExplore(true) 190 | getAutoCompleteUserList() 191 | } 192 | 193 | // Perform interactive two step verification process 194 | fun performTwoFactorAuth(): Boolean { 195 | println("Two-factor authentication required") 196 | println("Enter 2FA verification code: ") 197 | val twoFactorAuthCode = readLine() 198 | val twoFactorAuthID = lastJSON?.read("$.two_factor_info")?.read("$.two_factor_identifier") 199 | val payload = JSONObject() 200 | .put("username", username) 201 | .put("verification_code", twoFactorAuthCode) 202 | .put("two_factor_identifier", twoFactorAuthID) 203 | .put("password", password) 204 | .put("device_id", deviceId) 205 | .put("ig_sig_key_version", KEY.SIG_KEY_VERSION) 206 | 207 | 208 | if (request.prepare(endpoint = Routes.twoFactorAuth(), payload = payload.toString()).send(true)) { 209 | if (lastJSON?.read("$.status") == "ok") { 210 | return true 211 | } 212 | } else { 213 | println(lastJSON?.read("$.message")) 214 | } 215 | 216 | return false 217 | } 218 | 219 | // Perform interactive challenge solving 220 | fun solveChallenge(): Boolean { 221 | println("Checkpoint challenge required") 222 | val challengeUrl = lastJSON?.read("$.challenge")?.read("$.api_path")?.removeRange(0, 1) 223 | request.prepare(endpoint = challengeUrl, API_URL = "https://i.instagram.com/").send(true) 224 | 225 | val choices = getChallengeChoices() 226 | choices.forEach { println(it) } 227 | print("Enter your choice: ") 228 | val selectedChoice = readLine()?.toInt() 229 | 230 | val payload = JSONObject() 231 | .put("choice", selectedChoice) 232 | 233 | if (request.prepare(endpoint = challengeUrl, payload = payload.toString()).send(true)) { 234 | println("A code has been sent to the method selected, please check.") 235 | println("Enter your code: ") 236 | val code = readLine()?.toInt() 237 | val secondPayload = JSONObject() 238 | .put("security_code", code) 239 | 240 | request.prepare(endpoint = challengeUrl, payload = secondPayload.toString()).send(true) 241 | if (lastJSON?.read("$.action") == "close" && lastJSON?.read("$.status") == "ok") { 242 | return true 243 | } 244 | } 245 | 246 | println("Failed to log in. Try again") 247 | return false 248 | } 249 | 250 | // Get challenge choices 251 | private fun getChallengeChoices(): List { 252 | val choices: MutableList = mutableListOf() 253 | if (lastJSON?.read("$.step_name") == "select_verify_method") { 254 | choices.add("Checkpoint challenge received") 255 | 256 | val stepData = lastJSON?.read("$.step_data") 257 | if (stepData?.has("phone_number") == true) { 258 | choices.add("0 - Phone ${stepData.get("$.phone_number")}") 259 | } 260 | 261 | if (stepData?.has("email") == true) { 262 | choices.add("0 - Phone ${stepData.get("$.email")}") 263 | } 264 | } 265 | 266 | if (lastJSON?.read("$.step_name") == "delta_login_review") { 267 | choices.add("Login attempt challenge received") 268 | choices.add("0 - It was me") 269 | choices.add("1 - It wasn't me") 270 | } 271 | 272 | if (choices.isEmpty()) { 273 | println("No challenge found, might need to change password") 274 | println("Proceed with changing password? (y/n)") 275 | val choice = readLine() 276 | if (choice == "y") { 277 | println("Enter your new password:") 278 | val newPassword = readLine() 279 | if (changePassword(newPassword!!)) { 280 | println("Password changed successfully. Please re-try now") 281 | } else { 282 | println("Failed to change password.") 283 | } 284 | } else if (choice == "n") { 285 | println("You must need to change password to avoid being detected by Instagram") 286 | } else { 287 | println("Invalid input") 288 | } 289 | choices.add("0 - Nothing found") 290 | println("Please quit and retry again") 291 | } 292 | 293 | return choices 294 | } 295 | 296 | //Logout from instagram 297 | fun logout(): Boolean { 298 | if (request.prepare(endpoint = Routes.logout(), payload = "{}").send()) { 299 | cookiePersistor.destroy() 300 | println("Logged out from instagram") 301 | return true 302 | } 303 | return false 304 | } 305 | 306 | private fun syncUserFeatures(): Boolean { 307 | val payload = JSONObject() 308 | .put("_csrftoken", token) 309 | .put("device_id", deviceId) 310 | .put("_uuid", uuid) 311 | .put("id", this.uuid) 312 | .put("experiments", EXPERIMENTS.EXPERIMENTS) 313 | 314 | val header = mapOf("X-DEVICE-ID" to uuid) 315 | 316 | return request.prepare(endpoint = Routes.qeSync(), payload = payload.toString(), header = header).send() 317 | } 318 | 319 | // Get zoneOffSet of current System timezone 320 | private fun getZoneOffSet(): String = 321 | ZoneId.of(Calendar.getInstance().timeZone.toZoneId().toString()).rules.getOffset(LocalDateTime.now()).toString() 322 | .replace( 323 | ":", 324 | "" 325 | ) 326 | 327 | // Get timeline feed 328 | fun getTimeline(options: List = listOf(), maxId: String = ""): Boolean { 329 | val payload = JSONObject() 330 | .put("_csrftoken", token) 331 | .put("_uuid", uuid) 332 | .put("is_prefetch", 0) 333 | .put("phone_id", Crypto.generateUUID(true)) 334 | .put("device_id", deviceId) 335 | .put("client_session_id", Crypto.generateUUID(true)) 336 | .put("battery_level", Random.Default.nextInt(25, 100)) 337 | .put("is_charging", Random.Default.nextInt(0, 1)) 338 | .put("will_sound_on", Random.Default.nextInt(0, 1)) 339 | .put("is_on_screen", true) 340 | .put("timezone_offset", getZoneOffSet()) 341 | .put("reason", "cold_start_fetch") 342 | .put("is_pull_to_refresh", "0") 343 | 344 | if ("is_pull_to_refresh" in options) { 345 | payload 346 | .put("reason", "pull_to_refresh") 347 | .put("is_pull_to_refresh", "1") 348 | } 349 | 350 | val header = mapOf("X-Ads-Opt-Out" to "0") 351 | 352 | return request.prepare(endpoint = Routes.timeline(maxId = maxId), payload = payload.toString(), header = header) 353 | .send() 354 | } 355 | 356 | // Get Reels(Stories) 357 | fun getReelsTrayFeed(reason: String = "pull_to_refresh"): Boolean { 358 | // reason can be = cold_start or pull_to_refresh 359 | val payload = JSONObject() 360 | .put("supported_capabilities_new", EXPERIMENTS.SUPPORTED_CAPABILITIES) 361 | .put("reason", reason) 362 | .put("_csrftoken", token) 363 | .put("_uuid", uuid) 364 | 365 | return request.prepare(endpoint = Routes.reelsTrayFeed(), payload = payload.toString()).send() 366 | } 367 | 368 | // Get suggested searches 369 | private fun getSuggestedSearches(type: String = "users"): Boolean { 370 | val payload = JSONObject() 371 | .put("type", type) 372 | 373 | return request.prepare(endpoint = Routes.suggestedSearches(), payload = payload.toString()).send() 374 | } 375 | 376 | // Get ranked recipients 377 | private fun getRankedRecipients(mode: String, showThreads: Boolean, query: String = ""): Boolean { 378 | val payload = JSONObject() 379 | .put("mode", mode) 380 | .put("show_threads", showThreads) 381 | .put("use_unified_inbox", "true") 382 | 383 | if (query.isNotEmpty()) { 384 | payload 385 | .put("query", query) 386 | } 387 | 388 | return request.prepare(endpoint = Routes.rankedRecipients(), payload = payload.toString()).send() 389 | } 390 | 391 | // Get Direct messages 392 | fun getInboxV2(): Boolean { 393 | val payload = JSONObject() 394 | .put("persistentBadging", true) 395 | .put("use_unified_inbox", true) 396 | 397 | return request.prepare(Routes.inboxV2(), payload = payload.toString()).send() 398 | } 399 | 400 | // Get presence 401 | private fun getPresence(): Boolean { 402 | return request.prepare(Routes.presence()).send() 403 | } 404 | 405 | // Get recent activity of user 406 | private fun getRecentActivity(): Boolean { 407 | return request.prepare(endpoint = Routes.recentActivity()).send() 408 | } 409 | 410 | fun getFollowingRecentActivity(): Boolean { 411 | return request.prepare(endpoint = "news").send() 412 | } 413 | 414 | fun getLoomFetchConfig(): Boolean { 415 | return request.prepare(endpoint = Routes.loomFetchConfig()).send() 416 | } 417 | 418 | fun getProfileNotice(): Boolean { 419 | return request.prepare(endpoint = Routes.profileNotice()).send() 420 | } 421 | 422 | fun getBatchFetch(): Boolean { 423 | val payload = JSONObject() 424 | .put("_csrftoken", token) 425 | .put("_uid", userId) 426 | .put("_uuid", uuid) 427 | .put("scale", 3) 428 | .put("version", 1) 429 | .put("vc_policy", "default") 430 | .put("surfaces_to_triggers", EXPERIMENTS.SURFACES_TO_TRIGGERS) 431 | .put("surfaces_to_queries", EXPERIMENTS.SURFACES_TO_QUERIES) 432 | 433 | return request.prepare(endpoint = Routes.batchFetch(), payload = payload.toString()).send() 434 | } 435 | 436 | 437 | // ====== MEDIA METHODS ===== // 438 | 439 | fun editMedia(mediaId: String, caption: String = ""): Boolean { 440 | val payload = JSONObject() 441 | .put("_csrftoken", this.token) 442 | .put("_uid", this.userId) 443 | .put("_uuid", this.uuid) 444 | .put("caption_text", caption) 445 | 446 | return request.prepare(endpoint = Routes.editMedia(mediaId = mediaId), payload = payload.toString()).send() 447 | } 448 | 449 | fun removeSelfTagFromMedia(mediaId: String): Boolean { 450 | val payload = JSONObject() 451 | .put("_csrftoken", this.token) 452 | .put("_uid", this.userId) 453 | .put("_uuid", this.uuid) 454 | 455 | return request.prepare( 456 | endpoint = Routes.removeSelfTagFromMedia(mediaId = mediaId), 457 | payload = payload.toString() 458 | ).send() 459 | } 460 | 461 | fun getMediaInfo(mediaId: String): Boolean { 462 | return request.prepare(endpoint = Routes.mediaInfo(mediaId = mediaId)).send() 463 | } 464 | 465 | fun archiveMedia(mediaId: String, mediaType: Int, undo: Boolean = false): Boolean { 466 | val action = if (undo) "undo_only_me" else "only_me" 467 | val payload = JSONObject() 468 | .put("_csrftoken", this.token) 469 | .put("_uid", this.userId) 470 | .put("_uuid", this.uuid) 471 | .put("media_id", mediaId) 472 | 473 | return request.prepare( 474 | endpoint = Routes.archiveMedia( 475 | mediaId = mediaId, 476 | action = action, 477 | mediaType = mediaType 478 | ), payload = payload.toString() 479 | ).send() 480 | } 481 | 482 | fun deleteMedia(mediaId: String): Boolean { 483 | val payload = JSONObject() 484 | .put("_csrftoken", this.token) 485 | .put("_uid", this.userId) 486 | .put("_uuid", this.uuid) 487 | .put("media_id", mediaId) 488 | 489 | return request.prepare(endpoint = Routes.deleteMedia(mediaId = mediaId), payload = payload.toString()).send() 490 | } 491 | 492 | private fun generateUserBreadCrumb(size: Int): String { 493 | val key = "iN4\$aGr0m" 494 | val timeElapsed = Random.nextInt(500, 1500) + (size * Random.nextInt(500, 1500)) 495 | val textChangeEventCount = max(1, (size / Random.nextInt(3, 5))) 496 | val dt: Long = System.currentTimeMillis() * 1000 497 | 498 | val payload = "$size $timeElapsed ${textChangeEventCount.toFloat()} $dt" 499 | val signedKeyAndData = Crypto.generateHMAC( 500 | key.toByteArray(Charsets.US_ASCII).toString(), 501 | payload.toByteArray(Charsets.US_ASCII).toString() 502 | ) 503 | 504 | return "${Base64.getEncoder().encodeToString(signedKeyAndData.toByteArray())}\n${Base64.getEncoder() 505 | .encodeToString( 506 | payload.toByteArray(Charsets.US_ASCII) 507 | )}" 508 | } 509 | 510 | fun comment(mediaId: String, commentText: String): Boolean { 511 | val payload = JSONObject() 512 | .put("container_module", "comments_v2") 513 | .put("user_breadcrumb", generateUserBreadCrumb(commentText.length)) 514 | .put("idempotence_token", Crypto.generateUUID(true)) 515 | .put("comment_text", commentText) 516 | .put("radio_type", "wifi-none") 517 | .put("device_id", this.deviceId) 518 | .put("_csrftoken", this.token) 519 | .put("_uid", this.userId) 520 | .put("_uuid", this.uuid) 521 | 522 | return request.prepare(endpoint = Routes.comment(mediaId = mediaId), payload = payload.toString()).send() 523 | } 524 | 525 | fun replyToComment(mediaId: String, parentCommentId: String, commentText: String): Boolean { 526 | val payload = JSONObject() 527 | .put("comment_text", commentText) 528 | .put("replied_to_comment_id", parentCommentId) 529 | 530 | return request.prepare(endpoint = Routes.comment(mediaId = mediaId), payload = payload.toString()).send() 531 | } 532 | 533 | 534 | fun deleteComment(mediaId: String, commentId: String): Boolean { 535 | val payload = JSONObject() 536 | .put("_csrftoken", this.token) 537 | .put("_uid", this.userId) 538 | .put("_uuid", this.uuid) 539 | 540 | return request.prepare( 541 | endpoint = Routes.deleteComment(mediaId = mediaId, commentId = commentId), 542 | payload = payload.toString() 543 | ).send() 544 | } 545 | 546 | 547 | fun getCommentLiker(commentId: String): Boolean { 548 | return request.prepare(endpoint = Routes.commentLikers(commentId = commentId)).send() 549 | } 550 | 551 | fun getMediaLiker(mediaId: String): Boolean { 552 | return request.prepare(endpoint = Routes.mediaLikers(mediaId = mediaId)).send() 553 | } 554 | 555 | fun likeComment(commentId: String): Boolean { 556 | val payload = JSONObject() 557 | .put("_csrftoken", this.token) 558 | .put("_uid", this.userId) 559 | .put("_uuid", this.uuid) 560 | .put("is_carousel_bumped_post", false) 561 | .put("container_module", "comments_v2") 562 | .put("feed_position", "0") 563 | 564 | return request.prepare(endpoint = Routes.likeComment(commentId = commentId), payload = payload.toString()) 565 | .send() 566 | } 567 | 568 | fun unlikeComment(commentId: String): Boolean { 569 | val payload = JSONObject() 570 | .put("_csrftoken", this.token) 571 | .put("_uid", this.userId) 572 | .put("_uuid", this.uuid) 573 | .put("is_carousel_bumped_post", false) 574 | .put("container_module", "comments_v2") 575 | .put("feed_position", "0") 576 | 577 | return request.prepare(endpoint = Routes.unlikeComment(commentId = commentId), payload = payload.toString()) 578 | .send() 579 | } 580 | 581 | fun like( 582 | mediaId: String, doubleTap: Int = 0, containerModule: String = "feed_short_url", 583 | feedPosition: Int = 0, username: String = "", userId: String = "", 584 | hashTagName: String = "", hashTagId: String = "", entityPageName: String = "", entityPageId: String = "" 585 | ): Boolean { 586 | 587 | val payload = JSONObject() 588 | .put("radio_type", "wifi-none") 589 | .put("device_id", this.deviceId) 590 | .put("media_id", mediaId) 591 | .put("container_module", containerModule) 592 | .put("feed_position", feedPosition.toString()) 593 | .put("is_carousel_bumped_post", "false") 594 | 595 | if (containerModule == "feed_timeline") { 596 | payload 597 | .put("inventory_source", "media_or_ad") 598 | } 599 | 600 | if (username.isNotEmpty()) { 601 | payload 602 | .put("username", username) 603 | .put("user_id", userId) 604 | } 605 | 606 | if (hashTagName.isNotEmpty()) { 607 | payload 608 | .put("hashtag_name", hashTagName) 609 | .put("hashtag_id", hashTagId) 610 | } 611 | 612 | if (entityPageName.isNotEmpty()) { 613 | payload 614 | .put("entity_page_name", entityPageName) 615 | .put("entity_page_id", entityPageId) 616 | } 617 | 618 | payload 619 | .put("_csrftoken", this.token) 620 | .put("_uid", this.userId) 621 | .put("_uuid", this.uuid) 622 | .put("d=", Random.nextInt(0, 1).toString()) 623 | 624 | val dt = if (doubleTap != 0) Random.nextInt(0, 1).toString() else doubleTap.toString() 625 | val extraSig = mutableMapOf("d=" to dt) 626 | 627 | return request.prepare( 628 | endpoint = Routes.like(mediaId = mediaId), 629 | payload = payload.toString(), 630 | extraSig = extraSig 631 | ).send() 632 | } 633 | 634 | fun unlike(mediaId: String): Boolean { 635 | val payload = JSONObject() 636 | .put("_csrftoken", this.token) 637 | .put("_uid", this.userId) 638 | .put("_uuid", this.uuid) 639 | .put("media_id", mediaId) 640 | .put("radio_type", "wifi-none") 641 | .put("is_carousel_bumped_post", "false") 642 | .put("container_module", "photo_view_other") 643 | .put("feed_position", "0") 644 | 645 | return request.prepare(endpoint = Routes.unlike(mediaId = mediaId), payload = payload.toString()).send() 646 | } 647 | 648 | fun getMediaComments(mediaId: String, maxId: String = ""): Boolean { 649 | return request.prepare(endpoint = Routes.mediaComments(mediaId = mediaId, maxId = maxId)).send() 650 | } 651 | 652 | fun getExplore(isPrefetch: Boolean = false): Boolean { 653 | val payload = JSONObject() 654 | .put("is_prefetch", isPrefetch) 655 | .put("is_from_promote", false) 656 | .put("timezone_offset", getZoneOffSet()) 657 | .put("session_id", Crypto.generateUUID(true)) 658 | .put("supported_capabilities_new", EXPERIMENTS.SUPPORTED_CAPABILITIES) 659 | 660 | if (isPrefetch) { 661 | payload 662 | .put("max_id", 0) 663 | .put("module", "explore_popular") 664 | } 665 | 666 | return request.prepare(endpoint = Routes.explore(), payload = payload.toString()).send() 667 | } 668 | 669 | // Get auto complete user list 670 | fun getAutoCompleteUserList(): Boolean { 671 | return request.prepare(endpoint = Routes.autoCompleteUserList()).send() 672 | } 673 | 674 | fun getMegaPhoneLog(): Boolean { 675 | return request.prepare(endpoint = Routes.megaphoneLog()).send() 676 | } 677 | 678 | fun expose(): Boolean { 679 | val payload = JSONObject() 680 | .put("id", uuid) 681 | .put("experiment", "ig_android_profile_contextual_feed") 682 | 683 | return request.prepare(endpoint = Routes.expose(), payload = payload.toString()).send() 684 | } 685 | 686 | fun getUserInfoByName(username: String): Boolean { 687 | return request.prepare(endpoint = Routes.userInfoByName(username = username)).send() 688 | } 689 | 690 | fun getUserInfoByID(userId: String): Boolean { 691 | return request.prepare(endpoint = Routes.userInfoById(userId = userId)).send() 692 | } 693 | 694 | fun getUserTagMedias(userId: String): Boolean { 695 | return request.prepare(endpoint = Routes.userTags(userId = userId, rankToken = rankToken)).send() 696 | } 697 | 698 | fun getSelfUserTags(): Boolean { 699 | return getUserTagMedias(userId) 700 | } 701 | 702 | fun getGeoMedia(userId: String): Boolean { 703 | return request.prepare(endpoint = Routes.geoMedia(userId = userId)).send() 704 | } 705 | 706 | fun getSelfGeoMedia(): Boolean { 707 | return getGeoMedia(userId) 708 | } 709 | 710 | 711 | // ====== FEED METHODS ===== // 712 | 713 | private fun getUserFeed(userId: String, maxId: String = "", minTimeStamp: String = ""): Boolean { 714 | return request.prepare( 715 | endpoint = Routes.userFeed( 716 | userId = userId, 717 | maxId = maxId, 718 | minTimeStamp = minTimeStamp, 719 | rankToken = rankToken 720 | ) 721 | ).send() 722 | } 723 | 724 | fun getHashTagFeed(hashTag: String, maxId: String = ""): Boolean { 725 | return request.prepare(endpoint = Routes.hashTagFeed(hashTag = hashTag, maxId = maxId, rankToken = rankToken)) 726 | .send() 727 | } 728 | 729 | fun getLocationFeed(locationId: String, maxId: String = ""): Boolean { 730 | return request.prepare( 731 | endpoint = Routes.locationFeed( 732 | locationId = locationId, 733 | maxId = maxId, 734 | rankToken = rankToken 735 | ) 736 | ).send() 737 | } 738 | 739 | fun getPopularFeeds(): Boolean { 740 | return request.prepare(endpoint = Routes.popularFeed(rankToken = rankToken)).send() 741 | } 742 | 743 | private fun getLikedMedia(maxId: String = ""): Boolean { 744 | return request.prepare(endpoint = Routes.likedFeed(maxId = maxId)).send() 745 | } 746 | 747 | 748 | // ====== FRIENDSHIPS METHODS ===== // 749 | private fun getUserFollowers(userId: String, maxId: String = ""): Boolean { 750 | return request.prepare(endpoint = Routes.userFollowers(userId = userId, maxId = maxId, rankToken = rankToken)) 751 | .send() 752 | } 753 | 754 | private fun getUserFollowings(userId: String, maxId: String = ""): Boolean { 755 | return request.prepare(endpoint = Routes.userFollowings(userId = userId, maxId = maxId, rankToken = rankToken)) 756 | .send() 757 | } 758 | 759 | private fun getSelfUserFollowings(): Boolean { 760 | return getUserFollowings(userId) 761 | } 762 | 763 | 764 | fun follow(userId: String): Boolean { 765 | val payload = JSONObject() 766 | .put("radio_type", "wifi-none") 767 | .put("device_id", this.deviceId) 768 | .put("user_id", userId) 769 | .put("_csrftoken", this.token) 770 | .put("_uid", this.userId) 771 | .put("_uuid", this.uuid) 772 | 773 | return request.prepare(endpoint = Routes.follow(userId = userId), payload = payload.toString()).send() 774 | } 775 | 776 | fun unfollow(userId: String): Boolean { 777 | val payload = JSONObject() 778 | .put("radio_type", "wifi-none") 779 | .put("device_id", this.deviceId) 780 | .put("user_id", userId) 781 | .put("_csrftoken", this.token) 782 | .put("_uid", this.userId) 783 | .put("_uuid", this.uuid) 784 | 785 | return request.prepare(endpoint = Routes.unfollow(userId = userId), payload = payload.toString()).send() 786 | } 787 | 788 | fun removeFollower(userId: String): Boolean { 789 | val payload = JSONObject() 790 | .put("user_id", userId) 791 | .put("_csrftoken", this.token) 792 | .put("_uid", this.userId) 793 | .put("_uuid", this.uuid) 794 | 795 | return request.prepare(endpoint = Routes.removeFollower(userId = userId), payload = payload.toString()).send() 796 | } 797 | 798 | fun block(userId: String): Boolean { 799 | val payload = JSONObject() 800 | .put("user_id", userId) 801 | .put("_csrftoken", this.token) 802 | .put("_uid", this.userId) 803 | .put("_uuid", this.uuid) 804 | 805 | return request.prepare(endpoint = Routes.block(userId = userId), payload = payload.toString()).send() 806 | } 807 | 808 | fun unblock(userId: String): Boolean { 809 | val payload = JSONObject() 810 | .put("user_id", userId) 811 | .put("_csrftoken", this.token) 812 | .put("_uid", this.userId) 813 | .put("_uuid", this.uuid) 814 | 815 | return request.prepare(endpoint = Routes.unblock(userId = userId), payload = payload.toString()).send() 816 | } 817 | 818 | fun getUserFriendship(userId: String): Boolean { 819 | val payload = JSONObject() 820 | .put("user_id", userId) 821 | .put("_csrftoken", this.token) 822 | .put("_uid", this.userId) 823 | .put("_uuid", this.uuid) 824 | 825 | return request.prepare(endpoint = Routes.userFriendship(userId = userId), payload = payload.toString()).send() 826 | } 827 | 828 | fun muteUser(userId: String, isMutePosts: Boolean = false, isMuteStory: Boolean = false): Boolean { 829 | val payload = JSONObject() 830 | .put("_csrftoken", this.token) 831 | .put("_uid", this.userId) 832 | .put("_uuid", this.uuid) 833 | 834 | if (isMutePosts) { 835 | payload 836 | .put("target_posts_author_id", userId) 837 | } 838 | 839 | if (isMuteStory) { 840 | payload 841 | .put("target_reel_author_id", userId) 842 | } 843 | 844 | return request.prepare(endpoint = Routes.muteUser(), payload = payload.toString()).send() 845 | } 846 | 847 | fun getMutedUsers(mutedContentType: String): Boolean { 848 | if (mutedContentType != "stories") { 849 | throw NotImplementedError("API does not support getting friends with provided muted content type") 850 | } 851 | 852 | return request.prepare(endpoint = Routes.getMutedUser()).send() 853 | } 854 | 855 | fun unmuteUser(userId: String, isUnmutePosts: Boolean = false, isUnmuteStory: Boolean = false): Boolean { 856 | val payload = JSONObject() 857 | .put("_csrftoken", this.token) 858 | .put("_uid", this.userId) 859 | .put("_uuid", this.uuid) 860 | 861 | if (isUnmutePosts) { 862 | payload 863 | .put("target_posts_author_id", userId) 864 | } 865 | 866 | if (isUnmuteStory) { 867 | payload 868 | .put("target_reel_author_id", userId) 869 | } 870 | 871 | return request.prepare(endpoint = Routes.unmuteUser(), payload = payload.toString()).send() 872 | } 873 | 874 | fun getPendingFriendRequests(): Boolean { 875 | return request.prepare(endpoint = Routes.pendingFriendRequests()).send() 876 | } 877 | 878 | fun approvePendingFollowRequest(userId: String): Boolean { 879 | val payload = JSONObject() 880 | .put("_csrftoken", this.token) 881 | .put("_uid", this.userId) 882 | .put("_uuid", this.uuid) 883 | .put("user_id", userId) 884 | 885 | return request.prepare( 886 | endpoint = Routes.approvePendingFollowRequest(userId = userId), 887 | payload = payload.toString() 888 | ).send() 889 | } 890 | 891 | fun rejectPendingFollowRequest(userId: String): Boolean { 892 | val payload = JSONObject() 893 | .put("_csrftoken", this.token) 894 | .put("_uid", this.userId) 895 | .put("_uuid", this.uuid) 896 | .put("user_id", userId) 897 | 898 | return request.prepare( 899 | endpoint = Routes.rejectPendingFollowRequest(userId = userId), 900 | payload = payload.toString() 901 | ).send() 902 | } 903 | 904 | fun getDirectShare(): Boolean { 905 | return request.prepare(endpoint = Routes.directShare()).send() 906 | } 907 | 908 | private fun getTotalFollowersOrFollowings( 909 | userId: String, amount: Int = Int.MAX_VALUE, isFollower: Boolean = true, isUsername: Boolean = false, 910 | isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", 911 | isOverwrite: Boolean = false 912 | ): Flow = flow { 913 | 914 | val userType = if (isFollower) "follower_count" else "following_count" 915 | val userKey = if (isUsername) "username" else "pk" 916 | var nextMaxId = "" 917 | var sleepTrack = 0 918 | var counter = 0 919 | val total: Int 920 | val isWriteToFile = fileNameToWrite.isNotEmpty() 921 | val userInfo: JsonResult? 922 | 923 | getUserInfoByID(userId).let { userInfo = lastJSON } 924 | 925 | val user = userInfo?.read("$.user") 926 | 927 | if (user != null) { 928 | 929 | if (user.read("$.is_private") == true) { 930 | return@flow 931 | } 932 | total = min(amount, user.read("$.${userType}")!!) 933 | 934 | if (total >= 20000) { 935 | println("Consider saving the result in file. This operation will take time") 936 | } 937 | } else { 938 | return@flow 939 | } 940 | 941 | if (isWriteToFile) { 942 | if (File(fileNameToWrite).exists()) { 943 | if (!isOverwrite) { 944 | println("File $fileNameToWrite already exist. Not overwriting") 945 | return@flow 946 | } else { 947 | println("Overwriting $fileNameToWrite file") 948 | } 949 | } 950 | 951 | withContext(Dispatchers.IO) { 952 | File(fileNameToWrite).createNewFile() 953 | } 954 | 955 | } 956 | 957 | val type = if (isFollower) "Followers" else "Following" 958 | println("Getting $type of $userId") 959 | val br = if (isWriteToFile) File(fileNameToWrite).bufferedWriter() else null 960 | while (true) { 961 | if (isFollower) { 962 | getUserFollowers(userId, nextMaxId) 963 | } else { 964 | getUserFollowings(userId, nextMaxId) 965 | } 966 | 967 | lastJSON?.read("$.users")?.forEach { 968 | val obj = it as JSONObject 969 | if (isFilterPrivate && obj.read("$.is_private") == true) { 970 | return@forEach 971 | } 972 | if (isFilterVerified && obj.read("$.is_verified") == true) { 973 | return@forEach 974 | } 975 | 976 | val key = obj.get(userKey).toString() 977 | emit(key) 978 | counter += 1 979 | 980 | if (isWriteToFile) { 981 | br?.appendln(key) 982 | } 983 | 984 | if (counter >= total) { 985 | withContext(Dispatchers.IO) { 986 | br?.close() 987 | } 988 | return@flow 989 | } 990 | 991 | sleepTrack += 1 992 | if (sleepTrack >= 5000) { 993 | val sleepTime = Random.nextLong(120, 180) 994 | println("Waiting %.2f minutes due to too many requests.".format((sleepTime.toFloat() / 60))) 995 | delay(sleepTime * 1000) 996 | sleepTrack = 0 997 | } 998 | } 999 | 1000 | if (lastJSON?.read("$.big_list") == false) { 1001 | withContext(Dispatchers.IO) { 1002 | br?.close() 1003 | } 1004 | return@flow 1005 | } 1006 | 1007 | nextMaxId = 1008 | if (isFollower) lastJSON?.read("$.next_max_id") 1009 | .toString() else lastJSON?.read("$.next_max_id").toString() 1010 | } 1011 | } 1012 | 1013 | fun getTotalFollowers( 1014 | userId: String, amountOfFollowers: Int = Int.MAX_VALUE, isUsername: Boolean = false, 1015 | isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", 1016 | isOverwrite: Boolean = false 1017 | ): Flow { 1018 | return getTotalFollowersOrFollowings( 1019 | userId = userId, amount = amountOfFollowers, isFollower = true, isUsername = isUsername, 1020 | isFilterPrivate = isFilterPrivate, isFilterVerified = isFilterVerified, fileNameToWrite = fileNameToWrite, 1021 | isOverwrite = isOverwrite 1022 | ) 1023 | } 1024 | 1025 | fun getTotalFollowing( 1026 | userId: String, amountOfFollowing: Int = Int.MAX_VALUE, isUsername: Boolean = false, 1027 | isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", 1028 | isOverwrite: Boolean = false 1029 | ): Flow { 1030 | return getTotalFollowersOrFollowings( 1031 | userId = userId, amount = amountOfFollowing, isFollower = false, isUsername = isUsername, 1032 | isFilterPrivate = isFilterPrivate, isFilterVerified = isFilterVerified, fileNameToWrite = fileNameToWrite, 1033 | isOverwrite = isOverwrite 1034 | ) 1035 | } 1036 | 1037 | fun getLastUserFeed(userId: String, amount: Int, minTimeStamp: String = ""): Flow = flow { 1038 | var counter = 0 1039 | var nextMaxId = "" 1040 | 1041 | while (true) { 1042 | if (getUserFeed(userId = userId, maxId = nextMaxId, minTimeStamp = minTimeStamp)) { 1043 | val items = lastJSON?.read("$.items") 1044 | 1045 | if (items != null) { 1046 | items.forEach { 1047 | emit(it as JSONObject) 1048 | counter += 1 1049 | if (counter >= amount) { 1050 | return@flow 1051 | } 1052 | } 1053 | } else { 1054 | return@flow 1055 | } 1056 | 1057 | if (lastJSON?.read("$.more_available") == false) { 1058 | return@flow 1059 | } 1060 | 1061 | nextMaxId = lastJSON?.read("$.next_max_id")!! 1062 | } else { 1063 | return@flow 1064 | } 1065 | } 1066 | } 1067 | 1068 | fun getTotalHashTagMedia(hashTag: String, amount: Int = 10): Flow = flow { 1069 | var counter = 0 1070 | var nextMaxId = "" 1071 | 1072 | while (true) { 1073 | if (getHashTagFeed(hashTag = hashTag, maxId = nextMaxId)) { 1074 | val rankedItems = lastJSON?.read("$.ranked_items") 1075 | rankedItems?.forEach { 1076 | emit(it as JSONObject) 1077 | counter += 1 1078 | if (counter >= amount) { 1079 | return@flow 1080 | } 1081 | } 1082 | val items = lastJSON?.read("$.items") 1083 | items?.forEach { 1084 | emit(it as JSONObject) 1085 | counter += 1 1086 | if (counter >= amount) { 1087 | return@flow 1088 | } 1089 | } 1090 | 1091 | if (lastJSON?.read("$.more_available") == false) { 1092 | return@flow 1093 | } 1094 | 1095 | nextMaxId = lastJSON?.read("$.next_max_id")!! 1096 | } else { 1097 | return@flow 1098 | } 1099 | } 1100 | } 1101 | 1102 | fun getTotalHashTagUsers(hashTag: String, amount: Int = 10): Flow = flow { 1103 | var counter = 0 1104 | var nextMaxId = "" 1105 | 1106 | while (true) { 1107 | if (getHashTagFeed(hashTag = hashTag, maxId = nextMaxId)) { 1108 | val rankedItems = lastJSON?.read("$.ranked_items") 1109 | rankedItems?.forEach { it -> 1110 | val item = it as JSONObject 1111 | item.read("$.user")?.let { emit(it) } 1112 | counter += 1 1113 | if (counter >= amount) { 1114 | return@flow 1115 | } 1116 | } 1117 | val items = lastJSON?.read("$.items") 1118 | items?.forEach { it -> 1119 | val item = it as JSONObject 1120 | item.read("$.user")?.let { emit(it) } 1121 | counter += 1 1122 | if (counter >= amount) { 1123 | return@flow 1124 | } 1125 | } 1126 | 1127 | if (lastJSON?.read("$.more_available") == false) { 1128 | return@flow 1129 | } 1130 | 1131 | nextMaxId = lastJSON?.read("$.next_max_id")!! 1132 | } else { 1133 | return@flow 1134 | } 1135 | } 1136 | } 1137 | 1138 | fun getTotalLikedMedia(amount: Int): Flow = flow { 1139 | var counter = 0 1140 | var nextMaxId = "" 1141 | 1142 | while (true) { 1143 | if (getLikedMedia(maxId = nextMaxId)) { 1144 | val items = lastJSON?.read("$.items") 1145 | items?.forEach { 1146 | emit(it as JSONObject) 1147 | counter += 1 1148 | if (counter >= amount) { 1149 | return@flow 1150 | } 1151 | } 1152 | 1153 | if (lastJSON?.read("$.more_available") == false) { 1154 | return@flow 1155 | } 1156 | 1157 | nextMaxId = lastJSON?.read("$.next_max_id")!! 1158 | } else { 1159 | return@flow 1160 | } 1161 | } 1162 | } 1163 | 1164 | fun changePassword(newPassword: String): Boolean { 1165 | val payload = JSONObject() 1166 | .put("old_password", this.password) 1167 | .put("new_password1", newPassword) 1168 | .put("new_password2", newPassword) 1169 | 1170 | return request.prepare(endpoint = Routes.changePassword(), payload = payload.toString()).send(true) 1171 | } 1172 | 1173 | fun removeProfilePicture(): Boolean { 1174 | val payload = JSONObject() 1175 | .put("_csrftoken", this.token) 1176 | .put("_uid", this.userId) 1177 | .put("_uuid", this.uuid) 1178 | 1179 | return request.prepare(endpoint = Routes.removeProfilePicture(), payload = payload.toString()).send() 1180 | } 1181 | 1182 | fun setAccountPrivate(): Boolean { 1183 | val payload = JSONObject() 1184 | .put("_csrftoken", this.token) 1185 | .put("_uid", this.userId) 1186 | .put("_uuid", this.uuid) 1187 | 1188 | return request.prepare(endpoint = Routes.setAccountPrivate(), payload = payload.toString()).send() 1189 | } 1190 | 1191 | fun setAccountPublic(): Boolean { 1192 | val payload = JSONObject() 1193 | .put("_csrftoken", this.token) 1194 | .put("_uid", this.userId) 1195 | .put("_uuid", this.uuid) 1196 | 1197 | return request.prepare(endpoint = Routes.setAccountPublic(), payload = payload.toString()).send() 1198 | } 1199 | 1200 | fun setNameAndPhone(name: String = "", phone: String = ""): Boolean { 1201 | val payload = JSONObject() 1202 | .put("_csrftoken", this.token) 1203 | .put("_uid", this.userId) 1204 | .put("_uuid", this.uuid) 1205 | .put("first_name", name) 1206 | .put("phone_number", phone) 1207 | 1208 | return request.prepare(endpoint = Routes.setNameAndPhone(), payload = payload.toString()).send() 1209 | } 1210 | 1211 | fun getProfileData(): Boolean { 1212 | val payload = JSONObject() 1213 | .put("_csrftoken", this.token) 1214 | .put("_uid", this.userId) 1215 | .put("_uuid", this.uuid) 1216 | 1217 | return request.prepare(endpoint = Routes.profileData(), payload = payload.toString()).send() 1218 | } 1219 | 1220 | fun editProfile( 1221 | url: String = "", 1222 | phone: String, 1223 | firstName: String, 1224 | biography: String, 1225 | email: String, 1226 | gender: Int 1227 | ): Boolean { 1228 | val payload = JSONObject() 1229 | .put("_csrftoken", this.token) 1230 | .put("_uid", this.userId) 1231 | .put("_uuid", this.uuid) 1232 | .put("external_url", url) 1233 | .put("phone_number", phone) 1234 | .put("username", this.username) 1235 | .put("full_name", firstName) 1236 | .put("biography", biography) 1237 | .put("email", email) 1238 | .put("gender", gender) 1239 | 1240 | return request.prepare(endpoint = Routes.editAccount(), payload = payload.toString()).send() 1241 | } 1242 | 1243 | fun searchUsers(userName: String): Boolean { 1244 | return request.prepare(endpoint = Routes.searchUser(userName = userName, rankToken = this.rankToken)).send() 1245 | } 1246 | 1247 | fun searchHashTags(hashTagName: String, amount: Int = 50): Boolean { 1248 | return request.prepare( 1249 | endpoint = Routes.searchHashTag( 1250 | hashTagName = hashTagName, 1251 | amount = amount, 1252 | rankToken = this.rankToken 1253 | ) 1254 | ).send() 1255 | } 1256 | 1257 | fun searchLocations(locationName: String, amount: Int = 50): Boolean { 1258 | return request.prepare( 1259 | endpoint = Routes.searchLocation( 1260 | locationName = locationName, 1261 | amount = amount, 1262 | rankToken = this.rankToken 1263 | ) 1264 | ).send() 1265 | } 1266 | 1267 | fun getUserReel(userId: String): Boolean { 1268 | return request.prepare(endpoint = Routes.userReel(userId = userId)).send() 1269 | } 1270 | 1271 | fun getUsersReel(userIds: List): Boolean { 1272 | val payload = JSONObject() 1273 | .put("_csrftoken", token) 1274 | .put("_uid", userId) 1275 | .put("_uuid", uuid) 1276 | .put("user_ids", userIds) 1277 | 1278 | return request.prepare(endpoint = Routes.multipleUsersReel(), payload = payload.toString()).send() 1279 | } 1280 | 1281 | fun watchReels(reels: List): Boolean { 1282 | val storySeen: MutableMap> = mutableMapOf() 1283 | val currentTime = System.currentTimeMillis() 1284 | val reverseSortedReels = reels.sortedByDescending { it.get("taken_at").toString() } 1285 | //it.read("$.taken_at") 1286 | 1287 | for ((index, story) in reverseSortedReels.withIndex()) { 1288 | val storySeenAt = currentTime - min( 1289 | index + 1 + Random.nextLong(0, 2), 1290 | max(0, currentTime - story.get("taken_at").toString().toLong()) 1291 | ) 1292 | storySeen["${story.get("id")}_${story.read("$.user")?.get("pk").toString()}"] = 1293 | listOf("${story.get("taken_at")}_${storySeenAt}") 1294 | // storySeen["${JEToString(story["id"])}_${JEToString(story["user"]?.get("pk"))}"] = listOf("${JEToString(story["taken_at"])}_${storySeenAt}") 1295 | } 1296 | 1297 | val payload = JSONObject() 1298 | .put("_csrftoken", this.token) 1299 | .put("_uid", this.userId) 1300 | .put("_uuid", this.uuid) 1301 | .put("reels", storySeen as Map?) 1302 | 1303 | return request.prepare( 1304 | endpoint = Routes.watchReels(), 1305 | payload = payload.toString(), 1306 | API_URL = "https://i.instagram.com/api/v2/" 1307 | ).send() 1308 | } 1309 | 1310 | fun getUserStories(userId: String): Boolean { 1311 | return request.prepare(endpoint = Routes.userStories(userId = userId)).send() 1312 | } 1313 | 1314 | fun getSelfStoryViewers(storyId: String): Boolean { 1315 | return request.prepare(endpoint = Routes.selfStoryViewers(storyId = storyId)).send() 1316 | } 1317 | 1318 | fun getIGTVSuggestions(): Boolean { 1319 | return request.prepare(endpoint = Routes.igtvSuggestions()).send() 1320 | } 1321 | 1322 | fun getHashTagStories(hashTag: String): Boolean { 1323 | return request.prepare(endpoint = Routes.hashTagStories(hashTag = hashTag)).send() 1324 | } 1325 | 1326 | fun followHashTag(hashTag: String): Boolean { 1327 | val payload = JSONObject() 1328 | .put("_csrftoken", this.token) 1329 | .put("_uid", this.userId) 1330 | .put("_uuid", this.uuid) 1331 | 1332 | return request.prepare(endpoint = Routes.followHashTag(hashTag = hashTag), payload = payload.toString()).send() 1333 | } 1334 | 1335 | fun unfollowHashTag(hashTag: String): Boolean { 1336 | val payload = JSONObject() 1337 | .put("_csrftoken", this.token) 1338 | .put("_uid", this.userId) 1339 | .put("_uuid", this.uuid) 1340 | 1341 | return request.prepare(endpoint = Routes.unfollowHashTag(hashTag = hashTag), payload = payload.toString()) 1342 | .send() 1343 | } 1344 | 1345 | fun getTagsFollowedByUser(userId: String): Boolean { 1346 | return request.prepare(endpoint = Routes.tagsFollowedByUser(userId = userId)).send() 1347 | } 1348 | 1349 | fun getHashTagSelection(hashTag: String): Boolean { 1350 | val payload = JSONObject() 1351 | .put("_csrftoken", this.token) 1352 | .put("_uid", this.userId) 1353 | .put("_uuid", this.uuid) 1354 | .put("supported_tabs", "['top','recent','places']") 1355 | .put("include_persistent", "true") 1356 | 1357 | return request.prepare(endpoint = Routes.hashTagSelection(hashTag = hashTag), payload = payload.toString()) 1358 | .send() 1359 | } 1360 | 1361 | fun getMediaInsight(mediaId: String): Boolean { 1362 | return request.prepare(endpoint = Routes.mediaInsight(mediaId = mediaId)).send() 1363 | } 1364 | 1365 | fun getSelfInsight(): Boolean { 1366 | return request.prepare(endpoint = Routes.selfInsight()).send() 1367 | } 1368 | 1369 | fun saveMedia(mediaId: String, moduleName: String = "feed_timeline"): Boolean { 1370 | val payload = JSONObject() 1371 | .put("_csrftoken", this.token) 1372 | .put("_uid", this.userId) 1373 | .put("_uuid", this.uuid) 1374 | .put("radio_type", "wifi-none") 1375 | .put("device_id", this.deviceId) 1376 | .put("module_name", moduleName) 1377 | 1378 | return request.prepare(endpoint = Routes.saveMedia(mediaId = mediaId), payload = payload.toString()).send() 1379 | } 1380 | 1381 | fun unsaveMedia(mediaId: String): Boolean { 1382 | val payload = JSONObject() 1383 | .put("_csrftoken", this.token) 1384 | .put("_uid", this.userId) 1385 | .put("_uuid", this.uuid) 1386 | 1387 | return request.prepare(endpoint = Routes.unsaveMedia(mediaId = mediaId), payload = payload.toString()).send() 1388 | } 1389 | 1390 | fun getSavedMedias(): Boolean { 1391 | return request.prepare(endpoint = Routes.getSavedMedia()).send() 1392 | } 1393 | 1394 | 1395 | // ====== DIRECT(DM) METHODS ===== // 1396 | 1397 | fun sendDirectItem(itemType: String, users: List, options: Map? = null): Boolean { 1398 | 1399 | if (!isLoggedIn) { 1400 | throw LoginException("Not logged in") 1401 | } 1402 | 1403 | val payload: MutableMap = mutableMapOf( 1404 | "_csrftoken" to this.token, 1405 | "_uid" to this.userId, 1406 | "_uuid" to this.uuid, 1407 | "client_context" to Crypto.generateUUID(true), 1408 | "action" to "send_item", 1409 | "recipient_users" to "[[${users.joinToString(separator = ",")}]]" 1410 | ) 1411 | 1412 | val header = mutableMapOf() 1413 | 1414 | var endpoint = Routes.directItem(itemType = itemType) 1415 | 1416 | val text = if (options?.get("text")?.isNotEmpty() == true) options["text"] else "" 1417 | 1418 | if (options?.get("threadId")?.isNotEmpty() == true) { 1419 | payload["thread_ids"] = options["threadId"] 1420 | } 1421 | 1422 | if (itemType == "text") { 1423 | payload["text"] = text 1424 | } else if (itemType == "link" && options?.get("urls")?.isNotEmpty() == true) { 1425 | payload["link_text"] = text 1426 | payload["link_urls"] = options["urls"] 1427 | } else if (itemType == "media_share" && options?.get("media_type") 1428 | ?.isNotEmpty() == true && options.get("media_id")?.isNotEmpty() == true 1429 | ) { 1430 | payload["text"] = text 1431 | payload["media_type"] = options["media_type"]?.toInt() 1432 | payload["media_id"] = options["media_id"] 1433 | } else if (itemType == "hashtag" && options?.get("hashtag")?.isNotEmpty() == true) { 1434 | payload["text"] = text 1435 | payload["hashtag"] = options["hashtag"] 1436 | } else if (itemType == "profile" && options?.get("profile_user_id")?.isNotEmpty() == true) { 1437 | payload["text"] = text 1438 | payload["profile_user_id"] = options["profile_user_id"] 1439 | } else if (itemType == "photo" && options?.get("filePath")?.isNotEmpty() == true) { 1440 | endpoint = Routes.directPhoto() 1441 | val filePath = options["filePath"] 1442 | val uploadId = (System.currentTimeMillis() * 1000).toString() 1443 | val file = File(filePath!!) 1444 | val photo = ByteArray(file.length().toInt()) 1445 | file.inputStream().read(photo) 1446 | val photoData = listOf( 1447 | "direct_temp_photo_${uploadId}.jpg", 1448 | Base64.getEncoder().encodeToString(photo), 1449 | "application/octet-stream", 1450 | mapOf("Content-Transfer-Encoding" to "binary") 1451 | ) 1452 | payload["photo"] = photoData 1453 | payload["photo"] = photoData 1454 | header["Content-type"] = "multipart/form-data" 1455 | } 1456 | 1457 | val url = "${HTTP.API_URL}$endpoint" 1458 | // Need to send separate request as it doesn't require signature 1459 | request.prepare(endpoint = endpoint, payload = payload.toString(), header = header) 1460 | val response = post( 1461 | url, 1462 | headers = request.headers, 1463 | data = payload, 1464 | cookies = request.persistedCookies, 1465 | allowRedirects = true 1466 | ) 1467 | 1468 | lastResponse = response 1469 | statusCode = lastResponse.statusCode 1470 | 1471 | return if (response.statusCode == 200) { 1472 | lastJSON = JsonPath.parseOrNull(response.text) 1473 | true 1474 | } else { 1475 | println("Failed to send item") 1476 | false 1477 | } 1478 | } 1479 | 1480 | fun getPendingInbox(): Boolean { 1481 | return request.prepare(endpoint = Routes.pendingInbox()).send() 1482 | } 1483 | 1484 | fun getPendingThreads(): Flow = flow { 1485 | getPendingInbox() 1486 | lastJSON?.read("$.inbox")?.read("$.threads")?.forEach { 1487 | emit(it as JSONObject) 1488 | } 1489 | } 1490 | 1491 | fun approvePendingThread(threadId: String): Boolean { 1492 | val payload = JSONObject() 1493 | .put("_csrftoken", this.token) 1494 | .put("_uid", this.userId) 1495 | .put("_uuid", this.uuid) 1496 | 1497 | return request.prepare( 1498 | endpoint = Routes.approvePendingThread(threadId = threadId), 1499 | payload = payload.toString() 1500 | ).send() 1501 | } 1502 | 1503 | fun hidePendingThread(threadId: String): Boolean { 1504 | val payload = JSONObject() 1505 | .put("_csrftoken", this.token) 1506 | .put("_uid", this.userId) 1507 | .put("_uuid", this.uuid) 1508 | 1509 | return request.prepare(endpoint = Routes.hidePendingThread(threadId = threadId), payload = payload.toString()) 1510 | .send() 1511 | } 1512 | 1513 | fun rejectPendingThread(threadId: String): Boolean { 1514 | val payload = JSONObject() 1515 | .put("_csrftoken", this.token) 1516 | .put("_uid", this.userId) 1517 | .put("_uuid", this.uuid) 1518 | 1519 | return request.prepare( 1520 | endpoint = Routes.declinePendingThread(threadId = threadId), 1521 | payload = payload.toString() 1522 | ).send() 1523 | } 1524 | 1525 | // ====== DOWNLOAD(PHOTO/VIDEO/STORY) METHODS ===== // 1526 | 1527 | fun downloadMedia(url: String, username: String, folderName: String, fileName: String): Boolean { 1528 | val directory = File("$folderName/$username") 1529 | if (!directory.exists()) { 1530 | directory.mkdirs() 1531 | } 1532 | val file = File(directory, fileName) 1533 | if (file.exists()) { 1534 | println("media already exist") 1535 | return true 1536 | } 1537 | 1538 | request.prepare(endpoint = "") 1539 | val response = get(url = url, headers = request.headers, cookies = request.persistedCookies, stream = true) 1540 | return if (response.statusCode == 200) { 1541 | response.contentIterator(chunkSize = 1024).forEach { 1542 | file.appendBytes(it) 1543 | } 1544 | true 1545 | } else { 1546 | println("Failed to download media: ${response.text}") 1547 | false 1548 | } 1549 | } 1550 | 1551 | } --------------------------------------------------------------------------------