package awais.instagrabber.webservices import android.util.Log import awais.instagrabber.repositories.GraphQLService import awais.instagrabber.repositories.responses.* import awais.instagrabber.utils.Constants import awais.instagrabber.utils.ResponseBodyUtils import awais.instagrabber.utils.extensions.TAG import org.json.JSONException import org.json.JSONObject import java.util.* open class GraphQLRepository(private val service: GraphQLService) { // TODO convert string response to a response class private suspend fun fetch( queryHash: String, variables: String, arg1: String, arg2: String, backup: User?, ): PostsFetchResponse { val queryMap = mapOf( "query_hash" to queryHash, "variables" to variables, ) val response = service.fetch(queryMap) return parsePostResponse(response, arg1, arg2, backup) } suspend fun fetchLocationPosts( locationId: Long, maxId: String?, ): PostsFetchResponse = fetch( "36bd0f2bf5911908de389b8ceaa3be6d", "{\"id\":\"" + locationId + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", Constants.EXTRAS_LOCATION, "edge_location_to_media", null ) suspend fun fetchHashtagPosts( tag: String, maxId: String?, ): PostsFetchResponse = fetch( "9b498c08113f1e09617a1703c22b2f32", "{\"tag_name\":\"" + tag + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", Constants.EXTRAS_HASHTAG, "edge_hashtag_to_media", null, ) suspend fun fetchProfilePosts( profileId: Long, postsPerPage: Int, maxId: String?, backup: User?, ): PostsFetchResponse = fetch( "02e14f6a7812a876f7d133c9555b1151", "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", Constants.EXTRAS_USER, "edge_owner_to_timeline_media", backup, ) suspend fun fetchTaggedPosts( profileId: Long, postsPerPage: Int, maxId: String?, ): PostsFetchResponse = fetch( "31fe64d9463cbbe58319dced405c6206", "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", Constants.EXTRAS_USER, "edge_user_to_photos_of_you", null, ) @Throws(JSONException::class) private fun parsePostResponse( response: String, arg1: String, arg2: String, backup: User?, ): PostsFetchResponse { if (response.isBlank()) { Log.e(TAG, "parseResponse: feed response body is empty") return PostsFetchResponse(emptyList(), false, null) } return parseResponseBody(response, arg1, arg2, backup) } @Throws(JSONException::class) private fun parseResponseBody( body: String, arg1: String, arg2: String, backup: User?, ): PostsFetchResponse { val items: MutableList = ArrayList() val timelineFeed = JSONObject(body) .getJSONObject("data") .getJSONObject(arg1) .getJSONObject(arg2) val endCursor: String? val hasNextPage: Boolean val pageInfo = timelineFeed.getJSONObject("page_info") if (pageInfo.has("has_next_page")) { hasNextPage = pageInfo.getBoolean("has_next_page") endCursor = if (hasNextPage) pageInfo.getString("end_cursor") else null } else { hasNextPage = false endCursor = null } val feedItems = timelineFeed.getJSONArray("edges") for (i in 0 until feedItems.length()) { val itemJson = feedItems.optJSONObject(i) ?: continue val media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup) if (media != null) { items.add(media) } } return PostsFetchResponse(items, hasNextPage, endCursor) } // TODO convert string response to a response class suspend fun fetchCommentLikers( commentId: String, endCursor: String?, ): GraphQLUserListFetchResponse { val queryMap = mapOf( "query_hash" to "5f0b1f6281e72053cbc07909c8d154ae", "variables" to "{\"comment_id\":\"" + commentId + "\"," + "\"first\":30," + "\"after\":\"" + (endCursor ?: "") + "\"}" ) val response = service.fetch(queryMap) val body = JSONObject(response) val status = body.getString("status") val data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by") val pageInfo = data.getJSONObject("page_info") val newEndCursor = if (pageInfo.getBoolean("has_next_page")) pageInfo.getString("end_cursor") else null val users = data.getJSONArray("edges") val usersLen = users.length() val userModels: MutableList = ArrayList() for (j in 0 until usersLen) { val userObject = users.getJSONObject(j).getJSONObject("node") userModels.add( User( userObject.getLong("id"), userObject.getString("username"), userObject.optString("full_name"), userObject.optBoolean("is_private"), userObject.getString("profile_pic_url"), userObject.optBoolean("is_verified") ) ) } return GraphQLUserListFetchResponse(newEndCursor, status, userModels) } suspend fun fetchComments( shortCodeOrCommentId: String?, root: Boolean, cursor: String?, ): String { val variables = mapOf( (if (root) "shortcode" else "comment_id") to shortCodeOrCommentId, "first" to 50, "after" to (cursor ?: "") ) val queryMap = mapOf( "query_hash" to if (root) "bc3296d1ce80a24b1b6e40b1e72903f5" else "51fdd02b67508306ad4484ff574a0b62", "variables" to JSONObject(variables).toString() ) return service.fetch(queryMap) } // TODO convert string response to a response class open suspend fun fetchUser( username: String, ): User? { val response = service.getUser(username) try { val body = JSONObject( response .split("").get(0) .trim().replace(Regex("\\};$"), "}") ) val userJson = body .getJSONObject("entry_data") .getJSONArray("ProfilePage") .getJSONObject(0) .getJSONObject("graphql") .getJSONObject(Constants.EXTRAS_USER) val isPrivate = userJson.getBoolean("is_private") val id = userJson.optLong(Constants.EXTRAS_ID, 0) val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media") // if (timelineMedia.has("edges")) { // final JSONArray edges = timelineMedia.getJSONArray("edges"); // } var url: String? = userJson.optString("external_url") if (url.isNullOrBlank()) url = null return User( id, username, userJson.getString("full_name"), isPrivate, userJson.getString("profile_pic_url_hd"), userJson.getBoolean("is_verified"), friendshipStatus = FriendshipStatus( userJson.optBoolean("followed_by_viewer"), userJson.optBoolean("follows_viewer"), userJson.optBoolean("blocked_by_viewer"), false, isPrivate, userJson.optBoolean("has_requested_viewer"), userJson.optBoolean("requested_by_viewer"), false, userJson.optBoolean("restricted_by_viewer"), false ), mediaCount = timelineMedia.getLong("count"), followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"), followingCount = userJson.getJSONObject("edge_follow").getLong("count"), biography = userJson.getString("biography"), externalUrl = url, ) } catch (e: Exception) { Log.e(TAG, "fetchUser failed", e) return null } } // TODO convert string response to a response class suspend fun fetchPost( shortcode: String, ): Media { val response = service.getPost(shortcode) val body = JSONObject(response) val media = body.getJSONObject("graphql").getJSONObject("shortcode_media") return ResponseBodyUtils.parseGraphQLItem(media, null) } // TODO convert string response to a response class suspend fun fetchTag( tag: String, ): Hashtag { val response = service.getTag(tag) val body = JSONObject(response .split("").get(0) .trim().replace(Regex("\\};$"), "}")) .getJSONObject("entry_data") .getJSONArray("TagPage") .getJSONObject(0) .getJSONObject("graphql") .getJSONObject(Constants.EXTRAS_HASHTAG) val timelineMedia = body.getJSONObject("edge_hashtag_to_media") return Hashtag( body.getString(Constants.EXTRAS_ID), body.getString("name"), timelineMedia.getLong("count"), if (body.optBoolean("is_following")) 1 else 0, null ) } // TODO convert string response to a response class suspend fun fetchLocation( locationId: Long, ): Location { val response = service.getLocation(locationId) val body = JSONObject(response .split("").get(1) .trim().replace(Regex("};$"), "}")) .getJSONObject("entry_data") .getJSONArray("LocationsPage") .getJSONObject(0) .getJSONObject("graphql") .getJSONObject(Constants.EXTRAS_LOCATION) // val timelineMedia = body.getJSONObject("edge_location_to_media") val address = JSONObject(body.getString("address_json")) return Location( body.getLong(Constants.EXTRAS_ID), body.getString("slug"), body.getString("name"), address.optString("street_address"), address.optString("city_name"), body.optDouble("lng", 0.0), body.optDouble("lat", 0.0) ) } companion object { @Volatile private var INSTANCE: GraphQLRepository? = null fun getInstance(): GraphQLRepository { return INSTANCE ?: synchronized(this) { val service = RetrofitFactory.retrofitWeb.create(GraphQLService::class.java) GraphQLRepository(service).also { INSTANCE = it } } } } }