BarInsta/app/src/main/java/awais/instagrabber/webservices/GraphQLRepository.kt

306 lines
11 KiB
Kotlin

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<Media> = 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<User> = 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("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").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("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").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("<script type=\"text/javascript\">window._sharedData = ", "</script>").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 }
}
}
}
}