From 40ec1ca3464b4539d3ad114ebbec02d2982644f0 Mon Sep 17 00:00:00 2001 From: hadiarajesh Date: Wed, 6 May 2020 13:35:44 +0530 Subject: [PATCH] Initial commit --- .gitignore | 6 + build.gradle | 28 + gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++ gradlew.bat | 84 + local.properties | 8 + settings.gradle | 2 + src/main/kotlin/BotTest.kt | 18 + src/main/kotlin/api/InstagramAPI.kt | 1566 +++++++++++++++ src/main/kotlin/api/Request.kt | 147 ++ src/main/kotlin/bot/InstagramBot.kt | 1749 +++++++++++++++++ src/main/kotlin/jsonpath/JsonPath.kt | 134 ++ src/main/kotlin/jsonpath/JsonResult.kt | 16 + src/main/kotlin/jsonpath/PathCompiler.kt | 254 +++ src/main/kotlin/jsonpath/Token.kt | 275 +++ src/main/kotlin/jsonpath/cache/Cache.kt | 21 + .../kotlin/jsonpath/cache/CacheProvider.kt | 37 + src/main/kotlin/jsonpath/cache/LRUCache.kt | 29 + .../kotlin/jsonpath/extension/JSONArray.kt | 12 + .../kotlin/jsonpath/extension/JSONObject.kt | 12 + src/main/kotlin/util/Config.kt | 286 +++ src/main/kotlin/util/CookiePersistor.kt | 46 + src/main/kotlin/util/Crypto.kt | 70 + src/main/kotlin/util/Device.kt | 6 + src/main/kotlin/util/LoginException.kt | 8 + src/main/kotlin/util/devices.kt | 16 + 28 files changed, 5009 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 local.properties create mode 100644 settings.gradle create mode 100644 src/main/kotlin/BotTest.kt create mode 100644 src/main/kotlin/api/InstagramAPI.kt create mode 100644 src/main/kotlin/api/Request.kt create mode 100644 src/main/kotlin/bot/InstagramBot.kt create mode 100644 src/main/kotlin/jsonpath/JsonPath.kt create mode 100644 src/main/kotlin/jsonpath/JsonResult.kt create mode 100644 src/main/kotlin/jsonpath/PathCompiler.kt create mode 100644 src/main/kotlin/jsonpath/Token.kt create mode 100644 src/main/kotlin/jsonpath/cache/Cache.kt create mode 100644 src/main/kotlin/jsonpath/cache/CacheProvider.kt create mode 100644 src/main/kotlin/jsonpath/cache/LRUCache.kt create mode 100644 src/main/kotlin/jsonpath/extension/JSONArray.kt create mode 100644 src/main/kotlin/jsonpath/extension/JSONObject.kt create mode 100644 src/main/kotlin/util/Config.kt create mode 100644 src/main/kotlin/util/CookiePersistor.kt create mode 100644 src/main/kotlin/util/Crypto.kt create mode 100644 src/main/kotlin/util/Device.kt create mode 100644 src/main/kotlin/util/LoginException.kt create mode 100644 src/main/kotlin/util/devices.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fba8f97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Project exclude paths +/.gradle/ +/build/ +/photos/ +/stories/ +/videos/ \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e35200a --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.61' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + jcenter() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3" + implementation 'com.github.jkcclemens:khttp:-SNAPSHOT' + implementation 'com.nfeld.jsonpathlite:json-path-lite:1.1.0' + compile("org.json:json:20180813") + +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..6d57edc --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..01cf6c0 --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Sun Mar 08 17:45:26 IST 2020 +sdk.dir=F\:\\SOFTWARE\\Android\\Android-SDK diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c6d70b6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'AutoGram' + diff --git a/src/main/kotlin/BotTest.kt b/src/main/kotlin/BotTest.kt new file mode 100644 index 0000000..17437b9 --- /dev/null +++ b/src/main/kotlin/BotTest.kt @@ -0,0 +1,18 @@ +import bot.InstagramBot +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect + +@InternalCoroutinesApi +@ExperimentalCoroutinesApi +fun main() = runBlocking { + + val username = "annon202020" + val password = "Bahuthard" + + val bot = InstagramBot() + bot.prepare(username, password) + bot.login() + + bot.getSelfFollowing(Int.MAX_VALUE, isUsername = true).collect { println(it) } + bot.getExploreTabMedias(7).collect { println(it) } +} \ No newline at end of file diff --git a/src/main/kotlin/api/InstagramAPI.kt b/src/main/kotlin/api/InstagramAPI.kt new file mode 100644 index 0000000..e782084 --- /dev/null +++ b/src/main/kotlin/api/InstagramAPI.kt @@ -0,0 +1,1566 @@ +package api + +import util.LoginException +import com.nfeld.jsonpathlite.JsonPath +import com.nfeld.jsonpathlite.JsonResult +import com.nfeld.jsonpathlite.extension.read +import khttp.get +import khttp.post +import khttp.responses.Response +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import util.* +import java.io.File +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* +import kotlin.math.max +import kotlin.math.min +import kotlin.random.Random + + +object InstagramAPI { + var username: String = "username" + var password: String = "password" + var deviceId: String = "xxxx" + var uuid: String = "xxxx" + var userId: String = "" + var token: String = "-" + var rankToken: String = "-" + var isLoggedIn: Boolean = false + var lastJSON: JsonResult? = null + lateinit var lastResponse: Response + var statusCode: Int = 0 + var totalRequests: Int = 0 + private var request: Request = Request() + private var cookiePersistor: CookiePersistor = CookiePersistor("") + + + // Prepare Instagram API + fun prepare() { + deviceId = Crypto.generateDeviceId(username) + uuid = Crypto.generateUUID(true) + cookiePersistor = CookiePersistor(username) + if (cookiePersistor.exist()) { + val cookieDisk = cookiePersistor.load() + val account = JSONObject(cookieDisk.account) + if (account.getString("status").toLowerCase() == "ok") { + println("Already logged in to Instagram") + val jar = cookieDisk.cookieJar + request.persistedCookies = jar + isLoggedIn = true + userId = jar.getCookie("ds_user_id")?.value.toString() + token = jar.getCookie("csrftoken")?.value.toString() + rankToken = "${userId}_$uuid" + } + } else { + println("Cookie file does not exist, need to login first") + } + } + + private fun preLoginFlow() { + println("Initiating pre login flow") + readMSISDNHeader() + syncLauncher(isLogin = true) + syncDeviceFeatures() + logAttribution() + setContactPointPrefill() + } + + private fun readMSISDNHeader(usage: String = "default"): Boolean { + val payload = JSONObject() + .put("device_id", this.uuid) + .put("mobile_subno_usage", usage) + + val header = mapOf("X-DEVICE-ID" to uuid) + + return request.prepare(endpoint = Routes.msisdnHeader(), payload = payload.toString(), header = header) + .send(true) + } + + private fun syncLauncher(isLogin: Boolean = false): Boolean { + val payload = JSONObject() + .put("id", uuid) + .put("server_config_retrieval", "1") + .put("experiments", EXPERIMENTS.LAUNCHER_CONFIGS) + + if (!isLogin) { + payload + .put("_csrftoken", token) + .put("_uid", userId) + .put("_uuid", uuid) + } + + return request.prepare(endpoint = Routes.launcherSync(), payload = payload.toString()).send(true) + } + + private fun syncDeviceFeatures(): Boolean { + val payload = JSONObject() + .put("id", uuid) + .put("server_config_retrieval", "1") + .put("experiments", EXPERIMENTS.LOGIN_EXPERIMENTS) + + val header = mapOf("X-DEVICE-ID" to uuid) + + return request.prepare(endpoint = Routes.qeSync(), payload = payload.toString(), header = header).send(true) + } + + private fun logAttribution(usage: String = "default"): Boolean { + val payload = JSONObject() + .put("adid", Crypto.generateUUID(true)) + + return request.prepare(endpoint = Routes.logAttribution(), payload = payload.toString()).send(true) + } + + private fun setContactPointPrefill(usage: String = "prefill"): Boolean { + val payload = JSONObject() + .put("id", this.uuid) + .put("phone_id", Crypto.generateUUID(true)) + .put("_csrftoken", this.token) + .put("usage", usage) + + return request.prepare(endpoint = Routes.contactPointPrefill(), payload = payload.toString()).send(true) + } + + // Login to Instagram + fun login(forceLogin: Boolean = false): Boolean { + if (!isLoggedIn || forceLogin) { + preLoginFlow() + + val payload = JSONObject() + .put("_csrftoken", "missing") + .put("device_id", deviceId) + .put("_uuid", uuid) + .put("username", username) + .put("password", password) + .put("login_attempt_count", "0") + + if (request.prepare(endpoint = Routes.login(), payload = payload.toString()).send(true)) { + saveSuccessfulLogin() + return true + } else { + println("Username or password is incorrect.") + } + } + return false + } + + private fun saveSuccessfulLogin() { + cookiePersistor.save(lastResponse.text, lastResponse.cookies) + val account = lastResponse.jsonObject + if (account.getString("status").toLowerCase() == "ok") { + val jar = lastResponse.cookies + isLoggedIn = true + userId = jar.getCookie("ds_user_id")?.value.toString() + token = jar.getCookie("csrftoken")?.value.toString() + rankToken = "${userId}_$uuid" + + println("Logged in successfully") + loginFlow() + } + } + + // Sync features after successful login + private fun loginFlow() { + syncLauncher(isLogin = false) + syncUserFeatures() + // Update feed and timeline + getTimeline() + getReelsTrayFeed(reason = "cold_start") + getSuggestedSearches("users") + getSuggestedSearches("blended") + // DM update + getRankedRecipients("reshare", true) + getRankedRecipients("save", true) + getInboxV2() + getPresence() + getRecentActivity() + // Config and other stuffs + getLoomFetchConfig() + getProfileNotice() + getBatchFetch() + getExplore(true) + getAutoCompleteUserList() + } + + // Perform interactive two step verification process + fun performTwoFactorAuth(): Boolean { + println("Two-factor authentication required") + println("Enter 2FA verification code: ") + val twoFactorAuthCode = readLine() + val twoFactorAuthID = lastJSON?.read("$.two_factor_info")?.read("$.two_factor_identifier") + val payload = JSONObject() + .put("username", username) + .put("verification_code", twoFactorAuthCode) + .put("two_factor_identifier", twoFactorAuthID) + .put("password", password) + .put("device_id", deviceId) + .put("ig_sig_key_version", KEY.SIG_KEY_VERSION) + + + if (request.prepare(endpoint = Routes.twoFactorAuth(), payload = payload.toString()).send(true)) { + if (lastJSON?.read("$.status") == "ok") { + return true + } + } else { + println(lastJSON?.read("$.message")) + } + + return false + } + + // Perform interactive challenge solving + fun solveChallenge(): Boolean { + println("Checkpoint challenge required") + val challengeUrl = lastJSON?.read("$.challenge")?.read("$.api_path")?.removeRange(0, 1) + request.prepare(endpoint = challengeUrl).send(true) + + val choices = getChallengeChoices() + choices.forEach { println(it) } + print("Enter your choice: ") + val selectedChoice = readLine()?.toInt() + + val payload = JSONObject() + .put("choice", selectedChoice) + + if (request.prepare(endpoint = challengeUrl, payload = payload.toString()).send(true)) { + println("A code has been sent to the method selected, please check.") + println("Enter your code: ") + val code = readLine()?.toInt() + val secondPayload = JSONObject() + .put("security_code", code) + + request.prepare(endpoint = challengeUrl, payload = secondPayload.toString()).send(true) + if (lastJSON?.read("$.action") == "close" && lastJSON?.read("$.status") == "ok") { + return true + } + } + + println("Failed to log in. Try again") + return false + } + + // Get challenge choices + private fun getChallengeChoices(): List { + val choices: MutableList = mutableListOf() + if (lastJSON?.read("$.step_name") == "select_verify_method") { + choices.add("Checkpoint challenge received") + + val stepData = lastJSON?.read("$.step_data") + if (stepData?.has("phone_number") == true) { + choices.add("0 - Phone ${stepData.get("$.phone_number")}") + } + + if (stepData?.has("email") == true) { + choices.add("0 - Phone ${stepData.get("$.email")}") + } + } + + if (lastJSON?.read("$.step_name") == "delta_login_review") { + choices.add("Login attempt challenge received") + choices.add("0 - It was me") + choices.add("1 - It wasn't me") + } + + if (choices.isEmpty()) { + println("No challenge found, might need to change password") + println("Proceed with changing password? (y/n)") + val choice = readLine() + if (choice == "y") { + println("Enter your new password:") + val newPassword = readLine() + if (changePassword(newPassword!!)) { + println("Password changed successfully. Please re-try now") + } else { + println("Failed to change password.") + } + } else if (choice == "n") { + println("You must need to change password to avoid being detected by Instagram") + } else { + println("Invalid input") + } + choices.add("0 - Nothing found") + println("Please quit and retry again") + } + + return choices + } + + //Logout from instagram + fun logout(): Boolean { + if (request.prepare(endpoint = Routes.logout(), payload = "{}").send()) { + cookiePersistor.destroy() + println("Logged out from instagram") + return true + } + return false + } + + private fun syncUserFeatures(): Boolean { + val payload = JSONObject() + .put("_csrftoken", token) + .put("device_id", deviceId) + .put("_uuid", uuid) + .put("id", this.uuid) + .put("experiments", EXPERIMENTS.EXPERIMENTS) + + val header = mapOf("X-DEVICE-ID" to uuid) + + return request.prepare(endpoint = Routes.qeSync(), payload = payload.toString(), header = header).send() + } + + // Get zoneOffSet of current System timezone + private fun getZoneOffSet(): String = + ZoneId.of(Calendar.getInstance().timeZone.toZoneId().toString()).rules.getOffset(LocalDateTime.now()).toString() + .replace( + ":", + "" + ) + + // Get timeline feed + fun getTimeline(options: List = listOf(), maxId: String = ""): Boolean { + val payload = JSONObject() + .put("_csrftoken", token) + .put("_uuid", uuid) + .put("is_prefetch", 0) + .put("phone_id", Crypto.generateUUID(true)) + .put("device_id", deviceId) + .put("client_session_id", Crypto.generateUUID(true)) + .put("battery_level", Random.Default.nextInt(25, 100)) + .put("is_charging", Random.Default.nextInt(0, 1)) + .put("will_sound_on", Random.Default.nextInt(0, 1)) + .put("is_on_screen", true) + .put("timezone_offset", getZoneOffSet()) + .put("reason", "cold_start_fetch") + .put("is_pull_to_refresh", "0") + + if ("is_pull_to_refresh" in options) { + payload + .put("reason", "pull_to_refresh") + .put("is_pull_to_refresh", "1") + } + + val header = mapOf("X-Ads-Opt-Out" to "0") + + return request.prepare(endpoint = Routes.timeline(maxId = maxId), payload = payload.toString(), header = header) + .send() + } + + // Get Reels(Stories) + fun getReelsTrayFeed(reason: String = "pull_to_refresh"): Boolean { + // reason can be = cold_start or pull_to_refresh + val payload = JSONObject() + .put("supported_capabilities_new", EXPERIMENTS.SUPPORTED_CAPABILITIES) + .put("reason", reason) + .put("_csrftoken", token) + .put("_uuid", uuid) + + return request.prepare(endpoint = Routes.reelsTrayFeed(), payload = payload.toString()).send() + } + + // Get suggested searches + private fun getSuggestedSearches(type: String = "users"): Boolean { + val payload = JSONObject() + .put("type", type) + + return request.prepare(endpoint = Routes.suggestedSearches(), payload = payload.toString()).send() + } + + // Get ranked recipients + private fun getRankedRecipients(mode: String, showThreads: Boolean, query: String = ""): Boolean { + val payload = JSONObject() + .put("mode", mode) + .put("show_threads", showThreads) + .put("use_unified_inbox", "true") + + if (query.isNotEmpty()) { + payload + .put("query", query) + } + + return request.prepare(endpoint = Routes.rankedRecipients(), payload = payload.toString()).send() + } + + // Get Direct messages + fun getInboxV2(): Boolean { + val payload = JSONObject() + .put("persistentBadging", true) + .put("use_unified_inbox", true) + + return request.prepare(Routes.inboxV2(), payload = payload.toString()).send() + } + + // Get presence + private fun getPresence(): Boolean { + return request.prepare(Routes.presence()).send() + } + + // Get recent activity of user + private fun getRecentActivity(): Boolean { + return request.prepare(endpoint = Routes.recentActivity()).send() + } + + fun getFollowingRecentActivity(): Boolean { + return request.prepare(endpoint = "news").send() + } + + fun getLoomFetchConfig(): Boolean { + return request.prepare(endpoint = Routes.loomFetchConfig()).send() + } + + fun getProfileNotice(): Boolean { + return request.prepare(endpoint = Routes.profileNotice()).send() + } + + fun getBatchFetch(): Boolean { + val payload = JSONObject() + .put("_csrftoken", token) + .put("_uid", userId) + .put("_uuid", uuid) + .put("scale", 3) + .put("version", 1) + .put("vc_policy", "default") + .put("surfaces_to_triggers", EXPERIMENTS.SURFACES_TO_TRIGGERS) + .put("surfaces_to_queries", EXPERIMENTS.SURFACES_TO_QUERIES) + + return request.prepare(endpoint = Routes.batchFetch(), payload = payload.toString()).send() + } + + + // ====== MEDIA METHODS ===== // + + fun editMedia(mediaId: String, caption: String = ""): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("caption_text", caption) + + return request.prepare(endpoint = Routes.editMedia(mediaId = mediaId), payload = payload.toString()).send() + } + + fun removeSelfTagFromMedia(mediaId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare( + endpoint = Routes.removeSelfTagFromMedia(mediaId = mediaId), + payload = payload.toString() + ).send() + } + + fun getMediaInfo(mediaId: String): Boolean { + return request.prepare(endpoint = Routes.mediaInfo(mediaId = mediaId)).send() + } + + fun archiveMedia(mediaId: String, mediaType: Int, undo: Boolean = false): Boolean { + val action = if (undo) "undo_only_me" else "only_me" + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("media_id", mediaId) + + return request.prepare( + endpoint = Routes.archiveMedia( + mediaId = mediaId, + action = action, + mediaType = mediaType + ), payload = payload.toString() + ).send() + } + + fun deleteMedia(mediaId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("media_id", mediaId) + + return request.prepare(endpoint = Routes.deleteMedia(mediaId = mediaId), payload = payload.toString()).send() + } + + private fun generateUserBreadCrumb(size: Int): String { + val key = "iN4\$aGr0m" + val timeElapsed = Random.nextInt(500, 1500) + (size * Random.nextInt(500, 1500)) + val textChangeEventCount = max(1, (size / Random.nextInt(3, 5))) + val dt: Long = System.currentTimeMillis() * 1000 + + val payload = "$size $timeElapsed ${textChangeEventCount.toFloat()} $dt" + val signedKeyAndData = Crypto.generateHMAC( + key.toByteArray(Charsets.US_ASCII).toString(), + payload.toByteArray(Charsets.US_ASCII).toString() + ) + + return "${Base64.getEncoder().encodeToString(signedKeyAndData.toByteArray())}\n${Base64.getEncoder() + .encodeToString( + payload.toByteArray(Charsets.US_ASCII) + )}" + } + + fun comment(mediaId: String, commentText: String): Boolean { + val payload = JSONObject() + .put("container_module", "comments_v2") + .put("user_breadcrumb", generateUserBreadCrumb(commentText.length)) + .put("idempotence_token", Crypto.generateUUID(true)) + .put("comment_text", commentText) + .put("radio_type", "wifi-none") + .put("device_id", this.deviceId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.comment(mediaId = mediaId), payload = payload.toString()).send() + } + + fun replyToComment(mediaId: String, parentCommentId: String, commentText: String): Boolean { + val payload = JSONObject() + .put("comment_text", commentText) + .put("replied_to_comment_id", parentCommentId) + + return request.prepare(endpoint = Routes.comment(mediaId = mediaId), payload = payload.toString()).send() + } + + + fun deleteComment(mediaId: String, commentId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare( + endpoint = Routes.deleteComment(mediaId = mediaId, commentId = commentId), + payload = payload.toString() + ).send() + } + + + fun getCommentLiker(commentId: String): Boolean { + return request.prepare(endpoint = Routes.commentLikers(commentId = commentId)).send() + } + + fun getMediaLiker(mediaId: String): Boolean { + return request.prepare(endpoint = Routes.mediaLikers(mediaId = mediaId)).send() + } + + fun likeComment(commentId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("is_carousel_bumped_post", false) + .put("container_module", "comments_v2") + .put("feed_position", "0") + + return request.prepare(endpoint = Routes.likeComment(commentId = commentId), payload = payload.toString()) + .send() + } + + fun unlikeComment(commentId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("is_carousel_bumped_post", false) + .put("container_module", "comments_v2") + .put("feed_position", "0") + + return request.prepare(endpoint = Routes.unlikeComment(commentId = commentId), payload = payload.toString()) + .send() + } + + fun like( + mediaId: String, doubleTap: Int = 0, containerModule: String = "feed_short_url", + feedPosition: Int = 0, username: String = "", userId: String = "", + hashTagName: String = "", hashTagId: String = "", entityPageName: String = "", entityPageId: String = "" + ): Boolean { + + val payload = JSONObject() + .put("radio_type", "wifi-none") + .put("device_id", this.deviceId) + .put("media_id", mediaId) + .put("container_module", containerModule) + .put("feed_position", feedPosition.toString()) + .put("is_carousel_bumped_post", "false") + + if (containerModule == "feed_timeline") { + payload + .put("inventory_source", "media_or_ad") + } + + if (username.isNotEmpty()) { + payload + .put("username", username) + .put("user_id", userId) + } + + if (hashTagName.isNotEmpty()) { + payload + .put("hashtag_name", hashTagName) + .put("hashtag_id", hashTagId) + } + + if (entityPageName.isNotEmpty()) { + payload + .put("entity_page_name", entityPageName) + .put("entity_page_id", entityPageId) + } + + payload + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("d=", Random.nextInt(0, 1).toString()) + + val dt = if (doubleTap != 0) Random.nextInt(0, 1).toString() else doubleTap.toString() + val extraSig = mutableMapOf("d=" to dt) + + return request.prepare( + endpoint = Routes.like(mediaId = mediaId), + payload = payload.toString(), + extraSig = extraSig + ).send() + } + + fun unlike(mediaId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("media_id", mediaId) + .put("radio_type", "wifi-none") + .put("is_carousel_bumped_post", "false") + .put("container_module", "photo_view_other") + .put("feed_position", "0") + + return request.prepare(endpoint = Routes.unlike(mediaId = mediaId), payload = payload.toString()).send() + } + + fun getMediaComments(mediaId: String, maxId: String = ""): Boolean { + return request.prepare(endpoint = Routes.mediaComments(mediaId = mediaId, maxId = maxId)).send() + } + + fun getExplore(isPrefetch: Boolean = false): Boolean { + val payload = JSONObject() + .put("is_prefetch", isPrefetch) + .put("is_from_promote", false) + .put("timezone_offset", getZoneOffSet()) + .put("session_id", Crypto.generateUUID(true)) + .put("supported_capabilities_new", EXPERIMENTS.SUPPORTED_CAPABILITIES) + + if (isPrefetch) { + payload + .put("max_id", 0) + .put("module", "explore_popular") + } + + return request.prepare(endpoint = Routes.explore(), payload = payload.toString()).send() + } + + // Get auto complete user list + fun getAutoCompleteUserList(): Boolean { + return request.prepare(endpoint = Routes.autoCompleteUserList()).send() + } + + fun getMegaPhoneLog(): Boolean { + return request.prepare(endpoint = Routes.megaphoneLog()).send() + } + + fun expose(): Boolean { + val payload = JSONObject() + .put("id", uuid) + .put("experiment", "ig_android_profile_contextual_feed") + + return request.prepare(endpoint = Routes.expose(), payload = payload.toString()).send() + } + + fun getUserInfoByName(username: String): Boolean { + return request.prepare(endpoint = Routes.userInfoByName(username = username)).send() + } + + fun getUserInfoByID(userId: String): Boolean { + return request.prepare(endpoint = Routes.userInfoById(userId = userId)).send() + } + + fun getUserIdByName(username: String): String { + request.prepare(endpoint = Routes.userInfoByName(username = username)).send() + return lastJSON?.read("$.user")?.get("pk").toString() + } + + fun getUserTagMedias(userId: String): Boolean { + return request.prepare(endpoint = Routes.userTags(userId = userId, rankToken = rankToken)).send() + } + + fun getSelfUserTags(): Boolean { + return getUserTagMedias(userId) + } + + fun getGeoMedia(userId: String): Boolean { + return request.prepare(endpoint = Routes.geoMedia(userId = userId)).send() + } + + fun getSelfGeoMedia(): Boolean { + return getGeoMedia(userId) + } + + + // ====== FEED METHODS ===== // + + private fun getUserFeed(userId: String, maxId: String = "", minTimeStamp: String = ""): Boolean { + return request.prepare( + endpoint = Routes.userFeed( + userId = userId, + maxId = maxId, + minTimeStamp = minTimeStamp, + rankToken = rankToken + ) + ).send() + } + + fun getSelfUserFeed(maxId: String = "", minTimeStamp: String = ""): Boolean { + return getUserFeed(userId = userId, maxId = maxId, minTimeStamp = minTimeStamp) + } + + fun getHashTagFeed(hashTag: String, maxId: String = ""): Boolean { + return request.prepare(endpoint = Routes.hashTagFeed(hashTag = hashTag, maxId = maxId, rankToken = rankToken)) + .send() + } + + fun getLocationFeed(locationId: String, maxId: String = ""): Boolean { + return request.prepare( + endpoint = Routes.locationFeed( + locationId = locationId, + maxId = maxId, + rankToken = rankToken + ) + ).send() + } + + fun getPopularFeeds(): Boolean { + return request.prepare(endpoint = Routes.popularFeed(rankToken = rankToken)).send() + } + + private fun getLikedMedia(maxId: String = ""): Boolean { + return request.prepare(endpoint = Routes.likedFeed(maxId = maxId)).send() + } + + + // ====== FRIENDSHIPS METHODS ===== // + private fun getUserFollowers(userId: String, maxId: String = ""): Boolean { + return request.prepare(endpoint = Routes.userFollowers(userId = userId, maxId = maxId, rankToken = rankToken)) + .send() + } + + private fun getSelfUserFollowers(): Boolean { + return getUserFollowers(userId) + } + + private fun getUserFollowings(userId: String, maxId: String = ""): Boolean { + return request.prepare(endpoint = Routes.userFollowings(userId = userId, maxId = maxId, rankToken = rankToken)) + .send() + } + + private fun getSelfUserFollowings(): Boolean { + return getUserFollowings(userId) + } + + + fun follow(userId: String): Boolean { + val payload = JSONObject() + .put("radio_type", "wifi-none") + .put("device_id", this.deviceId) + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.follow(userId = userId), payload = payload.toString()).send() + } + + fun unfollow(userId: String): Boolean { + val payload = JSONObject() + .put("radio_type", "wifi-none") + .put("device_id", this.deviceId) + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.unfollow(userId = userId), payload = payload.toString()).send() + } + + fun removeFollower(userId: String): Boolean { + val payload = JSONObject() + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.removeFollower(userId = userId), payload = payload.toString()).send() + } + + fun block(userId: String): Boolean { + val payload = JSONObject() + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.block(userId = userId), payload = payload.toString()).send() + } + + fun unblock(userId: String): Boolean { + val payload = JSONObject() + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.unblock(userId = userId), payload = payload.toString()).send() + } + + fun getUserFriendship(userId: String): Boolean { + val payload = JSONObject() + .put("user_id", userId) + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.userFriendship(userId = userId), payload = payload.toString()).send() + } + + fun muteUser(userId: String, isMutePosts: Boolean = false, isMuteStory: Boolean = false): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + if (isMutePosts) { + payload + .put("target_posts_author_id", userId) + } + + if (isMuteStory) { + payload + .put("target_reel_author_id", userId) + } + + return request.prepare(endpoint = Routes.muteUser(), payload = payload.toString()).send() + } + + fun getMutedUsers(mutedContentType: String): Boolean { + if (mutedContentType != "stories") { + throw NotImplementedError("API does not support getting friends with provided muted content type") + } + + return request.prepare(endpoint = Routes.getMutedUser()).send() + } + + fun unmuteUser(userId: String, isUnmutePosts: Boolean = false, isUnmuteStory: Boolean = false): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + if (isUnmutePosts) { + payload + .put("target_posts_author_id", userId) + } + + if (isUnmuteStory) { + payload + .put("target_reel_author_id", userId) + } + + return request.prepare(endpoint = Routes.unmuteUser(), payload = payload.toString()).send() + } + + fun getPendingFriendRequests(): Boolean { + return request.prepare(endpoint = Routes.pendingFriendRequests()).send() + } + + fun approvePendingFollowRequest(userId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("user_id", userId) + + return request.prepare( + endpoint = Routes.approvePendingFollowRequest(userId = userId), + payload = payload.toString() + ).send() + } + + fun rejectPendingFollowRequest(userId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("user_id", userId) + + return request.prepare( + endpoint = Routes.rejectPendingFollowRequest(userId = userId), + payload = payload.toString() + ).send() + } + + fun getDirectShare(): Boolean { + return request.prepare(endpoint = Routes.directShare()).send() + } + + private fun getTotalFollowersOrFollowings( + userId: String, amount: Int = Int.MAX_VALUE, isFollower: Boolean = true, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow = flow { + + val userType = if (isFollower) "follower_count" else "following_count" + val userKey = if (isUsername) "username" else "pk" + var nextMaxId = "" + var sleepTrack = 0 + var counter = 0 + val total: Int + val isWriteToFile = fileNameToWrite.isNotEmpty() + val userInfo: JsonResult? + + getUserInfoByID(userId).let { userInfo = lastJSON } + + val user = userInfo?.read("$.user") + + if (user != null) { + + if (user.read("$.is_private") == true) { + return@flow + } + total = min(amount, user.read("$.${userType}")!!) + + if (total >= 20000) { + println("Consider saving the result in file. This operation will take time") + } + } else { + return@flow + } + + if (isWriteToFile) { + if (File(fileNameToWrite).exists()) { + if (!isOverwrite) { + println("File $fileNameToWrite already exist. Not overwriting") + return@flow + } else { + println("Overwriting $fileNameToWrite file") + } + } + + withContext(Dispatchers.IO) { + File(fileNameToWrite).createNewFile() + } + + } + + val type = if (isFollower) "Followers" else "Following" + println("Getting $type of $userId") + val br = if (isWriteToFile) File(fileNameToWrite).bufferedWriter() else null + while (true) { + if (isFollower) { + getUserFollowers(userId, nextMaxId) + } else { + getUserFollowings(userId, nextMaxId) + } + + lastJSON?.read("$.users")?.forEach { + val obj = it as JSONObject + if (isFilterPrivate && obj.read("$.is_private") == true) { + return@forEach + } + if (isFilterVerified && obj.read("$.is_verified") == true) { + return@forEach + } + + val key = obj.get(userKey).toString() + emit(key) + counter += 1 + + if (isWriteToFile) { + br?.appendln(key) + } + + if (counter >= total) { + withContext(Dispatchers.IO) { + br?.close() + } + return@flow + } + + sleepTrack += 1 + if (sleepTrack >= 5000) { + val sleepTime = Random.nextLong(120, 180) + println("Waiting %.2f minutes due to too many requests.".format((sleepTime.toFloat() / 60))) + delay(sleepTime * 1000) + sleepTrack = 0 + } + } + + if (lastJSON?.read("$.big_list") == false) { + withContext(Dispatchers.IO) { + br?.close() + } + return@flow + } + + nextMaxId = + if (isFollower) lastJSON?.read("$.next_max_id") + .toString() else lastJSON?.read("$.next_max_id").toString() + } + } + + fun getTotalFollowers( + userId: String, amountOfFollowers: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return getTotalFollowersOrFollowings( + userId = userId, amount = amountOfFollowers, isFollower = true, isUsername = isUsername, + isFilterPrivate = isFilterPrivate, isFilterVerified = isFilterVerified, fileNameToWrite = fileNameToWrite, + isOverwrite = isOverwrite + ) + } + + fun getTotalFollowing( + userId: String, amountOfFollowing: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return getTotalFollowersOrFollowings( + userId = userId, amount = amountOfFollowing, isFollower = false, isUsername = isUsername, + isFilterPrivate = isFilterPrivate, isFilterVerified = isFilterVerified, fileNameToWrite = fileNameToWrite, + isOverwrite = isOverwrite + ) + } + + fun getLastUserFeed(userId: String, amount: Int, minTimeStamp: String = ""): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (getUserFeed(userId = userId, maxId = nextMaxId, minTimeStamp = minTimeStamp)) { + val items = lastJSON?.read("$.items") + + if (items != null) { + items.forEach { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } else { + return@flow + } + + if (lastJSON?.read("$.more_available") == false) { + return@flow + } + + nextMaxId = lastJSON?.read("$.next_max_id")!! + } else { + return@flow + } + } + } + + fun getTotalUserFeed(userId: String, minTimeStamp: String = ""): Flow { + return getLastUserFeed(userId = userId, amount = Int.MAX_VALUE, minTimeStamp = minTimeStamp) + } + + fun getTotalHashTagMedia(hashTag: String, amount: Int = 10): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (getHashTagFeed(hashTag = hashTag, maxId = nextMaxId)) { + val rankedItems = lastJSON?.read("$.ranked_items") + rankedItems?.forEach { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + val items = lastJSON?.read("$.items") + items?.forEach { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + + if (lastJSON?.read("$.more_available") == false) { + return@flow + } + + nextMaxId = lastJSON?.read("$.next_max_id")!! + } else { + return@flow + } + } + } + + fun getTotalHashTagUsers(hashTag: String, amount: Int = 10): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (getHashTagFeed(hashTag = hashTag, maxId = nextMaxId)) { + val rankedItems = lastJSON?.read("$.ranked_items") + rankedItems?.forEach { it -> + val item = it as JSONObject + item.read("$.user")?.let { emit(it) } + counter += 1 + if (counter >= amount) { + return@flow + } + } + val items = lastJSON?.read("$.items") + items?.forEach { it -> + val item = it as JSONObject + item.read("$.user")?.let { emit(it) } + counter += 1 + if (counter >= amount) { + return@flow + } + } + + if (lastJSON?.read("$.more_available") == false) { + return@flow + } + + nextMaxId = lastJSON?.read("$.next_max_id")!! + } else { + return@flow + } + } + } + + fun getTotalLikedMedia(amount: Int): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (getLikedMedia(maxId = nextMaxId)) { + val items = lastJSON?.read("$.items") + items?.forEach { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + + if (lastJSON?.read("$.more_available") == false) { + return@flow + } + + nextMaxId = lastJSON?.read("$.next_max_id")!! + } else { + return@flow + } + } + } + + fun changePassword(newPassword: String): Boolean { + val payload = JSONObject() + .put("old_password", this.password) + .put("new_password1", newPassword) + .put("new_password2", newPassword) + + return request.prepare(endpoint = Routes.changePassword(), payload = payload.toString()).send(true) + } + + fun removeProfilePicture(): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.removeProfilePicture(), payload = payload.toString()).send() + } + + fun setAccountPrivate(): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.setAccountPrivate(), payload = payload.toString()).send() + } + + fun setAccountPublic(): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.setAccountPublic(), payload = payload.toString()).send() + } + + fun setNameAndPhone(name: String = "", phone: String = ""): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("first_name", name) + .put("phone_number", phone) + + return request.prepare(endpoint = Routes.setNameAndPhone(), payload = payload.toString()).send() + } + + fun getProfileData(): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.profileData(), payload = payload.toString()).send() + } + + fun editProfile( + url: String = "", + phone: String, + firstName: String, + biography: String, + email: String, + gender: Int + ): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("external_url", url) + .put("phone_number", phone) + .put("username", this.username) + .put("full_name", firstName) + .put("biography", biography) + .put("email", email) + .put("gender", gender) + + return request.prepare(endpoint = Routes.editAccount(), payload = payload.toString()).send() + } + + fun searchUsers(userName: String): Boolean { + return request.prepare(endpoint = Routes.searchUser(userName = userName, rankToken = this.rankToken)).send() + } + + fun searchHashTags(hashTagName: String, amount: Int = 50): Boolean { + return request.prepare( + endpoint = Routes.searchHashTag( + hashTagName = hashTagName, + amount = amount, + rankToken = this.rankToken + ) + ).send() + } + + fun searchLocations(locationName: String, amount: Int = 50): Boolean { + return request.prepare( + endpoint = Routes.searchLocation( + locationName = locationName, + amount = amount, + rankToken = this.rankToken + ) + ).send() + } + + fun getUserReel(userId: String): Boolean { + return request.prepare(endpoint = Routes.userReel(userId = userId)).send() + } + + fun getUsersReel(userIds: List): Boolean { + val payload = JSONObject() + .put("_csrftoken", token) + .put("_uid", userId) + .put("_uuid", uuid) + .put("user_ids", userIds) + + return request.prepare(endpoint = Routes.multipleUsersReel(), payload = payload.toString()).send() + } + + fun watchReels(reels: List): Boolean { + val storySeen: MutableMap> = mutableMapOf() + val currentTime = System.currentTimeMillis() + val reverseSortedReels = reels.sortedByDescending { it.get("taken_at").toString() } + //it.read("$.taken_at") + + for ((index, story) in reverseSortedReels.withIndex()) { + val storySeenAt = currentTime - min( + index + 1 + Random.nextLong(0, 2), + max(0, currentTime - story.get("taken_at").toString().toLong()) + ) + storySeen["${story.get("id")}_${story.read("$.user")?.get("pk").toString()}"] = + listOf("${story.get("taken_at")}_${storySeenAt}") +// storySeen["${JEToString(story["id"])}_${JEToString(story["user"]?.get("pk"))}"] = listOf("${JEToString(story["taken_at"])}_${storySeenAt}") + } + + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("reels", storySeen as Map?) + + return request.prepare( + endpoint = Routes.watchReels(), + payload = payload.toString(), + API_URL = "https://i.instagram.com/api/v2/" + ).send() + } + + fun getUserStories(userId: String): Boolean { + return request.prepare(endpoint = Routes.userStories(userId = userId)).send() + } + + fun getSelfStoryViewers(storyId: String): Boolean { + return request.prepare(endpoint = Routes.selfStoryViewers(storyId = storyId)).send() + } + + fun getIGTVSuggestions(): Boolean { + return request.prepare(endpoint = Routes.igtvSuggestions()).send() + } + + fun getHashTagStories(hashTag: String): Boolean { + return request.prepare(endpoint = Routes.hashTagStories(hashTag = hashTag)).send() + } + + fun followHashTag(hashTag: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.followHashTag(hashTag = hashTag), payload = payload.toString()).send() + } + + fun unfollowHashTag(hashTag: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.unfollowHashTag(hashTag = hashTag), payload = payload.toString()) + .send() + } + + fun getTagsFollowedByUser(userId: String): Boolean { + return request.prepare(endpoint = Routes.tagsFollowedByUser(userId = userId)).send() + } + + fun getHashTagSelection(hashTag: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("supported_tabs", "['top','recent','places']") + .put("include_persistent", "true") + + return request.prepare(endpoint = Routes.hashTagSelection(hashTag = hashTag), payload = payload.toString()) + .send() + } + + fun getMediaInsight(mediaId: String): Boolean { + return request.prepare(endpoint = Routes.mediaInsight(mediaId = mediaId)).send() + } + + fun getSelfInsight(): Boolean { + return request.prepare(endpoint = Routes.selfInsight()).send() + } + + fun saveMedia(mediaId: String, moduleName: String = "feed_timeline"): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + .put("radio_type", "wifi-none") + .put("device_id", this.deviceId) + .put("module_name", moduleName) + + return request.prepare(endpoint = Routes.saveMedia(mediaId = mediaId), payload = payload.toString()).send() + } + + fun unsaveMedia(mediaId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.unsaveMedia(mediaId = mediaId), payload = payload.toString()).send() + } + + fun getSavedMedias(): Boolean { + return request.prepare(endpoint = Routes.getSavedMedia()).send() + } + + + // ====== DIRECT(DM) METHODS ===== // + + fun sendDirectItem(itemType: String, users: List, options: Map? = null): Boolean { + + if (!isLoggedIn) { + throw LoginException("Not logged in") + } + + val payload: MutableMap = mutableMapOf( + "_csrftoken" to this.token, + "_uid" to this.userId, + "_uuid" to this.uuid, + "client_context" to Crypto.generateUUID(true), + "action" to "send_item", + "recipient_users" to "[[${users.joinToString(separator = ",")}]]" + ) + + val header = mutableMapOf() + + var endpoint = Routes.directItem(itemType = itemType) + + val text = if (options?.get("text")?.isNotEmpty() == true) options["text"] else "" + + if (options?.get("threadId")?.isNotEmpty() == true) { + payload["thread_ids"] = options["threadId"] + } + + if (itemType == "text") { + payload["text"] = text + } else if (itemType == "link" && options?.get("urls")?.isNotEmpty() == true) { + payload["link_text"] = text + payload["link_urls"] = options["urls"] + } else if (itemType == "media_share" && options?.get("media_type") + ?.isNotEmpty() == true && options.get("media_id")?.isNotEmpty() == true + ) { + payload["text"] = text + payload["media_type"] = options["media_type"]?.toInt() + payload["media_id"] = options["media_id"] + } else if (itemType == "hashtag" && options?.get("hashtag")?.isNotEmpty() == true) { + payload["text"] = text + payload["hashtag"] = options["hashtag"] + } else if (itemType == "profile" && options?.get("profile_user_id")?.isNotEmpty() == true) { + payload["text"] = text + payload["profile_user_id"] = options["profile_user_id"] + } else if (itemType == "photo" && options?.get("filePath")?.isNotEmpty() == true) { + endpoint = Routes.directPhoto() + val filePath = options["filePath"] + val uploadId = (System.currentTimeMillis() * 1000).toString() + val file = File(filePath!!) + val photo = ByteArray(file.length().toInt()) + file.inputStream().read(photo) + val photoData = listOf( + "direct_temp_photo_${uploadId}.jpg", + Base64.getEncoder().encodeToString(photo), + "application/octet-stream", + mapOf("Content-Transfer-Encoding" to "binary") + ) + payload["photo"] = photoData + payload["photo"] = photoData + header["Content-type"] = "multipart/form-data" + } + + val url = "${HTTP.API_URL}$endpoint" + // Need to send separate request as it doesn't require signature + request.prepare(endpoint = endpoint, payload = payload.toString(), header = header) + val response = post( + url, + headers = request.headers, + data = payload, + cookies = request.persistedCookies, + allowRedirects = true + ) + + lastResponse = response + statusCode = lastResponse.statusCode + + return if (response.statusCode == 200) { + lastJSON = JsonPath.parseOrNull(response.text) + true + } else { + println("Failed to send item") + false + } + } + + fun getPendingInbox(): Boolean { + return request.prepare(endpoint = Routes.pendingInbox()).send() + } + + fun getPendingThreads(): Flow = flow { + getPendingInbox() + lastJSON?.read("$.inbox")?.read("$.threads")?.forEach { + emit(it as JSONObject) + } + } + + fun approvePendingThread(threadId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare( + endpoint = Routes.approvePendingThread(threadId = threadId), + payload = payload.toString() + ).send() + } + + fun hidePendingThread(threadId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare(endpoint = Routes.hidePendingThread(threadId = threadId), payload = payload.toString()) + .send() + } + + fun rejectPendingThread(threadId: String): Boolean { + val payload = JSONObject() + .put("_csrftoken", this.token) + .put("_uid", this.userId) + .put("_uuid", this.uuid) + + return request.prepare( + endpoint = Routes.declinePendingThread(threadId = threadId), + payload = payload.toString() + ).send() + } + + // ====== DOWNLOAD(PHOTO/VIDEO/STORY) METHODS ===== // + + fun downloadMedia(url: String, username: String, folderName: String, fileName: String): Boolean { + val directory = File("$folderName/$username") + if (!directory.exists()) { + directory.mkdirs() + } + val file = File(directory, fileName) + if (file.exists()) { + println("media already exist") + return true + } + + request.prepare(endpoint = "") + val response = get(url = url, headers = request.headers, cookies = request.persistedCookies, stream = true) + return if (response.statusCode == 200) { + response.contentIterator(chunkSize = 1024).forEach { + file.appendBytes(it) + } + true + } else { + println("Failed to download media: ${response.text}") + false + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/api/Request.kt b/src/main/kotlin/api/Request.kt new file mode 100644 index 0000000..63765db --- /dev/null +++ b/src/main/kotlin/api/Request.kt @@ -0,0 +1,147 @@ +package api + +import util.LoginException +import com.nfeld.jsonpathlite.JsonPath +import khttp.get +import khttp.post +import khttp.responses.Response +import khttp.structures.cookie.CookieJar +import util.Crypto +import util.HTTP +import java.io.File +import kotlin.random.Random + +// Generic class to send GET/POST request +class Request { + private var url: String = "" + private var data: String = "" + private var isGet = true + var persistedCookies: CookieJar? = null + var headers = HTTP.HEADERS + private var extraSignature: MutableMap? = null + + fun prepare( + endpoint: String?, + payload: String = "", + header: Map? = null, + extraSig: Map? = null, + API_URL: String = "https://i.instagram.com/api/v1/" + ): Request { + url = "$API_URL$endpoint" + data = payload + isGet = data.isEmpty() + extraSig?.let { extraSignature?.putAll(it) } + header?.let { headers.putAll(it) } + val extraHeaders = mapOf( + "X-IG-Connection-Speed" to "-1kbps", + "X-IG-Bandwidth-Speed-KBPS" to Random.Default.nextInt(7000, 10000).toString(), + "X-IG-Bandwidth-TotalBytes-B" to Random.Default.nextInt(500000, 900000).toString(), + "X-IG-Bandwidth-TotalTime-MS" to Random.Default.nextInt(50, 150).toString() + ) + headers.putAll(extraHeaders) + + return this + } + + fun send(isLogin: Boolean = false): Boolean { + + if (!InstagramAPI.isLoggedIn && !isLogin) { + throw LoginException("Please login first") + } + + val response: Response + if (isGet) { + response = if (persistedCookies == null) { + get(url = url, headers = headers) + } else { + get(url = url, headers = headers, cookies = persistedCookies) + } + } else { + val signature = data.let { Crypto.signData(it) } + val payload = mutableMapOf( + "signed_body" to "${signature.signed}.${signature.payload}", + "ig_sig_key_version" to signature.sigKeyVersion + ) + + response = if (persistedCookies == null) { + post(url, headers = headers, data = payload) + } else { + extraSignature?.let { payload.putAll(it) } + post(url, headers = headers, data = payload, cookies = persistedCookies, allowRedirects = true) + } + } + + if (persistedCookies == null) { + persistedCookies = response.cookies + } else { + persistedCookies?.putAll(response.cookies) + } + + InstagramAPI.totalRequests += 1 + InstagramAPI.lastResponse = response + InstagramAPI.statusCode = response.statusCode + + if (response.statusCode == 200) { + InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) + return true + } else { + if (response.statusCode != 404) { + InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) + + if (InstagramAPI.lastJSON?.read("$.message") == "feedback_required") { + println("ATTENTION! feedback required") + } + } + + when (response.statusCode) { + 429 -> { + val sleepMinutes = 5L + println("Request return 429, it means too many request. I will go to sleep for $sleepMinutes minutes") + Thread.sleep(sleepMinutes * 60 * 1000) + } + 400 -> { + InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) + when { + InstagramAPI.lastJSON?.read("$.two_factor_required") == true -> { + // Perform interactive two factor authentication + return InstagramAPI.performTwoFactorAuth() + } + + InstagramAPI.lastJSON?.read("$.message") == "challenge_required" -> { + // Perform interactive challenge solving + return InstagramAPI.solveChallenge() + } + + else -> { + println("Instagram's error message: ${InstagramAPI.lastJSON?.read("$.message")}, STATUS_CODE: ${response.statusCode}") + return false + } + } + } + 403 -> { + InstagramAPI.lastJSON = JsonPath.parseOrNull(response.text) + + if (InstagramAPI.lastJSON?.read("$.message") == "login_required") { + println("Re-login required. Clearing cookie file") + val cookieFile = File(InstagramAPI.username) + if (cookieFile.exists()) { + if (cookieFile.delete()) { + println("Cookie file cleared successfully") + } + } + println("Cookie file does not found") + } else { + println("Something went wrong. ${response.text}") + } + return false + } + 405 -> { + println("This method is not allowed") + return false + } + } + } + + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/bot/InstagramBot.kt b/src/main/kotlin/bot/InstagramBot.kt new file mode 100644 index 0000000..f26c665 --- /dev/null +++ b/src/main/kotlin/bot/InstagramBot.kt @@ -0,0 +1,1749 @@ +package bot + +import api.InstagramAPI +import com.nfeld.jsonpathlite.JsonResult +import com.nfeld.jsonpathlite.extension.read +import khttp.responses.Response +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.util.* +import kotlin.random.Random + +class InstagramBot( + maxLikesPerDay: Int = 1000, + maxUnlikesPerDay: Int = 1000, + maxFollowsPerDay: Int = 350, + maxUnfollowsPerDay: Int = 350, + maxCommentsPerDay: Int = 100, + maxBlocksPerDay: Int = 100, + maxUnblocksPerDay: Int = 100, + maxMessagesPerDay: Int = 300, + val blockedActionProtection: Boolean = true, + val blockedActionSleep: Boolean = false, + val blockedActionSleepDelay: Int = 300 +) { + + val minSleepTime = 60 + val maxSleepTime = 120 + + val api = InstagramAPI + + var startTime = Date() + + private val actions: List = listOf( + "likes", "unlikes", "follows", "unfollows", "comments", + "blocks", "unblocks", "messages", "archived", "unarchived", "stories_viewed" + ) + + private val totalActionPerformed: MutableMap = actions.map { it to 0 }.toMap().toMutableMap() + private var blockedActions = actions.map { it to false }.toMap().toMutableMap() + private var sleepingActions = actions.map { it to false }.toMap().toMutableMap() + private var maxActionPerDays: Map = mutableMapOf( + "likes" to maxLikesPerDay, "unlikes" to maxUnlikesPerDay, + "follows" to maxFollowsPerDay, "unfollows" to maxUnfollowsPerDay, "comments" to maxCommentsPerDay, + "blocks" to maxBlocksPerDay, "unblocks" to maxUnblocksPerDay, "messages" to maxMessagesPerDay + ) + + + val username: String + get() = api.username + val password: String + get() = api.password + val userId: String + get() = api.userId + val statusCode: Int + get() = api.statusCode + val lastJson: JsonResult? + get() = api.lastJSON + val lastResponse: Response + get() = api.lastResponse + + private fun resetCounters() { + totalActionPerformed.replaceAll { t, u -> 0 } + blockedActions.replaceAll { _, _ -> false } + startTime = Date() + } + + private fun reachedLimit(key: String): Boolean { + val currentTime = Date() + val passedDays = currentTime.compareTo(startTime) + if (passedDays > 0) { + resetCounters() + } + + return (maxActionPerDays.getValue(key) - totalActionPerformed.getValue(key)) <= 0 + } + + fun prepare(username: String, password: String) { + api.username = username + api.password = password + api.prepare() + } + + fun login(forceLogin: Boolean = false): Boolean { + return api.login(forceLogin = forceLogin) + } + + fun logout(): Boolean { + return api.logout() + } + + // === USER INFO METHODS === // + fun getUserInfoByID(userId: String): JSONObject? { + api.getUserInfoByID(userId) + return api.lastJSON?.read("$.user") + + } + + fun getUserInfoByName(username: String): JSONObject? { + api.getUserInfoByName(username) + return api.lastJSON?.read("$.user") + } + + private fun getUserIdByName(username: String): String { + return api.getUserIdByName(username) + } + + + private fun convertToUserId(value: String): String { + return if (value.toLongOrNull() != null) value else { + getUserIdByName(value.replace("@", "")) + } + } + + // === FOLLOWER/FOLLOWING METHODS === // + fun getSelfFollowing( + amountOfFollowing: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return api.getTotalFollowing( + userId, + amountOfFollowing, + isUsername, + isFilterPrivate, + isFilterVerified, + fileNameToWrite, + isOverwrite + ) + } + + fun getSelfFollowers( + amountOfFollowers: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return api.getTotalFollowers( + userId, + amountOfFollowers, + isUsername, + isFilterPrivate, + isFilterVerified, + fileNameToWrite, + isOverwrite + ) + + } + + fun getUserFollowing( + userId: String, amountOfFollowing: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return api.getTotalFollowing( + convertToUserId(userId), + amountOfFollowing, + isUsername, + isFilterPrivate, + isFilterVerified, + fileNameToWrite, + isOverwrite + ) + } + + fun getUserFollowers( + userId: String, amountOfFollowers: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + return api.getTotalFollowers( + convertToUserId(userId), + amountOfFollowers, + isUsername, + isFilterPrivate, + isFilterVerified, + fileNameToWrite, + isOverwrite + ) + } + + // === USER STORIES METHODS === // + fun getUserStoriesURL(userId: String): Flow = flow { + api.getUserReel(convertToUserId(userId)) + api.lastJSON?.read("$.media_count")?.let { it -> + if (it > 0) { + val items = api.lastJSON?.read("$.items") + items?.forEach { + if ((it as JSONObject).read("$.media_type") == 1) { + (it?.read("$.image_versions2")?.read("$.candidates") + ?.first() as JSONObject)?.read( + "$.url" + )?.let { emit(it) } + } else if (it.read("$.media_type") == 2) { + (it?.read("$.video_versions")?.first() as JSONObject)?.read("$.url") + ?.let { emit(it) } + } + } + } + } + } + + // Emit all info about users's story + fun getUsersStories(userIds: List): Flow = flow { + api.getUsersReel(userIds.map { convertToUserId(it) }) + val reels = api.lastJSON?.read("$.reels") + reels?.keySet()?.forEach { + val story = reels?.read("$.${it}")!! + if (story.has("items") && story?.read("$.items")?.length()!! > 0) { + emit(story) + } + } + } + + // Emit all story items of user individually + fun getUsersStoriesItems(userIds: List): Flow = flow { + getUsersStories(userIds).collect { + it?.read("$.items")?.forEach { + emit(it as JSONObject) + } + } + } + + fun getSelfStoryViewers(): Flow = flow { + getUsersStoriesItems(listOf(username)).collect { it -> + api.getSelfStoryViewers(it.get("id").toString()) + api.lastJSON?.read("$.users")?.forEach { + emit(it as JSONObject) + } + } + } + + + // === MEDIA METHODS === // + fun getSavedMedias(): Flow = flow { + api.getSavedMedias() + api.lastJSON?.read("$.items")?.forEach { it -> + (it as JSONObject)?.read("$.media")?.let { emit(it) } + } + } + + fun getExploreTabMedias(amount: Int = 5): Flow = flow { + var counter = 0 + api.getExplore() + api.lastJSON?.read("$.items")?.forEach { it -> + (it as JSONObject)?.read("$.media")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + } + + + // === USER METHODS === // + fun getExploreTabUsers(amount: Int = 10): Flow = flow { + var counter = 0 + api.getExplore() + api.lastJSON?.read("$.items")?.forEach { it -> + (it as JSONObject)?.read("$.media")?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + } + + fun searchLocations(locationName: String, amount: Int = 5): Flow = flow { + api.searchLocations(locationName, amount) + api.lastJSON?.read("$.items")?.forEach { it -> + (it as JSONObject)?.read("$.location")?.let { emit(it) } + } + } + + fun getUsersByLocation(locationName: String, amount: Int = 5): Flow = flow { + var counter = 0 + var nextMaxId = "" + + val locationIds = searchLocations(locationName, amount).toList() + locationIds.forEach { it -> + api.getLocationFeed(it.get("pk").toString(), nextMaxId) + + val rankedItems = api.lastJSON?.read("$.ranked_items") + rankedItems?.forEach { it -> + (it as JSONObject)?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + val items = api.lastJSON?.read("$.items") + items?.forEach { it -> + (it as JSONObject)?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + + api.lastJSON?.read("$.more_available")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } + } + + fun getUsersTaggedInLocation(locationName: String, amount: Int = 5): Flow = flow { + var counter = 0 + var nextMaxId = "" + + val locationIds = searchLocations(locationName, amount).toList() + locationIds.forEach { it -> + api.getLocationFeed(it.get("pk").toString(), nextMaxId) + + val rankedItems = api.lastJSON?.read("$.ranked_items") + rankedItems?.forEach { it -> + (it as JSONObject)?.read("$.usertags")?.read("$.in") + ?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + val items = api.lastJSON?.read("$.items") + items?.forEach { it -> + (it as JSONObject)?.read("$.usertags")?.read("$.in") + ?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + api.lastJSON?.read("$.more_available")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } + } + + fun getMediasByLocation(locationName: String, amount: Int = 5): Flow = flow { + var counter = 0 + var nextMaxId = "" + + val locationIds = searchLocations(locationName, amount).toList() + locationIds.forEach { + + api.getLocationFeed(it.get("pk").toString(), nextMaxId) + + val rankedItems = api.lastJSON?.read("$.ranked_items") + rankedItems?.forEach { it -> + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + + } + + val items = api.lastJSON?.read("$.items") + items?.forEach { it -> + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + + } + + + api.lastJSON?.read("$.more_available")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } + } + + // === ACCOUNT METHODS === // + fun setAccountPublic(): Boolean { + api.setAccountPublic() + return api.lastJSON?.read("$.user")?.read("$.is_private") == false + } + + fun setAccountPrivate(): Boolean { + api.setAccountPrivate() + return api.lastJSON?.read("$.user")?.read("$.is_private") == true + } + + fun getProfileData(): JSONObject? { + api.getProfileData() + return api.lastJSON?.read("$.user") + } + + fun editProfile( + url: String = "", phone: String, firstName: String, biography: String, + email: String, gender: Int + ): JSONObject? { + api.editProfile(url, phone, firstName, biography, email, gender) + return api.lastJSON?.read("$.user") + } + + fun getPendingFollowRequests(): Flow = flow { + api.getPendingFriendRequests() + api.lastJSON?.read("$.users")?.forEach { emit(it as JSONObject) } + } + + fun getSelfUserMedias(): Flow = flow { + api.getSelfUserFeed() + api.lastJSON?.read("$.items")?.forEach { emit(it as JSONObject) } + } + + fun getTimelineMedias(amount: Int = 8): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (api.getTimeline(maxId = nextMaxId)) { + val feedItems = api.lastJSON?.read("$.feed_items") + feedItems?.forEach { it -> + (it as JSONObject)?.read("$.media_or_ad")?.let { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + api.lastJSON?.read("$.more_available")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } else { + return@flow + } + } + } + + fun getTotalUserMedias(userId: String): Flow { + return api.getTotalUserFeed(convertToUserId(userId)) + } + + fun getTotalSelfMedias(): Flow { + return api.getTotalUserFeed(userId) + } + + @ExperimentalCoroutinesApi + fun getLastUserMedias(userId: String, amount: Int): Flow { + return api.getLastUserFeed(convertToUserId(userId), amount) + } + + fun getHashTagMedias(hashTag: String, amount: Int): Flow { + return api.getTotalHashTagMedia(hashTag, amount) + } + + fun getLikedMedias(amount: Int): Flow { + return api.getTotalLikedMedia(amount) + } + + fun getMediaInfo(mediaId: String): JSONObject? { + api.getMediaInfo(mediaId) + return api.lastJSON?.read("$.items")?.first() as JSONObject + } + + fun getTimelineUsers(): Flow = flow { + api.getTimeline() + val feedItems = api.lastJSON?.read("$.feed_items") + feedItems?.forEach { it -> + (it as JSONObject)?.read("$.media_or_ad")?.read("$.user")?.let { + emit(it) + } + } + } + + fun getHashTagUsers(hashTag: String, amount: Int = 10): Flow { + return api.getTotalHashTagUsers(hashTag, amount) + } + + fun getUserTagMedias(userId: String): Flow = flow { + api.getUserTagMedias(convertToUserId(userId)) + api.lastJSON?.read("$.items")?.forEach { + emit(it as JSONObject) + } + } + + fun getMediaComments(mediaId: String, amount: Int = 5): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (api.getMediaComments(mediaId, nextMaxId)) { + api.lastJSON?.read("$.comments")?.forEach { + emit(it as JSONObject) + counter += 1 + if (counter >= amount) { + return@flow + } + } + + api.lastJSON?.read("$.has_more_comments")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } else { + return@flow + } + } + } + + fun getMediaCommenter(mediaId: String, amount: Int = 10): Flow = flow { + var counter = 0 + var nextMaxId = "" + + while (true) { + if (api.getMediaComments(mediaId, nextMaxId)) { + api.lastJSON?.read("$.comments")?.forEach { + (it as JSONObject)?.read("$.user")?.let { + emit(it) + counter += 1 + if (counter >= amount) { + return@flow + } + } + } + + api.lastJSON?.read("$.has_more_comments")?.let { if (!it) return@flow } + api.lastJSON?.read("$.next_max_id")?.let { nextMaxId = it } + } else { + return@flow + } + } + } + + fun getMediaLiker(mediaId: String): Flow = flow { + api.getMediaLiker(mediaId) + api.lastJSON?.read("$.users")?.forEach { emit(it as JSONObject) } + } + + fun getCommentLiker(commentId: String): Flow = flow { + api.getCommentLiker(commentId) + api.lastJSON?.read("$.users")?.forEach { emit(it as JSONObject) } + } + + @ExperimentalCoroutinesApi + suspend fun getUserLiker(userId: String, mediaAmount: Int = 5): List { + val userLiker = mutableListOf() + getLastUserMedias(userId, mediaAmount).collect { it -> + getMediaLiker(it.get("pk").toString()).collect { + userLiker.add(it) + } + } + + return userLiker.distinctBy { it.get("username") } + } + + + fun getMediaIdFromLink(mediaLink: String): String? { + if (!mediaLink.contains("instagram.com/p/")) { + return null + } + + var result = 0L + val alphabet = mapOf( + 'A' to 0, 'B' to 1, 'C' to 2, 'D' to 3, 'E' to 4, 'F' to 5, + 'G' to 6, 'H' to 7, 'I' to 8, 'J' to 9, 'K' to 10, 'L' to 11, 'M' to 12, 'N' to 13, 'O' to 14, + 'P' to 15, 'Q' to 16, 'R' to 17, 'S' to 18, 'T' to 19, 'U' to 20, 'V' to 21, 'W' to 22, 'X' to 23, + 'Y' to 24, 'Z' to 25, 'a' to 26, 'b' to 27, 'c' to 28, 'd' to 29, 'e' to 30, 'f' to 31, 'g' to 32, + 'h' to 33, 'i' to 34, 'j' to 35, 'k' to 36, 'l' to 37, 'm' to 38, 'n' to 39, 'o' to 40, 'p' to 41, + 'q' to 42, 'r' to 43, 's' to 44, 't' to 45, 'u' to 46, 'v' to 47, 'w' to 48, 'x' to 49, 'y' to 50, + 'z' to 51, '0' to 52, '1' to 53, '2' to 54, '3' to 55, '4' to 56, '5' to 57, '6' to 58, '7' to 59, + '8' to 60, '9' to 61, '-' to 62, '-' to 63 + ) + + val link = mediaLink.split("/") + val code = link.subList(link.indexOf("p") + 1, link.size - 1).toString().removePrefix("[").removeSuffix("]") + code.forEach { result = (result * 64) + alphabet.getValue(it) } + return result.toString() + } + + fun getLinkFromMediaId(mediaId: String): String { + val alphabet = mapOf( + 'A' to 0, 'B' to 1, 'C' to 2, 'D' to 3, 'E' to 4, 'F' to 5, + 'G' to 6, 'H' to 7, 'I' to 8, 'J' to 9, 'K' to 10, 'L' to 11, 'M' to 12, 'N' to 13, 'O' to 14, + 'P' to 15, 'Q' to 16, 'R' to 17, 'S' to 18, 'T' to 19, 'U' to 20, 'V' to 21, 'W' to 22, 'X' to 23, + 'Y' to 24, 'Z' to 25, 'a' to 26, 'b' to 27, 'c' to 28, 'd' to 29, 'e' to 30, 'f' to 31, 'g' to 32, + 'h' to 33, 'i' to 34, 'j' to 35, 'k' to 36, 'l' to 37, 'm' to 38, 'n' to 39, 'o' to 40, 'p' to 41, + 'q' to 42, 'r' to 43, 's' to 44, 't' to 45, 'u' to 46, 'v' to 47, 'w' to 48, 'x' to 49, 'y' to 50, + 'z' to 51, '0' to 52, '1' to 53, '2' to 54, '3' to 55, '4' to 56, '5' to 57, '6' to 58, '7' to 59, + '8' to 60, '9' to 61, '-' to 62, '-' to 63 + ) + + var id = mediaId.toLong() + var result = "" + while (id > 0) { + val char = (id % 64).toInt() + id /= 64 + result += alphabet.filterValues { it == char }.keys.first() + } + return "https://instagram.com/p/${result.reversed()}/" + } + + fun getInbox(): Flow = flow { + api.getInboxV2() + api.lastJSON?.read("$.inbox")?.read("$.threads")?.forEach { + emit(it as JSONObject) + } + } + + fun searchUsers(username: String): Flow = flow { + api.searchUsers(username) + api.lastJSON?.read("$.users")?.forEach { emit(it as JSONObject) } + } + + fun getMutedUsers(): Flow = flow { + api.getMutedUsers(mutedContentType = "stories") + api.lastJSON?.read("$.users")?.forEach { emit(it as JSONObject) } + } + + fun getPendingInbox(): Flow = flow { + api.getPendingInbox() + api.lastJSON?.read("$.inbox")?.read("$.threads")?.forEach { + emit(it as JSONObject) + } + } + + suspend fun like( + mediaId: String, containerModule: String = "feed_short_url", + feedPosition: Int = 0, username: String = "", userId: String = "", + hashTagName: String = "", hashTagId: String = "", entityPageName: String = "", entityPageId: String = "" + ): Boolean { + + if (!reachedLimit("likes")) { + if (blockedActions["likes"] == true) { + println("Your Like action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping like action") + } + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + api.like( + mediaId = mediaId, containerModule = containerModule, feedPosition = feedPosition, + username = username, userId = userId, hashTagName = hashTagName, + hashTagId = hashTagId, entityPageName = entityPageName, entityPageId = entityPageId + ) + + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Like action is blocked") + if (!blockedActionSleep) { + if (blockedActionProtection) { + blockedActions["likes"] = true + } + } else { + if (sleepingActions["likes"] == true && blockedActionProtection) { + println("This is the second blocked like action. \nActivating blocked protection for like action") + sleepingActions["likes"] = false + blockedActions["likes"] = true + } else { + println("Like action is going to sleep for $blockedActionSleepDelay seconds") + sleepingActions["likes"] = true + delay(blockedActionSleepDelay * 1000L) + } + } + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Liked media - $mediaId") + totalActionPerformed["likes"] = totalActionPerformed["likes"]!!.plus(1) + if (blockedActionSleep && sleepingActions["likes"] == true) { + sleepingActions["likes"] = false + } + return true + } + } + + println("out of likes for today") + return false + } + + fun likeMedias( + mediaIds: List, username: String = "", userId: String = "", + hashTagName: String = "", hashTagId: String = "" + ): + Flow = flow { + + var feedPosition = 0 + mediaIds.forEach { + if (reachedLimit("likes")) { + println("out of likes for today") + return@flow + } + + if (like( + mediaId = it, + feedPosition = feedPosition, + username = username, + userId = userId, + hashTagName = hashTagName, + hashTagId = hashTagId + ) + ) { + emit(it) + } else { + delay(10 * 1000L) + } + feedPosition += 1 + } + } + + + suspend fun likeComment(commentId: String): Boolean { + if (!reachedLimit("likes")) { + if (blockedActions.get("likes") == true) { + println("Your Like action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping like action") + } + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + api.likeComment(commentId) + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Like action is blocked") + blockedActions["likes"] = true + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Liked comment - $commentId") + totalActionPerformed["likes"] = totalActionPerformed["likes"]!!.plus(1) + if (blockedActionSleep && sleepingActions["likes"] == true) { + sleepingActions["likes"] = false + } + return true + } + } + + println("out of likes for today") + return false + } + + suspend fun likeTimelineMedias(amount: Int = 5): Flow { + val mediaIds = mutableListOf() + getTimelineMedias(amount).toList().forEach { + mediaIds.add(it?.read("$.pk").toString()) + } + return likeMedias(mediaIds = mediaIds) + } + + suspend fun likeMediaComments(mediaId: String, amount: Int = 5): Flow = flow { + getMediaComments(mediaId, amount).toList().forEach { + if (!it?.read("has_liked_comment")!!) { + val commentId = it.read("$.pk").toString() + if (likeComment(commentId)) { + emit(commentId) + } else { + delay(10 * 1000L) + } + } + } + } + + @ExperimentalCoroutinesApi + suspend fun likeUserMedias(userId: String, amount: Int = 5): Flow { + val mediaIds = mutableListOf() + getLastUserMedias(convertToUserId(userId), amount).toList().forEach { + mediaIds.add(it.read("$.pk").toString()) + } + return likeMedias(mediaIds = mediaIds) + } + + suspend fun likeExploreTabMedias(amount: Int): Flow { + val mediaIds = mutableListOf() + getExploreTabMedias(amount).toList().forEach { + mediaIds.add(it.get("pk").toString()) + } + return likeMedias(mediaIds = mediaIds) + } + + suspend fun likeHashTagMedias(hashTag: String, amount: Int = 5): Flow { + val mediaIds = mutableListOf() + getHashTagMedias(hashTag, amount).toList().forEach { + mediaIds.add(it.read("$.pk").toString()) + } + return likeMedias(mediaIds = mediaIds) + } + + suspend fun likeLocationMedias(locationName: String, amount: Int = 5): Flow { + val mediaIds = mutableListOf() + getMediasByLocation(locationName, amount).toList().forEach { + mediaIds.add(it.read("$.pk").toString()) + } + + return likeMedias(mediaIds = mediaIds) + } + + @ExperimentalCoroutinesApi + suspend fun likeUserFollowers(userId: String, amountOfFollowers: Int = 1, amountOfMedias: Int = 1): Flow { + val mediaIds = mutableListOf() + val followers = getUserFollowers(userId, amountOfFollowers).toList() + followers.forEach { it -> + val medias = getLastUserMedias(it, amountOfMedias).toList() + medias.forEach { + mediaIds.add(it.read("$.pk").toString()) + } + + } + return likeMedias(mediaIds) + } + + + @ExperimentalCoroutinesApi + suspend fun likeUserFollowing(userId: String, amountOfFollowing: Int = 1, amountOfMedias: Int = 1): Flow { + val mediaIds = mutableListOf() + val following = getUserFollowing(userId, amountOfFollowing).toList() + following.forEach { it -> + val medias = getLastUserMedias(it, amountOfMedias).toList() + medias.forEach { + mediaIds.add(it.read("$.pk").toString()) + } + + } + return likeMedias(mediaIds) + } + + suspend fun unlike(mediaId: String): Boolean { + if (!reachedLimit("unlikes")) { + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.unlike(mediaId)) { + totalActionPerformed["unlikes"] = totalActionPerformed["unlikes"]!!.plus(1) + return true + } + } + + println("out of unlikes for today") + return false + } + + fun unlikeComment(commentId: String): Boolean { + return api.unlikeComment(commentId) + } + + suspend fun unlikeMediaComments(mediaId: String): Flow = flow { + getMediaComments(mediaId, 10).toList().forEach { + if (it.read("has_liked_comment")!!) { + val commentId = it.read("$.pk").toString() + if (unlikeComment(commentId)) { + emit(commentId) + } else { + delay(10 * 1000L) + } + } + } + } + + fun unlikeMedias(mediaIds: List): Flow = flow { + mediaIds.forEach { + if (unlike(mediaId = it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + @ExperimentalCoroutinesApi + suspend fun unlikeUserMedias(userId: String, amount: Int = 5): Flow { + val mediaIds = mutableListOf() + getLastUserMedias(convertToUserId(userId), amount).toList().forEach { + mediaIds.add(it.read("$.pk").toString()) + } + return unlikeMedias(mediaIds = mediaIds) + } + + suspend fun downloadMedia(url: String, username: String, folderName: String, fileName: String): Boolean { + return withContext(Dispatchers.IO) { + api.downloadMedia(url, username, folderName, fileName) + } + } + + fun downloadUserStories(userId: String): Flow = flow { + getUserStoriesURL(userId).collect { + val filename = it.split("/").last().split(".").first() + if (downloadMedia(it, userId, "stories", "$filename.jpg")) { + emit(filename) + } + } + } + + fun changePassword(newPassword: String): Boolean { + return api.changePassword(newPassword) + } + + suspend fun watchUsersStories(userIds: List): Boolean { + val unseenReels = mutableListOf() + getUsersStories(userIds.map { convertToUserId(it) }).collect { it -> + val lastReelSeenAt = if (it.has("seen")) it.read("$.seen")!! else 0 + it.read("$.items")?.forEach { + if ((it as JSONObject).read("$.taken_at")!! > lastReelSeenAt) { + unseenReels.add(it) + } + } + } + + println("Going to watch ${unseenReels.size} stories") + totalActionPerformed["stories_viewed"] = totalActionPerformed["stories_viewed"]!!.plus(unseenReels.size) + return api.watchReels(reels = unseenReels) + } + + + /* + It will return 3 values. + 1. URL of media + 2. Caption of media + 3. True if media is photo, false if video + */ + private fun getMediaURLAndDescription( + mediaId: String, + isSaveDescription: Boolean + ): Flow> = flow { + val mediaInfo = getMediaInfo(mediaId) + + val caption = if (isSaveDescription) mediaInfo?.read("$.caption")?.read("$.text")!! else "" + + when (mediaInfo?.read("$.media_type")) { + 1 -> { + val image = + mediaInfo.read("$.image_versions2")?.read("$.candidates") + ?.first() as JSONObject + val url = image.read("$.url")!! + emit(Triple(url, caption, true)) + return@flow + } + 2 -> { + val video = mediaInfo.read("$.video_versions")?.first() as JSONObject + val url = video.read("$.url")!! + emit(Triple(url, caption, false)) + return@flow + } + 8 -> { + mediaInfo.read("$.carousel_media")?.forEach { + when ((it as JSONObject).read("$.media_type")) { + 1 -> { + val image = + it.read("$.image_versions2")?.read("$.candidates") + ?.first() as JSONObject + val url = image.read("$.url")!! + emit(Triple(url, caption, true)) + } + 2 -> { + val video = it.read("$.video_versions")?.first() as JSONObject + val url = video.read("$.url")!! + emit(Triple(url, caption, false)) + } + } + } + } + } + } + + + @ExperimentalCoroutinesApi + suspend fun downloadUserMedias(userId: String, amount: Int, isSaveDescription: Boolean = false): Flow = + flow { + var needToSaveDescription = isSaveDescription + getLastUserMedias(userId, amount).collect { it -> + getMediaURLAndDescription(it.read("$.pk").toString(), isSaveDescription).collect { + val filename = it.first.split("/").last().split(".").first() + val folderName = if (it.third) "photos" else "videos" + val fileType = if (it.third) ".jpg" else ".mp4" + if (downloadMedia(it.first, userId, folderName, "$filename$fileType")) { + if (needToSaveDescription) { + File("$folderName/$userId", "$filename.txt").printWriter().use { out -> + out.print(it.second) + } + needToSaveDescription = false + } + emit("$filename$fileType") + } + } + needToSaveDescription = isSaveDescription + } + } + + suspend fun follow(userId: String): Boolean { + if (!reachedLimit("follows")) { + if (blockedActions["follows"] == true) { + println("Your Follow action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping follow action") + } + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + api.follow(convertToUserId(userId)) + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Follow action is blocked") + if (!blockedActionSleep) { + if (blockedActionProtection) { + blockedActions["follows"] = true + } + } else { + if (sleepingActions["follows"] == true && blockedActionProtection) { + println("This is the second blocked follow action. \nActivating blocked protection for follow action") + sleepingActions["follows"] = false + blockedActions["follows"] = true + } else { + println("Follow action is going to sleep for $blockedActionSleepDelay seconds") + sleepingActions["follows"] = true + delay(blockedActionSleepDelay * 1000L) + } + } + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Followed user - $userId") + totalActionPerformed["follows"] = totalActionPerformed["follows"]!!.plus(1) + if (blockedActionSleep && sleepingActions["follows"] == true) { + sleepingActions["follows"] = false + } + return true + } + } + + println("out of follows for today") + return false + } + + // Need to filter already followed and unfollowed users before performing action + fun followUsers(userIds: List): Flow = flow { + userIds.forEach { + if (reachedLimit("follows")) { + println("out of follows for today") + return@flow + } + + if (follow(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun followUserFollowers( + userId: String, amountOfFollowers: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + val followers = getUserFollowers( + userId, amountOfFollowers, isUsername, isFilterPrivate, + isFilterVerified, fileNameToWrite, isOverwrite + ).toList() + + return followUsers(followers) + } + + suspend fun followUserFollowing( + userId: String, amountOfFollowing: Int = Int.MAX_VALUE, isUsername: Boolean = false, + isFilterPrivate: Boolean = false, isFilterVerified: Boolean = false, fileNameToWrite: String = "", + isOverwrite: Boolean = false + ): Flow { + val following = getUserFollowing( + userId, amountOfFollowing, isUsername, isFilterPrivate, + isFilterVerified, fileNameToWrite, isOverwrite + ).toList() + + return followUsers(following) + } + + private suspend fun unfollow(userId: String): Boolean { + if (!reachedLimit("unfollows")) { + if (blockedActions["unfollows"] == true) { + println("Your Unfollow action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping unfollow action") + } + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + api.unfollow(convertToUserId(userId)) + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Unfollow action is blocked") + if (!blockedActionSleep) { + if (blockedActionProtection) { + blockedActions["unfollows"] = true + } + } else { + if (sleepingActions["unfollows"] == true && blockedActionProtection) { + println("This is the second blocked follow action. \nActivating blocked protection for unfollow action") + sleepingActions["unfollows"] = false + blockedActions["unfollows"] = true + } else { + println("Unfollow action is going to sleep for $blockedActionSleepDelay seconds") + sleepingActions["unfollows"] = true + delay(blockedActionSleepDelay * 1000L) + } + } + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Unfollowed user - $userId") + totalActionPerformed["unfollows"] = totalActionPerformed["unfollows"]!!.plus(1) + if (blockedActionSleep && sleepingActions["unfollows"] == true) { + sleepingActions["unfollows"] = false + } + return true + } + } + + println("out of unfollows for today") + return false + } + + fun unfollowUsers(userIds: List): Flow = flow { + userIds.forEach { + if (reachedLimit("unfollows")) { + println("out of unfollows for today") + return@flow + } + + if (unfollow(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun unfollowNonFollowers(): Flow { + val nonFollowers = getSelfFollowing().toSet().subtract(getSelfFollowers().toSet()).toList() + return unfollowUsers(nonFollowers) + } + + fun approvePendingFollowRequest(userId: String): Boolean { + return api.approvePendingFollowRequest(userId) + } + + fun rejectPendingFollowRequest(userId: String): Boolean { + return api.rejectPendingFollowRequest(userId) + } + + suspend fun approveAllPendingFollowRequests(): Flow = flow { + getPendingFollowRequests().collect { + if (approvePendingFollowRequest(it.read("$.pk").toString())) { + emit(it.read("$.username")!!) + } + } + } + + suspend fun rejectAllPendingFollowRequests(): Flow = flow { + getPendingFollowRequests().collect { + if (rejectPendingFollowRequest(it.read("$.pk").toString())) { + emit(it.read("$.username")!!) + } + } + } + + + private fun extractURL(text: String): String { + val pattern = + """((?:(?:http|https|Http|Https|rtsp|Rtsp)://(?:(?:[a-zA-Z0-9${'$'}\-\_\.\+\!\*\'\(\)\,\;\?\&\=]|(?:%[a-fA-F0-9]{2})){1,64}(?::(?:[a-zA-Z0-9${'$'}\-\_\.\+\!\*\'\(\)\,\;\?\&\=]|(?:%[a-fA-F0-9]{2})){1,25})?@)?)?(?:(?:(?:[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\_][a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\_\-]{0,64}\.)+(?:(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])|(?:biz|b[abdefghijmnorstvwyz])|(?:cat|com|coop|c[acdfghiklmnoruvxyz])|d[ejkmoz]|(?:edu|e[cegrstu])|f[ijkmor]|(?:gov|g[abdefghilmnpqrstuwy])|h[kmnrtu]|(?:info|int|i[delmnoqrst])|(?:jobs|j[emop])|k[eghimnprwyz]|l[abcikrstuvy]|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])|(?:name|net|n[acefgilopruz])|(?:org|om)|(?:pro|p[aefghklmnrstwy])|qa|r[eosuw]|s[abcdeghijklmnortuvyz]|(?:tel|travel|t[cdfghjklmnoprtvwz])|u[agksyz]|v[aceginu]|w[fs]|(?:\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE|\u0438\u0441\u043F\u044B\u0442\u0430\u043D\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05D8\u05E2\u05E1\u05D8|\u0622\u0632\u0645\u0627\u06CC\u0634\u06CC|\u0625\u062E\u062A\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062F\u0646|\u0627\u0644\u062C\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062F\u064A\u0629|\u0627\u0644\u0645\u063A\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062A|\u0628\u06BE\u0627\u0631\u062A|\u062A\u0648\u0646\u0633|\u0633\u0648\u0631\u064A\u0629|\u0641\u0644\u0633\u0637\u064A\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092A\u0930\u0940\u0915\u094D\u0937\u093E|\u092D\u093E\u0930\u0924|\u09AD\u09BE\u09B0\u09A4|\u0A2D\u0A3E\u0A30\u0A24|\u0AAD\u0ABE\u0AB0\u0AA4|\u0B87\u0BA8\u0BCD\u0BA4\u0BBF\u0BAF\u0BBE|\u0B87\u0BB2\u0B99\u0BCD\u0B95\u0BC8|\u0B9A\u0BBF\u0B99\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0BC2\u0BB0\u0BCD|\u0BAA\u0BB0\u0BBF\u0B9F\u0BCD\u0B9A\u0BC8|\u0C2D\u0C3E\u0C30\u0C24\u0C4D|\u0DBD\u0D82\u0D9A\u0DCF|\u0E44\u0E17\u0E22|\u30C6\u30B9\u30C8|\u4E2D\u56FD|\u4E2D\u570B|\u53F0\u6E7E|\u53F0\u7063|\u65B0\u52A0\u5761|\u6D4B\u8BD5|\u6E2C\u8A66|\u9999\u6E2F|\uD14C\uC2A4\uD2B8|\uD55C\uAD6D|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--3e0b707e|xn--45brj9c|xn--80akhbyknj4f|xn--90a3ac|xn--9t4b11yi5a|xn--clchc0ea0b2g2a9gcd|xn--deba0ad|xn--fiqs8s|xn--fiqz9s|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--g6w251d|xn--gecrj9c|xn--h2brj9c|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--j6w193g|xn--jxalpdlp|xn--kgbechtv|xn--kprw13d|xn--kpry57d|xn--lgbbat1ad8j|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbc0a9azcg|xn--mgberp4a5d4ar|xn--o3cw4h|xn--ogbpf8fl|xn--p1ai|xn--pgbs0dh|xn--s9brj9c|xn--wgbh1c|xn--wgbl6a|xn--xkc2al3hye2a|xn--xkc2dl3a5ee0h|xn--yfro4i67o|xn--ygbi2ammx|xn--zckzah|xxx)|y[et]|z[amw]))|(?:(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[0-9])))(?::\d{1,5})?(?:/(?:(?:[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\;\/\?\:\@\&\=\#\~\-\.\+\!\*\'\(\)\,\_])|(?:%[a-fA-F0-9]{2}))*)?)(?:\b|${'$'})""".trimMargin() + val matches = Regex(pattern).findAll(text) + return matches.map { it.groupValues[1] }.toList().map { "\"$it\"" }.toString() + } + + suspend fun sendMessage(userIds: List, text: String, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + val urls = extractURL(text) + val itemType = if (urls != "[]") "link" else "text" + + if (api.sendDirectItem( + itemType = itemType, users = userIds.map { convertToUserId(it) }, + options = mapOf("text" to text, "urls" to urls, "threadId" to threadId) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendMessagesToUsers(userIds: List, text: String): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendMessage(listOf(it), text)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun sendMedia(mediaId: String, userIds: List, text: String, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + val media = getMediaInfo(mediaId) + + val mediaTye = media?.read("$.media_type").toString() + val mediaID = media?.read("$.id").toString() + + if (api.sendDirectItem( + itemType = "media_share", users = userIds.map { convertToUserId(it) }, + options = mapOf( + "text" to text, "threadId" to threadId, + "media_type" to mediaTye, "media_id" to mediaID + ) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendMediasToUsers(mediaId: String, userIds: List, text: String): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendMedia(mediaId, listOf(it), text)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun sendHashTag(hashTag: String, userIds: List, text: String, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.sendDirectItem( + itemType = "hashtag", users = userIds.map { convertToUserId(it) }, + options = mapOf("text" to text, "threadId" to threadId, "hashtag" to hashTag) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendHashTagToUsers(hashTag: String, userIds: List, text: String): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendHashTag(hashTag, listOf(it), text)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun sendProfile(profileId: String, userIds: List, text: String, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.sendDirectItem( + itemType = "profile", users = userIds.map { convertToUserId(it) }, + options = mapOf("text" to text, "threadId" to threadId, "profile_user_id" to profileId) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendProfileToUsers(profileId: String, userIds: List, text: String): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendProfile(profileId, listOf(it), text)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun sendLike(userIds: List, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.sendDirectItem( + itemType = "like", users = userIds.map { convertToUserId(it) }, + options = mapOf("threadId" to threadId) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendLikeToUsers(userIds: List): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendLike(listOf(it))) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + // Not working + suspend fun sendPhoto(userIds: List, filePath: String, threadId: String = ""): Boolean { + if (reachedLimit("messages")) { + println("out of messages for today") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.sendDirectItem( + itemType = "photo", users = userIds.map { convertToUserId(it) }, + options = mapOf("filePath" to filePath, "threadId" to threadId) + ) + ) { + totalActionPerformed["messages"] = totalActionPerformed["messages"]!!.plus(1) + return true + } + + return false + } + + fun sendPhotoToUsers(userIds: List, filePath: String): Flow = flow { + userIds.forEach { + if (reachedLimit("messages")) { + println("out of messages for today") + return@flow + } + + if (sendPhoto(listOf(it), filePath)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + fun getPendingThreadRequests(): Flow { + return api.getPendingThreads() + } + + private fun approvePendingThreadRequest(threadId: String): Boolean { + return api.approvePendingThread(threadId) + } + + private fun hidePendingThreadRequest(threadId: String): Boolean { + return api.hidePendingThread(threadId) + } + + private fun rejectPendingThreadRequest(threadId: String): Boolean { + return api.rejectPendingThread(threadId) + } + + // Need to check what can be returned here + suspend fun approveAllPendingThreadRequests(): Flow = flow { + getPendingThreadRequests().collect { + if (approvePendingThreadRequest(it?.read("$.thread_id").toString())) { + emit(it?.read("$.thread_id").toString()) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun hideAllPendingThreadRequests(): Flow = flow { + getPendingThreadRequests().collect { + if (hidePendingThreadRequest(it?.read("$.thread_id").toString())) { + emit(it?.read("$.thread_id").toString()) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun rejectAllPendingThreadRequests(): Flow = flow { + getPendingThreadRequests().collect { + if (rejectPendingThreadRequest(it?.read("$.thread_id").toString())) { + emit(it?.read("$.thread_id").toString()) + } else { + delay(10 * 1000L) + } + } + } + + fun deleteMedia(mediaId: String): Boolean { + return api.deleteMedia(mediaId) + } + + fun deleteMedias(mediaIds: List): Flow = flow { + mediaIds.forEach { + if (deleteMedia(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + fun deleteComment(mediaId: String, commentId: String): Boolean { + return api.deleteComment(mediaId, commentId) + } + + private fun archive(mediaId: String, undo: Boolean = false): Boolean { + val media = getMediaInfo(mediaId) + val mediaType = media?.read("$.media_type")!! + if (api.archiveMedia(mediaId, mediaType, undo)) { + if (!undo) { + totalActionPerformed["archived"] = totalActionPerformed["archived"]!!.plus(1) + } else { + totalActionPerformed["unarchived"] = totalActionPerformed["unarchived"]!!.plus(1) + } + return true + } + + return false + } + + private fun archiveMedia(mediaId: String): Boolean { + return archive(mediaId, false) + } + + private fun unarchiveMedia(mediaId: String): Boolean { + return archive(mediaId, true) + } + + fun archiveMedias(mediaIds: List): Flow = flow { + mediaIds.forEach { + if (archiveMedia(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + fun unarchiveMedias(mediaIds: List): Flow = flow { + mediaIds.forEach { + if (unarchiveMedia(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun isMediaCommented(mediaId: String): Boolean { + return getMediaCommenter(mediaId, Int.MAX_VALUE).toList().map { it?.read("$.username") } + .contains(this.username) + } + + private suspend fun comment(mediaId: String, commentText: String): Boolean { + if (isMediaCommented(mediaId)) { + return true + } + + if (!reachedLimit("comments")) { + if (blockedActions["comments"] == true) { + println("Your Comment action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping comment action") + } + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + api.comment(mediaId = mediaId, commentText = commentText) + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Comment action is blocked") + if (!blockedActionSleep) { + if (blockedActionProtection) { + blockedActions["comments"] = true + } + } else { + if (sleepingActions["comments"] == true && blockedActionProtection) { + println("This is the second blocked like action. \nActivating blocked protection for comments action") + sleepingActions["comments"] = false + blockedActions["comments"] = true + } else { + println("Comment action is going to sleep for $blockedActionSleepDelay seconds") + sleepingActions["comments"] = true + delay(blockedActionSleepDelay * 1000L) + } + } + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Commented media - $mediaId") + totalActionPerformed["comments"] = totalActionPerformed["comments"]!!.plus(1) + if (blockedActionSleep && sleepingActions["comments"] == true) { + sleepingActions["comments"] = false + } + return true + } + } + + println("out of comments for today") + return false + } + + suspend fun replyToComment(mediaId: String, parentCommentId: String, commentText: String): Boolean { + if (!isMediaCommented(mediaId)) { + println("Media is not commented yet") + return false + } + + if (!reachedLimit("comments")) { + if (blockedActions["comments"] == true) { + println("Your Comment action is blocked") + if (blockedActionProtection) { + println("Blocked action protection active, Skipping comment action") + } + return false + } + + if (commentText[0] != '@') { + println( + "A reply must start with mention, so '@' be the first " + + "char, followed by username you're replying to" + ) + return false + } + if (commentText.split(" ")[0].removeRange(0, 1) == this.username) { + println("You can't reply to yourself") + return false + } + + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + api.replyToComment(mediaId = mediaId, parentCommentId = parentCommentId, commentText = commentText) + + if (api.lastJSON?.read("$.message") == "feedback_required") { + println("Comment action is blocked") + return false + } else if (api.lastJSON?.read("$.status") == "ok") { + println("Commented media - $mediaId") + totalActionPerformed["comments"] = totalActionPerformed["comments"]!!.plus(1) + return true + } + } + + println("out of comments for today") + return false + } + + fun commentMedias(mediaIds: List, commentText: String): Flow = flow { + mediaIds.forEach { + if (reachedLimit("comments")) { + println("out of comments for today") + return@flow + } + + if (comment(mediaId = it, commentText = commentText)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } + + suspend fun commentExploreTabMedias(commentText: String, amountOfMedias: Int): Flow { + val mediaIds = mutableListOf() + getExploreTabMedias(amountOfMedias).toList().forEach { + mediaIds.add(it.get("pk").toString()) + } + return commentMedias(mediaIds = mediaIds, commentText = commentText) + } + + suspend fun commentHashTagMedias(hashTag: String, commentText: String, amountOfMedias: Int = 5): Flow { + val mediaIds = mutableListOf() + getHashTagMedias(hashTag, amountOfMedias).toList().forEach { + mediaIds.add(it?.read("$.pk").toString()) + } + return commentMedias(mediaIds = mediaIds, commentText = commentText) + } + + @ExperimentalCoroutinesApi + suspend fun commentUserMedias(userId: String, commentText: String, amountOfMedias: Int = 5): Flow { + val mediaIds = mutableListOf() + getLastUserMedias(convertToUserId(userId), amountOfMedias).toList().forEach { + mediaIds.add(it?.read("$.pk").toString()) + } + return commentMedias(mediaIds = mediaIds, commentText = commentText) + } + + suspend fun commentLocationMedias( + locationName: String, + commentText: String, + amountOfMedias: Int = 5 + ): Flow { + val mediaIds = mutableListOf() + getMediasByLocation(locationName, amountOfMedias).toList().forEach { + mediaIds.add(it?.read("$.pk").toString()) + } + + return commentMedias(mediaIds = mediaIds, commentText = commentText) + } + + suspend fun commentTimelineMedias(commentText: String, amountOfMedias: Int = 5): Flow { + val mediaIds = mutableListOf() + getTimelineMedias(amountOfMedias).toList().forEach { + mediaIds.add(it?.read("$.pk").toString()) + + } + return commentMedias(mediaIds = mediaIds, commentText = commentText) + } + + private suspend fun block(userId: String): Boolean { + if (!reachedLimit("blocks")) { + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.block(userId)) { + totalActionPerformed["blocks"] = totalActionPerformed["blocks"]!!.plus(1) + return true + } + } + + println("out of blocks for today") + return false + } + + private suspend fun unblock(userId: String): Boolean { + if (!reachedLimit("unblocks")) { + val sleepTimeInMillis = Random.nextInt(minSleepTime, maxSleepTime) + println("Sleeping $sleepTimeInMillis seconds") + delay(sleepTimeInMillis * 1000L) + + if (api.unblock(userId)) { + totalActionPerformed["unblocks"] = totalActionPerformed["unblocks"]!!.plus(1) + return true + } + } + + println("out of unblocks for today") + return false + } + + fun blockUsers(userIds: List): Flow = flow { + userIds.forEach { + if (block(it)) { + emit(it) + } else { + delay(10 * 1000L) + return@flow + } + } + } + + fun unblockUsers(userIds: List): Flow = flow { + userIds.forEach { + if (unblock(it)) { + emit(it) + } else { + delay(10 * 1000L) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/JsonPath.kt b/src/main/kotlin/jsonpath/JsonPath.kt new file mode 100644 index 0000000..b6a06ad --- /dev/null +++ b/src/main/kotlin/jsonpath/JsonPath.kt @@ -0,0 +1,134 @@ +package com.nfeld.jsonpathlite + +import com.nfeld.jsonpathlite.cache.CacheProvider +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class JsonPath(path: String) { + + private val path: String + internal val tokens: List + + /** + * Trim given path string and compile it on initialization + */ + init { + this.path = path.trim() + + val cache = CacheProvider.getCache() + val cachedJsonPath = cache?.get(this.path) + if (cachedJsonPath != null) { + tokens = cachedJsonPath.tokens + } else { + tokens = PathCompiler.compile(this.path) + cache?.put(this.path, this) + } + } + + /** + * Read the value at path in given JSON string + * + * @return Given type if value in path exists, null otherwise + */ + fun readFromJson(jsonString: String): T? { + /* + We don't need to parse this string into own JsonResult wrapper as we don't need those convenience methods at this point. + Use org.json directly based on first character of given string. Also pass it to private readFromJson method directly to skip a stack frame + */ + val trimmedJson = jsonString.trim() + return when (trimmedJson.firstOrNull()) { + '{' -> _readFromJson(JSONObject(trimmedJson)) + '[' -> _readFromJson(JSONArray(trimmedJson)) + else -> null + } + } + + /** + * Read the value at path in given JSON Object + * + * @return Given type if value in path exists, null otherwise + */ + fun readFromJson(jsonObject: JSONObject): T? = _readFromJson(jsonObject) + + /** + * Read the value at path in given JSON Array + * + * @return Given type if value in path exists, null otherwise + */ + fun readFromJson(jsonArray: JSONArray): T? = _readFromJson(jsonArray) + + @Suppress("UNCHECKED_CAST") + private fun _readFromJson(json: Any): T? { + var valueAtPath: Any? = json + tokens.forEach { token -> + valueAtPath?.let { valueAtPath = token.read(it) } + } + val lastValue = valueAtPath + if (lastValue is JSONArray && containsOnlyPrimitives(lastValue)) { + valueAtPath = lastValue.toList().toList() // return immutable list + } else if (lastValue == JSONObject.NULL) { + return null + } + return valueAtPath as? T + } + + /** + * Check if a JSONArray contains only primitive values (in this case, non-JSONObject/JSONArray). + */ + private fun containsOnlyPrimitives(jsonArray: JSONArray) : Boolean { + val it = jsonArray.iterator() + if(!it.hasNext()) { + return false + } + while (it.hasNext()) { + val item = it.next() + if (item is JSONObject || item is JSONArray) { + return false + } + } + return true + } + +// private fun isSpecialChar(c: Char): Boolean { +// return c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || c == 'r' || c == 't' +// } + + companion object { + /** + * Parse JSON string and return successful [JsonResult] or throw [JSONException] on parsing error + * + * @param jsonString JSON string to parse + * @return instance of parsed [JsonResult] object + * @throws JSONException + */ + @Throws(JSONException::class) + @JvmStatic + fun parse(jsonString: String): JsonResult = when { + jsonString.isEmpty() -> throw JSONException("JSON string is empty") + jsonString.first() == '{' -> JsonObject(JSONObject(jsonString)) + else -> JsonArray(JSONArray(jsonString)) + } + + /** + * Parse JSON string and return successful [JsonResult] or null otherwise + * + * @param jsonString JSON string to parse + * @return instance of parsed [JsonResult] object or null + */ + @JvmStatic + fun parseOrNull(jsonString: String): JsonResult? { + return jsonString.firstOrNull()?.run { + try { + if (this == '{') { + JsonObject(JSONObject(jsonString)) + } else { + JsonArray(JSONArray(jsonString)) + } + } catch (e: JSONException) { + null + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/JsonResult.kt b/src/main/kotlin/jsonpath/JsonResult.kt new file mode 100644 index 0000000..8e8ff1e --- /dev/null +++ b/src/main/kotlin/jsonpath/JsonResult.kt @@ -0,0 +1,16 @@ +package com.nfeld.jsonpathlite + +import org.json.JSONArray +import org.json.JSONObject + +data class JsonObject(val underlying: JSONObject) : JsonResult() { + override fun read(path: String): T? = JsonPath(path).readFromJson(underlying) +} + +data class JsonArray(val underlying: JSONArray): JsonResult() { + override fun read(path: String): T? = JsonPath(path).readFromJson(underlying) +} + +sealed class JsonResult { + abstract fun read(path: String): T? +} diff --git a/src/main/kotlin/jsonpath/PathCompiler.kt b/src/main/kotlin/jsonpath/PathCompiler.kt new file mode 100644 index 0000000..a7bcb98 --- /dev/null +++ b/src/main/kotlin/jsonpath/PathCompiler.kt @@ -0,0 +1,254 @@ +package com.nfeld.jsonpathlite + +internal object PathCompiler { + + /** + * @param path Path string to compile + * @return List of [Token] to read against a JSON + */ + @Throws(IllegalArgumentException::class) + internal fun compile(path: String): List { + if (path.firstOrNull() != '$') { + throw IllegalArgumentException("First character in path must be '$' root token") + } + + val tokens = mutableListOf() + var isDeepScan = false + val keyBuilder = StringBuilder() + + fun resetForNextToken() { + isDeepScan = false + keyBuilder.clear() + } + + fun addObjectAccessorToken() { + val key = keyBuilder.toString() + if (isDeepScan) { + tokens.add(DeepScanObjectAccessorToken(listOf(key))) + } else { + tokens.add(ObjectAccessorToken(key)) + } + } + + val len = path.length + var i = 1 + while (i < len) { + val c = path[i] + val next = path.getOrNull(i + 1) + when { + c == '.' -> { + if (keyBuilder.isNotEmpty()) { + addObjectAccessorToken() + resetForNextToken() + } + // check if it's followed by another dot. This means the following key will be used in deep scan + if (next == '.') { + isDeepScan = true + ++i + } else if (next == null) { + throw IllegalArgumentException("Unexpected ending with dot") + } + } + c == '[' -> { + if (keyBuilder.isNotEmpty()) { + addObjectAccessorToken() + resetForNextToken() + } + val closingBracketIndex = findMatchingClosingBracket(path, i) + if (closingBracketIndex > i + 1) { // i+1 checks to make sure atleast one char in the brackets + val token = compileBracket(path, i, closingBracketIndex) + if (isDeepScan) { + val deepScanToken: Token? = when (token) { + is ObjectAccessorToken -> DeepScanObjectAccessorToken(listOf(token.key)) + is MultiObjectAccessorToken -> DeepScanObjectAccessorToken(token.keys) + is ArrayAccessorToken -> DeepScanArrayAccessorToken(listOf(token.index)) + is MultiArrayAccessorToken -> DeepScanArrayAccessorToken(token.indices) + is ArrayLengthBasedRangeAccessorToken -> DeepScanLengthBasedArrayAccessorToken(token.startIndex, token.endIndex, token.offsetFromEnd) + else -> null + } + deepScanToken?.let { tokens.add(it) } + resetForNextToken() + } else { + tokens.add(token) + } + i = closingBracketIndex + } else { + throw IllegalArgumentException("Expecting closing array bracket with a value inside") + } + } + else -> keyBuilder.append(c) + } + ++i + } + + if (keyBuilder.isNotEmpty()) { + addObjectAccessorToken() + } + + return tokens.toList() + } + + /** + * @param path original path + * @param openingIndex opening bracket index we are to search matching closing bracket for + * @return closing bracket index, or -1 if not found + */ + internal fun findMatchingClosingBracket(path: String, openingIndex: Int): Int { + var expectingClosingQuote = false + var i = openingIndex + 1 + val len = path.length + + while (i < len) { + val c = path[i] + val next = path.getOrNull(i + 1) + when { + c == '\'' -> expectingClosingQuote = !expectingClosingQuote + c == ']' && !expectingClosingQuote -> return i + c == '\\' && expectingClosingQuote -> { + if (next == '\'') { + ++i // skip this char so we don't process escaped quote + } else if (next == null) { + throw IllegalArgumentException("Unexpected char at end of path") + } + } + } + ++i + } + + return -1 + } + + /** + * Compile path expression inside of brackets + * + * @param path original path + * @param openingIndex index of opening bracket + * @param closingIndex index of closing bracket + * @return Compiled [Token] + */ + internal fun compileBracket(path: String, openingIndex: Int, closingIndex: Int): Token { + var isObjectAccessor = false + var isNegativeArrayAccessor = false // supplements isArrayAccessor + var expectingClosingQuote = false + var hasStartColon = false // found colon in beginning + var hasEndColon = false // found colon in end + var isRange = false // has starting and ending range. There will be two keys containing indices of each + + var i = openingIndex + 1 + val keys = mutableListOf() + val keyBuilder = StringBuilder() + + fun buildAndAddKey() { + var key = keyBuilder.toString() + if (!isObjectAccessor && isNegativeArrayAccessor) { + key = "-$key" + isNegativeArrayAccessor = false + } + keys.add(key) + keyBuilder.clear() + } + + //TODO handle escaped chars + while (i < closingIndex) { + val c = path[i] + + when { + c == ' ' && !expectingClosingQuote -> { + // skip empty space that's not enclosed in quotes + } + + c == ':' && !expectingClosingQuote -> { + if (openingIndex == i - 1) { + hasStartColon = true + } else if (i == closingIndex - 1) { + hasEndColon = true + // keybuilder should have a key... + buildAndAddKey() + } else if (keyBuilder.isNotEmpty()) { + buildAndAddKey() // becomes starting index of range + isRange = true + } + } + + c == '-' && !isObjectAccessor -> { + isNegativeArrayAccessor = true + } + + c == ',' && !expectingClosingQuote -> { + // object accessor would have added key on closing quote + if (!isObjectAccessor && keyBuilder.isNotEmpty()) { + buildAndAddKey() + } + } + + c == '\'' && expectingClosingQuote -> { // only valid inside array bracket and ending + if (keyBuilder.isEmpty()) { + throw IllegalArgumentException("Key is empty string") + } + buildAndAddKey() + expectingClosingQuote = false + } + + c == '\'' -> { + expectingClosingQuote = true + isObjectAccessor = true + } + + c.isDigit() || isObjectAccessor -> keyBuilder.append(c) + else -> throw IllegalArgumentException("Unexpected char, char=$c, index=$i") + } + + ++i + } + + if (keyBuilder.isNotEmpty()) { + buildAndAddKey() + } + + var token: Token? = null + if (isObjectAccessor) { + if (keys.size > 1) { + token = MultiObjectAccessorToken(keys) + } else { + keys.firstOrNull()?.let { + token = ObjectAccessorToken(it) + } + } + } else { + when { + isRange -> { + val start = keys[0].toInt(10) + val end = keys[1].toInt(10) // exclusive + val isEndNegative = end < 0 + token = if (start < 0 || isEndNegative) { + val offsetFromEnd = if (isEndNegative) end else 0 + val endIndex = if (!isEndNegative) end else null + ArrayLengthBasedRangeAccessorToken(start, endIndex, offsetFromEnd) + } else { + MultiArrayAccessorToken(IntRange(start, end - 1).toList()) + } + } + hasStartColon -> { + val end = keys[0].toInt(10) // exclusive + token = if (end < 0) { + ArrayLengthBasedRangeAccessorToken(0, null, end) + } else { + MultiArrayAccessorToken(IntRange(0, end - 1).toList()) + } + } + hasEndColon -> { + val start = keys[0].toInt(10) + token = ArrayLengthBasedRangeAccessorToken(start) + } + keys.size == 1 -> token = ArrayAccessorToken(keys[0].toInt(10)) + keys.size > 1 -> token = MultiArrayAccessorToken(keys.map { it.toInt(10) }) + } + } + + token?.let { + return it + } + + throw IllegalArgumentException("Not a valid path") + } +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/Token.kt b/src/main/kotlin/jsonpath/Token.kt new file mode 100644 index 0000000..76ec06b --- /dev/null +++ b/src/main/kotlin/jsonpath/Token.kt @@ -0,0 +1,275 @@ +package com.nfeld.jsonpathlite + +import org.json.JSONArray +import org.json.JSONObject + +/** + * Accesses value at [index] from [JSONArray] + * + * @param index index to access, can be negative which means to access from end + */ +internal data class ArrayAccessorToken(val index: Int) : Token { + override fun read(json: Any): Any? { + if (json is JSONArray) { + if (index < 0) { + // optimized to get array length only if we're accessing from last + val indexFromLast = json.length() + index + if (indexFromLast >= 0) { + return json.opt(indexFromLast) + } + } + return json.opt(index) + } + return null + } +} + +/** + * Accesses values at [indices] from [JSONArray]. When read, value returned will be [JSONArray] of values + * at requested indices in given order. + * + * @param indices indices to access, can be negative which means to access from end + */ +internal data class MultiArrayAccessorToken(val indices: List) : Token { + override fun read(json: Any): Any? { + val result = JSONArray() + + if (json is JSONArray) { + val jsonLength = json.length() + indices.forEach { index -> + if (index < 0) { + val indexFromLast = jsonLength + index + if (indexFromLast >= 0) { + json.opt(indexFromLast)?.let { result.put(it) } + } + } else { + json.opt(index)?.let { result.put(it) } + } + } + return result + } + return null + } +} + +/** + * Accesses values from [JSONArray] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. + * When read, value returned will be JSONArray of values at requested indices in order of values in range. + * + * @param startIndex starting index of range, inclusive. Can be negative. + * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] + * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] + */ +internal data class ArrayLengthBasedRangeAccessorToken(val startIndex: Int, + val endIndex: Int? = null, + val offsetFromEnd: Int = 0) : Token { + override fun read(json: Any): Any? { + val token = if (json is JSONArray) { + toMultiArrayAccessorToken(json) + } else null + return token?.read(json) + } + + fun toMultiArrayAccessorToken(json: JSONArray): MultiArrayAccessorToken? { + val len = json.length() + val start = if (startIndex < 0) { + len + startIndex + } else startIndex + + // use endIndex if we have it, otherwise calculate from json array length + val endInclusive = if (endIndex != null) { + endIndex - 1 + } else len + offsetFromEnd - 1 + + if (start >= 0 && endInclusive >= start) { + return MultiArrayAccessorToken(IntRange(start, endInclusive).toList()) + } + return MultiArrayAccessorToken(emptyList()) + } +} + +/** + * Accesses value at [key] from [JSONObject] + * + * @param index index to access, can be negative which means to access from end + */ +internal data class ObjectAccessorToken(val key: String) : Token { + override fun read(json: Any): Any? { + return if (json is JSONObject) { + json.opt(key) + } else null + } +} + +/** + * Accesses values at [keys] from [JSONObject]. When read, value returned will be [JSONObject] + * containing key/value pairs requested. Keys that are null or don't exist won't be added in Object + * + * @param keys keys to access for which key/values to return + */ +internal data class MultiObjectAccessorToken(val keys: List) : Token { + override fun read(json: Any): Any? { + val result = JSONObject() + + return if (json is JSONObject) { + keys.forEach { key -> + json.opt(key)?.let { + result.put(key, it) + } + } + result + } else null + } +} + +/** + * Recursive scan for values with keys in [targetKeys] list. Returns a [JSONArray] containing values found. + * + * @param targetKeys keys to find values for + */ +internal data class DeepScanObjectAccessorToken(val targetKeys: List) : Token { + private fun scan(jsonValue: Any, result: JSONArray) { + when (jsonValue) { + is JSONObject -> { + // first add all values from keys requested to result + if (targetKeys.size > 1) { + val resultToAdd = JSONObject() + targetKeys.forEach { targetKey -> + jsonValue.opt(targetKey)?.let { resultToAdd.put(targetKey, it) } + } + if (!resultToAdd.isEmpty) { + result.put(resultToAdd) + } + } else { + targetKeys.firstOrNull()?.let { key -> + jsonValue.opt(key)?.let { result.put(it) } + } + } + + // recursively scan all underlying objects/arrays + jsonValue.keySet().forEach { objKey -> + val objValue = jsonValue.opt(objKey) + if (objValue is JSONObject || objValue is JSONArray) { + scan(objValue, result) + } + } + } + is JSONArray -> { + jsonValue.forEach { + if (it is JSONObject || it is JSONArray) { + scan(it, result) + } + } + } + else -> {} + } + } + + override fun read(json: Any): Any? { + val result = JSONArray() + scan(json, result) + return result + } +} + +/** + * Recursive scan for values/objects/arrays found for all [indices] specified. Returns a [JSONArray] containing results found. + * + * @param indices indices to retrieve values/objects for + */ +internal data class DeepScanArrayAccessorToken(val indices: List) : Token { + private fun scan(jsonValue: Any, result: JSONArray) { + when (jsonValue) { + is JSONObject -> { + // traverse all key/value pairs and recursively scan underlying objects/arrays + jsonValue.keySet().forEach { objKey -> + val objValue = jsonValue.opt(objKey) + if (objValue is JSONObject || objValue is JSONArray) { + scan(objValue, result) + } + } + } + is JSONArray -> { + // first add all requested indices to our results + indices.forEach { index -> + ArrayAccessorToken(index).read(jsonValue)?.let { result.put(it) } + } + + // now recursively scan underlying objects/arrays + jsonValue.forEach { + if (it is JSONObject || it is JSONArray) { + scan(it, result) + } + } + } + else -> {} + } + } + + override fun read(json: Any): Any? { + val result = JSONArray() + scan(json, result) + return result + } +} + + +/** + * Recursive scan for values/objects/arrays from [JSONArray] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. + * When read, value returned will be JSONArray of values at requested indices in order of values in range. Returns a [JSONArray] containing results found. + * + * @param startIndex starting index of range, inclusive. Can be negative. + * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] + * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] + */ +internal data class DeepScanLengthBasedArrayAccessorToken(val startIndex: Int, + val endIndex: Int? = null, + val offsetFromEnd: Int = 0) : Token { + private fun scan(jsonValue: Any, result: JSONArray) { + when (jsonValue) { + is JSONObject -> { + // traverse all key/value pairs and recursively scan underlying objects/arrays + jsonValue.keySet().forEach { objKey -> + val objValue = jsonValue.opt(objKey) + if (objValue is JSONObject || objValue is JSONArray) { + scan(objValue, result) + } + } + } + is JSONArray -> { + ArrayLengthBasedRangeAccessorToken(startIndex, endIndex, offsetFromEnd) + .toMultiArrayAccessorToken(jsonValue) + ?.read(jsonValue) + ?.let { resultAny -> + val resultArray = resultAny as? JSONArray + resultArray?.forEach { result.put(it) } + } + + // now recursively scan underlying objects/arrays + jsonValue.forEach { + if (it is JSONObject || it is JSONArray) { + scan(it, result) + } + } + } + else -> {} + } + } + + override fun read(json: Any): Any? { + val result = JSONArray() + scan(json, result) + return result + } +} + +internal interface Token { + /** + * Takes in JSONObject/JSONArray and outputs next JSONObject/JSONArray or value by evaluating token against current object/array in path + * Unfortunately needs to be done with Any since [org.json.JSONObject] and [org.json.JSONArray] do not implement a common interface :( + * + * @param json [JSONObject] or [JSONArray] + * @return [JSONObject], [JSONArray], or any JSON primitive value + */ + fun read(json: Any): Any? +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/cache/Cache.kt b/src/main/kotlin/jsonpath/cache/Cache.kt new file mode 100644 index 0000000..9ce02b7 --- /dev/null +++ b/src/main/kotlin/jsonpath/cache/Cache.kt @@ -0,0 +1,21 @@ +package com.nfeld.jsonpathlite.cache + +import com.nfeld.jsonpathlite.JsonPath + +interface Cache { + /** + * Retrieve an instance of [JsonPath] containing the compiled path. + * + * @param path path string key for cache + * @return cached [JsonPath] instance or null if not cached + */ + fun get(path: String): JsonPath? + + /** + * Insert the given path and [JsonPath] as key/value pair into cache. + * + * @param path path string key for cache + * @param jsonPath instance of [JsonPath] containing compiled path + */ + fun put(path: String, jsonPath: JsonPath) +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/cache/CacheProvider.kt b/src/main/kotlin/jsonpath/cache/CacheProvider.kt new file mode 100644 index 0000000..c3b435b --- /dev/null +++ b/src/main/kotlin/jsonpath/cache/CacheProvider.kt @@ -0,0 +1,37 @@ +package com.nfeld.jsonpathlite.cache + +object CacheProvider { + + private var cache: Cache? = null + private var useDefault = true + + /** + * Consumer can set this to preferred max cache size. + */ + @JvmStatic + var maxCacheSize = 100 + + /** + * Set cache to custom implementation of [Cache]. + * + * @param newCache cache implementation to use, or null if no cache desired. + */ + @JvmStatic + fun setCache(newCache: Cache?) { + useDefault = false + cache = newCache + } + + internal fun getCache(): Cache? { + if (cache == null && useDefault) { + synchronized(this) { + if (cache == null) { + cache = createDefaultCache() + } + } + } + return cache + } + + private fun createDefaultCache(): Cache = LRUCache(maxCacheSize) +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/cache/LRUCache.kt b/src/main/kotlin/jsonpath/cache/LRUCache.kt new file mode 100644 index 0000000..1191a4c --- /dev/null +++ b/src/main/kotlin/jsonpath/cache/LRUCache.kt @@ -0,0 +1,29 @@ +package com.nfeld.jsonpathlite.cache + +import com.nfeld.jsonpathlite.JsonPath +import org.jetbrains.annotations.TestOnly +import java.util.* + +internal class LRUCache(private val maxCacheSize: Int): Cache { + private val map = LRUMap() + + @Synchronized + override fun get(path: String): JsonPath? = map.get(path) + + @Synchronized + override fun put(path: String, jsonPath: JsonPath) { + map.put(path, jsonPath) + } + + @TestOnly + internal fun toList(): List> = map.toList() + + private inner class LRUMap : LinkedHashMap(INITIAL_CAPACITY, LOAD_FACTOR, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxCacheSize + } + + companion object { + private const val INITIAL_CAPACITY = 16 + private const val LOAD_FACTOR = 0.75f + } +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/extension/JSONArray.kt b/src/main/kotlin/jsonpath/extension/JSONArray.kt new file mode 100644 index 0000000..580ad1a --- /dev/null +++ b/src/main/kotlin/jsonpath/extension/JSONArray.kt @@ -0,0 +1,12 @@ +package com.nfeld.jsonpathlite.extension + +import com.nfeld.jsonpathlite.JsonPath +import org.json.JSONArray + +fun JSONArray.read(jsonpath: String): T? { + return JsonPath(jsonpath).readFromJson(this) +} + +fun JSONArray.read(jsonpath: JsonPath): T? { + return jsonpath.readFromJson(this) +} \ No newline at end of file diff --git a/src/main/kotlin/jsonpath/extension/JSONObject.kt b/src/main/kotlin/jsonpath/extension/JSONObject.kt new file mode 100644 index 0000000..fe7961c --- /dev/null +++ b/src/main/kotlin/jsonpath/extension/JSONObject.kt @@ -0,0 +1,12 @@ +package com.nfeld.jsonpathlite.extension + +import com.nfeld.jsonpathlite.JsonPath +import org.json.JSONObject + +fun JSONObject.read(jsonpath: String): T? { + return JsonPath(jsonpath).readFromJson(this) +} + +fun JSONObject.read(jsonpath: JsonPath): T? { + return jsonpath.readFromJson(this) +} diff --git a/src/main/kotlin/util/Config.kt b/src/main/kotlin/util/Config.kt new file mode 100644 index 0000000..a1fc284 --- /dev/null +++ b/src/main/kotlin/util/Config.kt @@ -0,0 +1,286 @@ +package util + +import api.InstagramAPI + +/* +Configuration file of the project +*/ + +object KEY { + val SIG_KEY: String = "5f3e50f435583c9ae626302a71f7340044087a7e2c60adacfc254205a993e305" + val SIG_KEY_VERSION: String = "4" + val APP_VERSION: String = "105.0.0.18.119" +} + +object HTTP { + val HOST_NAME = "i.instagram.com" + val HOST = "https://$HOST_NAME/" + val API_URL = "${HOST}api/v1/" + + val HEADERS = mutableMapOf( + "User-Agent" to USER_AGENT, + "Connection" to "Keep-Alive", + "X-Pigeon-Session-Id" to Crypto.generateTemporaryGUID( + "pigeonSessionId", + InstagramAPI.uuid, + 1200000f + ), + "X-Pigeon-Rawclienttime" to "%.3f".format(System.currentTimeMillis() / 1000f), + "X-IG-Capabilities" to "IT7nCQ==", + "X-IG-App-ID" to "567067343352427", + "X-IG-Connection-Type" to "WIFI", + "X-IG-Prefetch-Request" to "foreground", + "X-IG-VP9-Capable" to "false", + "X-FB-HTTP-Engine" to "Liger", + "Accept" to "*/*", + "Accept-Encoding" to "gzip,deflate", + "Accept-Language" to "en-US", + "Content-type" to "application/x-www-form-urlencoded; charset=UTF-8", + "Cookie2" to "\$Version=1" + ) + +} + +val USER_AGENT: String = "Instagram ${one_plus_7.instagram_version} " + + "Android (${one_plus_7.android_version}/${one_plus_7.android_release}; " + + "${one_plus_7.dpi}; ${one_plus_7.resolution}; ${one_plus_7.manufacturer}; " + + "${one_plus_7.device}; ${one_plus_7.model}; ${one_plus_7.cpu}; en_US)" + + +object EXPERIMENTS { + const val EXPERIMENTS: String = + "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" + + const val LOGIN_EXPERIMENTS: String = + "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" + + const val LAUNCHER_CONFIGS: String = + "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" + + const val SURFACES_TO_TRIGGERS = + """{"5734":["instagram_feed_prompt"],"4715":["instagram_feed_header"],"5858":["instagram_feed_tool_tip"]}""" + + const val SURFACES_TO_QUERIES: String = + """{"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}}}}}}"}""" + + val SUPPORTED_CAPABILITIES: List> = listOf( + mapOf( + "name" to "SUPPORTED_SDK_VERSIONS", + "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" + ), + mapOf( + "name" to "FACE_TRACKER_VERSION", + "value" to "12" + ), + mapOf( + "name" to "segmentation", + "value" to "segmentation_enabled" + ), + mapOf( + "name" to "WORLD_TRACKER", + "value" to "WORLD_TRACKER_ENABLED" + ) + ) +} + + +// Routing endpoints +object Routes { + + fun msisdnHeader() = "accounts/read_msisdn_header/" + + fun logAttribution() = "attribution/log_attribution/" + + fun contactPointPrefill() = "accounts/contact_point_prefill/" + + fun login(): String = "accounts/login/" + + fun logout(): String = "accounts/logout/" + + fun reelsTrayFeed(): String = "feed/reels_tray/" + + fun suggestedSearches(): String = "fbsearch/suggested_searches/" + + fun rankedRecipients(): String = "direct_v2/ranked_recipients/" + + fun loomFetchConfig(): String = "loom/fetch_config/" + + fun profileNotice(): String = "users/profile_notice/" + + fun batchFetch(): String = "qp/batch_fetch/" + + fun twoFactorAuth(): String = "accounts/two_factor_login/" + + fun userTags(userId: String, rankToken: String): String = + "usertags/${userId}/feed/?rank_token=${rankToken}&ranked_content=true" + + fun geoMedia(userId: String): String = "maps/user/${userId}/" + + fun follow(userId: String): String = "friendships/create/${userId}/" + + fun unfollow(userId: String): String = "friendships/destroy/${userId}/" + + fun removeFollower(userId: String): String = "friendships/remove_follower/${userId}" + + fun expose(): String = "qe/expose/" + + fun explore(): String = "discover/explore/" + + fun saveMedia(mediaId: String): String = "media/${mediaId}/save/" + + fun unsaveMedia(mediaId: String): String = "media/${mediaId}/unsave/" + + fun getSavedMedia(): String = "feed/saved/" + + fun igtvSuggestions(): String = "igtv/tv_guide/" + + fun setAccountPrivate(): String = "accounts/set_private/" + + fun setAccountPublic(): String = "accounts/set_public/" + + fun editAccount(): String = "accounts/edit_profile/" + + fun profileData(): String = "accounts/current_user/?edit=true" + + fun setNameAndPhone(): String = "accounts/set_phone_and_name/" + + fun comment(mediaId: String): String = "media/${mediaId}/comment/" + + fun deleteComment(mediaId: String, commentId: String) = "media/${mediaId}/comment/${commentId}/delete/" + + fun commentLikers(commentId: String): String = "media/${commentId}/comment_likers/?" + + fun mediaLikers(mediaId: String): String = "media/${mediaId}/likers/?" + + fun likeComment(commentId: String): String = "media/${commentId}/comment_like/" + + fun unlikeComment(commentId: String): String = "media/${commentId}/comment_unlike/" + + fun like(mediaId: String): String = "media/${mediaId}/like/" + + fun unlike(mediaId: String): String = "media/${mediaId}/unlike/" + + fun userFriendship(userId: String): String = "friendships/show/${userId}/" + + fun userInfoById(userId: String): String = "users/${userId}/info/" + + fun userInfoByName(username: String): String = "users/${username}/usernameinfo/" + + fun userFeed(userId: String, maxId: String, minTimeStamp: String, rankToken: String): String = + "feed/user/${userId}/?max_id=${maxId}&min_timestamp=${minTimeStamp}&rank_token=${rankToken}&ranked_content=true" + + fun userStories(userId: String): String = "feed/user/${userId}/story/" + + fun timeline(maxId: String = ""): String = "feed/timeline/?max_id=${maxId}" + + fun hashTagFeed(hashTag: String, maxId: String = "", rankToken: String): String = + "feed/tag/${hashTag}/?max_id=${maxId}&rank_token=${rankToken}&ranked_content=true" + + fun likedFeed(maxId: String): String = "feed/liked/?max_id=${maxId}" + + fun locationFeed(locationId: String, maxId: String, rankToken: String): String = + "feed/location/${locationId}/?max_id=${maxId}&rank_token=${rankToken}&ranked_content=true" + + fun popularFeed(rankToken: String): String = + "feed/popular/?people_teaser_supported=1&rank_token=${rankToken}&ranked_content=true" + + fun userFollowings(userId: String, maxId: String = "", rankToken: String): String = + "friendships/${userId}/following/?max_id=${maxId}&ig_sig_key_version=${KEY.SIG_KEY_VERSION}&rank_token=${rankToken}" + + fun userFollowers(userId: String, maxId: String = "", rankToken: String): String = + "friendships/${userId}/followers/?max_id=${maxId}&ig_sig_key_version=${KEY.SIG_KEY_VERSION}&rank_token=${rankToken}" + + fun changePassword(): String = "accounts/change_password/" + + fun removeProfilePicture(): String = "accounts/remove_profile_picture/" + + fun searchUser(userName: String, rankToken: String): String = + "users/search/?ig_sig_key_version=${KEY.SIG_KEY_VERSION}&is_typehead=true&query=${userName}&rank_token=${rankToken}" + + fun searchHashTag(hashTagName: String, amount: Int, rankToken: String): String = + "tags/search/?count=${amount}&is_typeahead=true&q=${hashTagName}&rank_token=${rankToken}" + + fun hashTagStories(hashTag: String): String = "tags/${hashTag}/story/" + + fun hashTagSelection(hashTag: String): String = "tags/${hashTag}/sections/" + + fun mediaInsight(mediaId: String): String = + "insights/media_organic_insights/${mediaId}/?ig_sig_key_version=${KEY.SIG_KEY_VERSION}" + + fun selfInsight(): String = "insights/account_organic_insights/?show_promotions_in_landing_page=true&first=" + + fun followHashTag(hashTag: String): String = "tags/follow/${hashTag}/" + + fun unfollowHashTag(hashTag: String): String = "tags/unfollow/${hashTag}/" + + fun tagsFollowedByUser(userId: String): String = "users/${userId}/following_tags_info/" + + fun searchLocation(locationName: String, amount: Int, rankToken: String): String = + "fbsearch/places/?count=${amount}&query=${locationName}&rank_token=${rankToken}" + + fun mediaInfo(mediaId: String): String = "media/${mediaId}/info/" + + fun editMedia(mediaId: String): String = "media/${mediaId}/edit_media/" + + fun deleteMedia(mediaId: String): String = "media/${mediaId}/delete/" + + fun removeSelfTagFromMedia(mediaId: String): String = "media/${mediaId}/remove/" + + fun archiveMedia(mediaId: String, action: String, mediaType: Int): String = + "media/${mediaId}/${action}/?media_type=${mediaType}" + + fun mediaComments(mediaId: String, maxId: String = ""): String = "media/${mediaId}/comments/?max_id=${maxId}" + + fun qeSync(): String = "qe/sync/" + + fun launcherSync(): String = "launcher/sync/" + + fun inboxV2(): String = "direct_v2/inbox/?" + + fun presence(): String = "direct_v2/get_presence/" + + fun userReel(userId: String): String = "feed/user/${userId}/reel_media/" + + fun selfStoryViewers(storyId: String): String = + "media/${storyId}/list_reel_media_viewer/?supported_capabilities_new=${EXPERIMENTS.SUPPORTED_CAPABILITIES}" + + fun watchReels(): String = "media/seen/" + + fun multipleUsersReel(): String = "feed/reels_media/" + + fun pendingInbox(): String = "direct_v2/pending_inbox/?persistentBadging=true&use_unified_inbox=true" + + fun directItem(itemType: String): String = "direct_v2/threads/broadcast/${itemType}/" + + fun directPhoto(): String = "direct_v2/threads/broadcast/upload_photo/" + + fun approvePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/approve/" + + fun hidePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/hide/" + + fun declinePendingThread(threadId: String): String = "direct_v2/threads/${threadId}/decline/" + + fun autoCompleteUserList(): String = "friendships/autocomplete_user_list/?version=2&followinfo=True" + + fun megaphoneLog(): String = "megaphone/log/" + + fun block(userId: String): String = "friendships/block/${userId}/" + + fun unblock(userId: String): String = "friendships/unblock/${userId}/" + + fun recentActivity(): String = "news/inbox/" + + fun muteUser(): String = "friendships/mute_posts_or_story_from_follow/" + + fun getMutedUser(): String = "friendships/muted_reels" + + fun unmuteUser(): String = "friendships/unmute_posts_or_story_from_follow/" + + fun pendingFriendRequests(): String = "friendships/pending/" + + fun approvePendingFollowRequest(userId: String): String = "friendships/approve/${userId}/" + + fun rejectPendingFollowRequest(userId: String): String = "friendships/ignore/${userId}/" + + fun directShare(): String = "direct_share/inbox/" +} \ No newline at end of file diff --git a/src/main/kotlin/util/CookiePersistor.kt b/src/main/kotlin/util/CookiePersistor.kt new file mode 100644 index 0000000..200690b --- /dev/null +++ b/src/main/kotlin/util/CookiePersistor.kt @@ -0,0 +1,46 @@ +package util + +import khttp.structures.cookie.Cookie +import khttp.structures.cookie.CookieJar +import java.io.File + +class CookiePersistor(private val resource: String) { + // Check if persisted cookie is exists + fun exist(): Boolean { + return File(resource).exists() + } + + // Save account and cookies to storage + fun save(account: String, cookieJar: CookieJar) { + val cksList = arrayListOf() + cookieJar.entries.forEach { + cksList.add("$it") + } + val cookiesString = cksList.toList().joinToString("#") + File(resource).printWriter().use { out -> + out.print("account->$account\ncookies->$cookiesString") + } + } + + // Load cookies and account from storage + fun load(): CookieDisk { + val jar = CookieJar() + val split = File(resource).readText().split("\n") + val account = split[0].split("->")[1] + val cookiesString = split[1].split("->")[1].split("#") + cookiesString.forEach { it -> + val cks = Cookie(it) + jar.setCookie(cks) + } + return CookieDisk(account, jar) + } + + // Delete cookies + fun destroy() { + if (exist()) { + File(resource).delete() + } + } +} + +data class CookieDisk(val account: String, val cookieJar: CookieJar) diff --git a/src/main/kotlin/util/Crypto.kt b/src/main/kotlin/util/Crypto.kt new file mode 100644 index 0000000..7d931cc --- /dev/null +++ b/src/main/kotlin/util/Crypto.kt @@ -0,0 +1,70 @@ +package util + +import java.math.BigInteger +import java.util.* +import kotlin.math.roundToInt + +object Crypto { + + // Signature class + data class Signature(val signed: String, val appVersion: String, val sigKeyVersion: String, val payload: String) + + // Signature function + fun signData(payload: String): Signature { + val signed = generateHMAC(KEY.SIG_KEY, payload) + return Signature( + signed, + KEY.APP_VERSION, + KEY.SIG_KEY_VERSION, + payload + ) + } + + // Generate MD5 Hash of given string + private fun generateMD5(s: String): String { + return try { + val messageDigest = java.security.MessageDigest.getInstance("MD5") + messageDigest.update(s.toByteArray(), 0, s.length) + BigInteger(1, messageDigest.digest()).toString(16) + } catch (e: Exception) { + System.err.println("Error occurred while generating MD5 $e") + "" + } + } + + // Generate hash-based message authentication code of given data + fun generateHMAC(key: String, data: String): String { + return try { + val sha256HMAC = javax.crypto.Mac.getInstance("HmacSHA256") + val secretKey = javax.crypto.spec.SecretKeySpec(key.toByteArray(charset("UTF-8")), "HmacSHA256") + sha256HMAC.init(secretKey) + val bytes = sha256HMAC.doFinal(data.toByteArray(charset("UTF-8"))) + java.lang.String.format("%040x", BigInteger(1, bytes)) + } catch (e: Exception) { + System.err.println("Error occurred while generating HMAC $e") + "" + } + } + + // Random UUID Generator function + fun generateUUID(type: Boolean): String { + var uuid = UUID.randomUUID().toString() + if (!type) { + uuid = uuid.replace("-", "") + } + return uuid + } + + // Generate temporary GUID + fun generateTemporaryGUID(name: String, uuid: String, duration: Float): String { + return UUID.nameUUIDFromBytes("$name$uuid${(System.currentTimeMillis() / duration).roundToInt()}".toByteArray()) + .toString() + } + + // Generate Device ID + fun generateDeviceId(username: String): String { + val seed = 11111 + (Math.random() * ((99999 - 11111) + 1)) + val hash = generateMD5("$username$seed") + return "android-${hash.substring(0, 16)}" + } +} diff --git a/src/main/kotlin/util/Device.kt b/src/main/kotlin/util/Device.kt new file mode 100644 index 0000000..065f344 --- /dev/null +++ b/src/main/kotlin/util/Device.kt @@ -0,0 +1,6 @@ +package util + +data class Device(val instagram_version: String = "126.0.0.25.121", val android_version : Int = 29, + val android_release: String = "10.0", val dpi: String = "420dpi", + val resolution: String = "1080x2260", val manufacturer: String = "OnePlus", + val device: String = "GM1903", val model: String = "OnePlus7", val cpu: String = "qcom") \ No newline at end of file diff --git a/src/main/kotlin/util/LoginException.kt b/src/main/kotlin/util/LoginException.kt new file mode 100644 index 0000000..b1c341a --- /dev/null +++ b/src/main/kotlin/util/LoginException.kt @@ -0,0 +1,8 @@ +package util + +class LoginException(message: String): Exception() { + val msg = message + override fun toString(): String { + return msg + } +} \ No newline at end of file diff --git a/src/main/kotlin/util/devices.kt b/src/main/kotlin/util/devices.kt new file mode 100644 index 0000000..3170eff --- /dev/null +++ b/src/main/kotlin/util/devices.kt @@ -0,0 +1,16 @@ +package util + + +const val INSTAGRAM_VERSION: String = "105.0.0.18.119" + +// Released on August 2019 +val one_plus_7 = Device() + +// Released on February 2018 +val samsung_galaxy_s9_plus = Device( + INSTAGRAM_VERSION, 24, "7.0", "640dpi", + "1440x2560", "samsung", "SM-G965F", + "star2qltecs", "samsungexynos9810" +) + +val DEFAULT_DEVICE: Device = one_plus_7 \ No newline at end of file