diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/facebook/requests')
3 files changed, 248 insertions, 0 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt new file mode 100644 index 00000000..e3e77c5c --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -0,0 +1,154 @@ +package com.pitchedapps.frost.facebook.requests + +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.utils.L +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import okhttp3.* +import okhttp3.logging.HttpLoggingInterceptor +import org.apache.commons.text.StringEscapeUtils + +/** + * Created by Allan Wang on 21/12/17. + */ +private val authMap: MutableMap<String, RequestAuth> = mutableMapOf() + +/** + * Synchronously fetch [RequestAuth] from cookie + * [action] will only be called if a valid auth is found. + * Otherwise, [fail] will be called + */ +fun String.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) { + val savedAuth = authMap[this] + if (savedAuth != null) { + savedAuth.action() + } else { + val auth = getAuth() + if (!auth.isValid) { + L.e("Attempted fbrequest with invalid auth") + return fail() + } + authMap.put(this, auth) + L.i(null, "Found auth $auth") + auth.action() + } +} + +/** + * Underlying container for all fb requests + */ +data class RequestAuth(val userId: Long = -1, + val cookie: String = "", + val fb_dtsg: String = "", + val rev: String = "") { + val isValid + get() = userId > 0 && cookie.isNotEmpty() && fb_dtsg.isNotEmpty() && rev.isNotEmpty() +} + +/** + * Request container with the execution call + */ +class FrostRequest<out T : Any?>(val call: Call, private val invoke: (Call) -> T) { + fun invoke() = invoke(call) +} + +internal inline fun <T : Any?> RequestAuth.frostRequest( + noinline invoke: (Call) -> T, + builder: Request.Builder.() -> Request.Builder // to ensure we don't do anything extra at the end +): FrostRequest<T> { + val request = cookie.requestBuilder() + request.builder() + return FrostRequest(request.call(), invoke) +} + +private val client: OkHttpClient by lazy { + val builder = OkHttpClient.Builder() + if (BuildConfig.DEBUG) + builder.addInterceptor(HttpLoggingInterceptor() + .setLevel(HttpLoggingInterceptor.Level.BASIC)) + builder.build() +} + +internal fun List<Pair<String, Any?>>.toForm(): FormBody { + val builder = FormBody.Builder() + forEach { (key, value) -> + val v = value?.toString() ?: "" + builder.add(key, v) + } + return builder.build() +} + +internal fun List<Pair<String, Any?>>.withEmptyData(vararg key: String): List<Pair<String, Any?>> { + val newList = toMutableList() + newList.addAll(key.map { it to null }) + return newList +} + +private fun String.requestBuilder() = Request.Builder() + .header("Cookie", this) + .header("User-Agent", USER_AGENT_BASIC) + .cacheControl(CacheControl.FORCE_NETWORK) + +fun Request.Builder.call() = client.newCall(build())!! + +fun String.getAuth(): RequestAuth { + var auth = RequestAuth(cookie = this) + val id = FB_USER_MATCHER.find(this)[1]?.toLong() ?: return auth + auth = auth.copy(userId = id) + val call = this.requestBuilder() + .url(FB_URL_BASE) + .get() + .call() + call.execute().body()?.charStream()?.useLines { + it.forEach { + val text = StringEscapeUtils.unescapeEcmaScript(it) + val fb_dtsg = FB_DTSG_MATCHER.find(text)[1] + if (fb_dtsg != null) { + L.d(null, "fb_dtsg for ${auth.userId}: $fb_dtsg") + auth = auth.copy(fb_dtsg = fb_dtsg) + if (auth.isValid) return auth + } + + val rev = FB_REV_MATCHER.find(text)[1] + if (rev != null) { + L.d(null, "rev for ${auth.userId}: $rev") + auth = auth.copy(rev = rev) + if (auth.isValid) return auth + } + } + } + + return auth +} + +inline fun <T, reified R : Any, O> Array<T>.zip(crossinline mapper: (List<R>) -> O, + crossinline caller: (T) -> R): Single<O> { + val singles = map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) } + return Single.zip(singles) { + val results = it.mapNotNull { it as? R } + mapper(results) + } +} + +/** + * Execute the call and attempt to check validity + * Valid = not blank & no "error" instance + */ +fun executeForNoError(call: Call): Boolean { + val body = call.execute().body() ?: return false + var empty = true + body.charStream().useLines { + it.forEach { + if (it.contains("error")) return false + if (empty && it.isNotEmpty()) empty = false + } + } + return !empty +} + +fun getJsonUrl(call: Call): String? { + val body = call.execute().body() ?: return null + val url = FB_JSON_URL_MATCHER.find(body.string())[1] ?: return null + return StringEscapeUtils.unescapeEcmaScript(url) +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt new file mode 100644 index 00000000..61a94ac5 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt @@ -0,0 +1,67 @@ +package com.pitchedapps.frost.facebook.requests + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.data.DataFetcher +import com.pitchedapps.frost.facebook.FB_URL_BASE +import okhttp3.Call +import okhttp3.Request +import java.io.IOException +import java.io.InputStream + +/** + * Created by Allan Wang on 29/12/17. + */ +fun RequestAuth.getFullSizedImage(fbid: Long) = frostRequest(::getJsonUrl) { + url("${FB_URL_BASE}photo/view_full_size/?fbid=$fbid&__ajax__=&__user=$userId") + get() +} + +class ImageFbidFetcher(private val fbid: Long, + private val cookie: String) : DataFetcher<InputStream> { + + @Volatile private var cancelled: Boolean = false + private var urlCall: Call? = null + private var inputStream: InputStream? = null + + private fun DataFetcher.DataCallback<in InputStream>.fail(msg: String) { + onLoadFailed(RuntimeException(msg)) + } + + override fun getDataClass(): Class<InputStream> = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.REMOTE + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { + cookie.fbRequest(fail = { callback.fail("Invalid auth") }) { + if (cancelled) return@fbRequest callback.fail("Cancelled") + val url = getFullSizedImage(fbid).invoke() ?: return@fbRequest callback.fail("Null url") + if (cancelled) return@fbRequest callback.fail("Cancelled") + urlCall = Request.Builder().url(url).get().call() + + inputStream = try { + urlCall?.execute()?.body()?.byteStream() + } catch (e: IOException) { + null + } + + callback.onDataReady(inputStream) + } + } + + override fun cleanup() { + try { + inputStream?.close() + } catch (e: IOException) { + } finally { + inputStream = null + } + } + + override fun cancel() { + cancelled = true + urlCall?.cancel() + urlCall = null + cleanup() + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt new file mode 100644 index 00000000..82a9364b --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt @@ -0,0 +1,27 @@ +package com.pitchedapps.frost.facebook.requests + +import com.pitchedapps.frost.facebook.FB_URL_BASE + +/** + * Created by Allan Wang on 29/12/17. + */ +fun RequestAuth.markNotificationRead(notifId: Long): FrostRequest<Boolean> { + + val body = listOf( + "click_type" to "notification_click", + "id" to notifId, + "target_id" to "null", + "fb_dtsg" to fb_dtsg, + "__user" to userId + ).withEmptyData("m_sess", "__dyn", "__req", "__ajax__") + + return frostRequest(::executeForNoError) { + url("${FB_URL_BASE}a/jewel_notifications_log.php") + post(body.toForm()) + } +} + +fun RequestAuth.markNotificationsRead(vararg notifId: Long) = + notifId.toTypedArray().zip<Long, Boolean, Boolean>( + { it.all { it } }, + { markNotificationRead(it).invoke() })
\ No newline at end of file |