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 0000000..87b738c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..65d2f34 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Dec 24 00:37:30 CST 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..af6708f --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + 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