diff --git a/app/build.gradle b/app/build.gradle index 1c06cee97..a5b5cbc32 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,6 +170,8 @@ dependencies { implementation"com.mikepenz:fastadapter-extensions:${FAST_ADAPTER_EXTENSIONS}@aar" + implementation "com.github.bumptech.glide:okhttp3-integration:${GLIDE}" + //noinspection GradleDependency releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${LEAK_CANARY}" //noinspection GradleDependency diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt index e2e9d9e58..ad1800230 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt @@ -20,6 +20,7 @@ enum class FbItem( relativeUrl: String, val fragmentCreator: () -> BaseFragment = ::WebFragment ) : EnumBundle { + ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"), BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"), CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"), diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index 24f685be9..acc23cadf 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -24,6 +24,7 @@ val FB_EPOCH_MATCHER: Regex by lazy { Regex(":([0-9]+)") } val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_([0-9]+)") } val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9]+)") } val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|']?(.*?)[\"|']?\\)") } +val FB_JSON_URL_MATCHER: Regex by lazy { Regex("\"(http.*?)\"") } operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt similarity index 76% rename from app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt rename to app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt index 51e140973..e3e77c5cc 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -1,6 +1,7 @@ -package com.pitchedapps.frost.facebook +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 @@ -13,7 +14,12 @@ import org.apache.commons.text.StringEscapeUtils */ private val authMap: MutableMap = mutableMapOf() -fun String.fbRequest(action: RequestAuth.() -> Unit) { +/** + * 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() @@ -21,7 +27,7 @@ fun String.fbRequest(action: RequestAuth.() -> Unit) { val auth = getAuth() if (!auth.isValid) { L.e("Attempted fbrequest with invalid auth") - return + return fail() } authMap.put(this, auth) L.i(null, "Found auth $auth") @@ -29,6 +35,9 @@ fun String.fbRequest(action: RequestAuth.() -> Unit) { } } +/** + * Underlying container for all fb requests + */ data class RequestAuth(val userId: Long = -1, val cookie: String = "", val fb_dtsg: String = "", @@ -40,11 +49,11 @@ data class RequestAuth(val userId: Long = -1, /** * Request container with the execution call */ -class FrostRequest(val call: Call, private val invoke: (Call) -> T) { +class FrostRequest(val call: Call, private val invoke: (Call) -> T) { fun invoke() = invoke(call) } -private inline fun RequestAuth.frostRequest( +internal inline fun RequestAuth.frostRequest( noinline invoke: (Call) -> T, builder: Request.Builder.() -> Request.Builder // to ensure we don't do anything extra at the end ): FrostRequest { @@ -61,7 +70,7 @@ private val client: OkHttpClient by lazy { builder.build() } -private fun List>.toForm(): FormBody { +internal fun List>.toForm(): FormBody { val builder = FormBody.Builder() forEach { (key, value) -> val v = value?.toString() ?: "" @@ -70,7 +79,7 @@ private fun List>.toForm(): FormBody { return builder.build() } -private fun List>.withEmptyData(vararg key: String): List> { +internal fun List>.withEmptyData(vararg key: String): List> { val newList = toMutableList() newList.addAll(key.map { it to null }) return newList @@ -81,7 +90,7 @@ private fun String.requestBuilder() = Request.Builder() .header("User-Agent", USER_AGENT_BASIC) .cacheControl(CacheControl.FORCE_NETWORK) -private fun Request.Builder.call() = client.newCall(build()) +fun Request.Builder.call() = client.newCall(build())!! fun String.getAuth(): RequestAuth { var auth = RequestAuth(cookie = this) @@ -113,22 +122,6 @@ fun String.getAuth(): RequestAuth { return auth } -fun RequestAuth.markNotificationRead(notifId: Long): FrostRequest { - - 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()) - } -} - inline fun Array.zip(crossinline mapper: (List) -> O, crossinline caller: (T) -> R): Single { val singles = map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) } @@ -138,11 +131,6 @@ inline fun Array.zip(crossinline mapper: (List) -> } } -fun RequestAuth.markNotificationsRead(vararg notifId: Long) = - notifId.toTypedArray().zip( - { it.all { it } }, - { markNotificationRead(it).invoke() }) - /** * Execute the call and attempt to check validity * Valid = not blank & no "error" instance @@ -158,3 +146,9 @@ fun executeForNoError(call: Call): Boolean { } 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 000000000..61a94ac5a --- /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 { + + @Volatile private var cancelled: Boolean = false + private var urlCall: Call? = null + private var inputStream: InputStream? = null + + private fun DataFetcher.DataCallback.fail(msg: String) { + onLoadFailed(RuntimeException(msg)) + } + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.REMOTE + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + 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 000000000..82a9364be --- /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 { + + 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( + { it.all { it } }, + { markNotificationRead(it).invoke() }) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt index 74a8b98df..2b407b7d9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt @@ -9,9 +9,9 @@ import android.content.Context import android.content.Intent import android.os.BaseBundle import android.os.PersistableBundle -import com.pitchedapps.frost.facebook.RequestAuth -import com.pitchedapps.frost.facebook.fbRequest -import com.pitchedapps.frost.facebook.markNotificationRead +import com.pitchedapps.frost.facebook.requests.RequestAuth +import com.pitchedapps.frost.facebook.requests.fbRequest +import com.pitchedapps.frost.facebook.requests.markNotificationRead import com.pitchedapps.frost.utils.EnumBundle import com.pitchedapps.frost.utils.EnumBundleCompanion import com.pitchedapps.frost.utils.EnumCompanion diff --git a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt index 547920865..a565aa7d5 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt @@ -1,6 +1,6 @@ package com.pitchedapps.frost -import com.pitchedapps.frost.facebook.zip +import com.pitchedapps.frost.facebook.requests.zip import com.pitchedapps.frost.injectors.CssHider import org.junit.Test import kotlin.test.assertTrue diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt index da815b341..a79ccf3f1 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt @@ -41,6 +41,12 @@ class FbRegexTest { assertEquals(id, FB_MESSAGE_NOTIF_ID_MATCHER.find(data)[1]?.toLong(), "thread_fbid mismatch") val userData = "threadlist_row_other_user_fbid_${id}thread_fbid_" assertEquals(id, FB_MESSAGE_NOTIF_ID_MATCHER.find(userData)[1]?.toLong(), "user_fbid mismatch") + } + @Test + fun jsonUrlRegex() { + val url = "https://www.hello.world" + val data = "\"uri\":\"$url\"}" + assertEquals(url, FB_JSON_URL_MATCHER.find(data)[1]) } } \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt index c3b19727f..93f09fc6e 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt @@ -1,5 +1,8 @@ package com.pitchedapps.frost.facebook +import com.pitchedapps.frost.facebook.requests.getAuth +import com.pitchedapps.frost.facebook.requests.getFullSizedImage +import com.pitchedapps.frost.facebook.requests.markNotificationRead import com.pitchedapps.frost.internal.AUTH import com.pitchedapps.frost.internal.COOKIE import com.pitchedapps.frost.internal.USER_ID @@ -48,4 +51,12 @@ class FbRequestTest { AUTH.markNotificationRead(notifId).call.assertNoError() } + @Test + fun fullSizeImage() { + val fbid = 10155966932992838L // google's current cover photo + val url = AUTH.getFullSizedImage(fbid).invoke() + println(url) + assertTrue(url?.startsWith("https://scontent") == true) + } + } \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt index ed88453a8..fb2b2a455 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.internal import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.facebook.requests.RequestAuth +import com.pitchedapps.frost.facebook.requests.getAuth import com.pitchedapps.frost.utils.frostJsoup import org.junit.Assume import java.io.File diff --git a/gradle.properties b/gradle.properties index adbd6c109..1e7f0d8c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,6 +26,7 @@ CRASHLYTICS=2.8.0 DBFLOW=4.2.3 EXOMEDIA=4.1.0 FAST_ADAPTER_EXTENSIONS=3.0.3 +GLIDE=4.4.0 IAB=1.0.44 IICON_COMMUNITY=2.0.46.1 IICON_MATERIAL=2.2.0.4