1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 20:12:39 +01:00

Merge pull request #1542 from AllanWang/remove-auth

Remove auth and native ui elements by default
This commit is contained in:
Allan Wang 2019-09-15 01:26:10 -07:00 committed by GitHub
commit 96908453ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 11 additions and 996 deletions

View File

@ -61,7 +61,6 @@ import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.kotlin.subscribeDuringJob
import com.pitchedapps.frost.services.FrostRunnable
import com.pitchedapps.frost.utils.ARG_URL
import com.pitchedapps.frost.utils.ARG_USER_ID
import com.pitchedapps.frost.utils.BiometricUtils
@ -224,9 +223,6 @@ abstract class WebOverlayActivityBase : BaseActivity(),
}
}
FrostRunnable.propagate(this, intent)
L.v { "Done propagation" }
swipeBack = kauSwipeOnCreate {
if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx
transitionSystemBars = false

View File

@ -23,8 +23,6 @@ import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic
import com.pitchedapps.frost.R
import com.pitchedapps.frost.fragments.BaseFragment
import com.pitchedapps.frost.fragments.MenuFragment
import com.pitchedapps.frost.fragments.NotificationFragment
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.EnumBundle
import com.pitchedapps.frost.utils.EnumBundleCompanion
@ -47,15 +45,10 @@ enum class FbItem(
FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"),
GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"),
MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"),
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::MenuFragment),
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings"),
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
NOTES(R.string.notes, CommunityMaterial.Icon2.cmd_note, "notes"),
NOTIFICATIONS(
R.string.notifications,
MaterialDesignIconic.Icon.gmi_globe,
"notifications",
::NotificationFragment
),
NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications"),
ON_THIS_DAY(R.string.on_this_day, GoogleMaterial.Icon.gmd_today, "onthisday"),
PAGES(R.string.pages, GoogleMaterial.Icon.gmd_flag, "pages"),
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),

View File

@ -17,58 +17,11 @@
package com.pitchedapps.frost.facebook.requests
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.facebook.FB_DTSG_MATCHER
import com.pitchedapps.frost.facebook.FB_JSON_URL_MATCHER
import com.pitchedapps.frost.facebook.FB_REV_MATCHER
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
import com.pitchedapps.frost.facebook.USER_AGENT
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.kotlin.Flyweight
import com.pitchedapps.frost.utils.L
import kotlinx.coroutines.GlobalScope
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import org.apache.commons.text.StringEscapeUtils
/**
* Created by Allan Wang on 21/12/17.
*/
val fbAuth = Flyweight<String, RequestAuth>(GlobalScope, 3600000 /* an hour */) {
it.getAuth()
}
/**
* Underlying container for all fb requests
*/
data class RequestAuth(
val userId: Long = -1,
val cookie: String = "",
val fb_dtsg: String = "",
val rev: String = ""
) {
val isComplete
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)
}
val httpClient: OkHttpClient by lazy {
val builder = OkHttpClient.Builder()
@ -80,21 +33,6 @@ val httpClient: OkHttpClient by lazy {
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
}
internal fun String?.requestBuilder(): Request.Builder {
val builder = Request.Builder()
.header("User-Agent", USER_AGENT)
@ -105,58 +43,3 @@ internal fun String?.requestBuilder(): Request.Builder {
}
fun Request.Builder.call(): Call = httpClient.newCall(build())
fun String.getAuth(): RequestAuth {
L.v { "Getting auth for ${hashCode()}" }
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 { lines ->
lines.forEach {
val text = try {
StringEscapeUtils.unescapeEcmaScript(it)
} catch (ignore: Exception) {
return@forEach
}
val fb_dtsg = FB_DTSG_MATCHER.find(text)[1]
if (fb_dtsg != null) {
auth = auth.copy(fb_dtsg = fb_dtsg)
if (auth.isComplete) return auth
}
val rev = FB_REV_MATCHER.find(text)[1]
if (rev != null) {
auth = auth.copy(rev = rev)
if (auth.isComplete) return auth
}
}
}
return auth
}
/**
* 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 { lines ->
lines.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)
}

View File

@ -16,38 +16,17 @@
*/
package com.pitchedapps.frost.facebook.requests
import com.bumptech.glide.Priority
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.signature.ObjectKey
import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER
import com.pitchedapps.frost.facebook.FB_REDIRECT_URL_MATCHER
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.utils.L
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import okhttp3.Call
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()
}
/**
* Attempts to get the fbcdn url of the supplied image redirect url
@ -65,106 +44,3 @@ suspend fun String.getFullSizedImageUrl(url: String, timeout: Long = 3000): Stri
null
}
}
/**
* Request loader for a potentially hd version of a url
* In this case, each url may potentially return an id,
* which may potentially be used to fetch a higher res image url
* The following aims to allow such loading while adhering to Glide's lifecycle
*/
data class HdImageMaybe(val url: String, val cookie: String) {
val id: Long by lazy { FB_IMAGE_ID_MATCHER.find(url)[1]?.toLongOrNull() ?: -1 }
val isValid: Boolean by lazy {
id != -1L && cookie.isNotBlank()
}
}
/*
* The following was a test to see if hd image loading would work
*
* It's working and tested, though the improvements aren't really worth the extra data use
* and reload
*/
class HdImageLoadingFactory : ModelLoaderFactory<HdImageMaybe, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory) = HdImageLoading()
override fun teardown() = Unit
}
fun <T> RequestBuilder<T>.loadWithPotentialHd(model: HdImageMaybe) =
thumbnail(clone().load(model.url))
.load(model)
.apply(RequestOptions().override(Target.SIZE_ORIGINAL))
class HdImageLoading : ModelLoader<HdImageMaybe, InputStream> {
override fun buildLoadData(
model: HdImageMaybe,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? =
if (!model.isValid) null
else ModelLoader.LoadData(ObjectKey(model), HdImageFetcher(model))
override fun handles(model: HdImageMaybe) = model.isValid
}
class HdImageFetcher(private val model: HdImageMaybe) : 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>) {
if (!model.isValid) return callback.fail("Model is invalid")
val result: Result<InputStream?> = runCatching {
runBlocking {
withTimeout(20000L) {
val auth = fbAuth.fetch(model.cookie).await()
if (cancelled) throw RuntimeException("Cancelled")
val url = auth.getFullSizedImage(model.id).invoke()
?: throw RuntimeException("Null url")
if (cancelled) throw RuntimeException("Cancelled")
if (!url.contains("png") && !url.contains("jpg")) throw RuntimeException("Invalid format")
urlCall?.execute()?.body()?.byteStream()
}
}
}
if (result.isSuccess)
callback.onDataReady(result.getOrNull())
else
callback.onLoadFailed(
result.exceptionOrNull() as? Exception ?: RuntimeException("Failed")
)
}
override fun cleanup() {
try {
inputStream?.close()
} catch (e: IOException) {
} finally {
inputStream = null
}
}
override fun cancel() {
cancelled = true
urlCall?.cancel()
urlCall = null
cleanup()
}
}

View File

@ -1,192 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.facebook.requests
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.L
import okhttp3.Call
import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup
import java.io.IOException
/**
* Created by Allan Wang on 29/12/17.
*/
fun RequestAuth.getMenuData(): FrostRequest<MenuData?> {
val body = listOf(
"fb_dtsg" to fb_dtsg,
"__user" to userId
).withEmptyData("m_sess", "__dyn", "__req", "__ajax__")
return frostRequest(::parseMenu) {
url("${FB_URL_BASE}bookmarks/flyout/body/?id=u_0_2")
post(body.toForm())
}
}
fun parseMenu(call: Call): MenuData? {
val fullString = call.execute().body()?.string() ?: return null
var jsonString = fullString.substringAfter("bookmarkGroups", "")
.substringAfter("[", "")
if (jsonString.isBlank()) return null
jsonString = "{ \"data\" : [${StringEscapeUtils.unescapeEcmaScript(jsonString)}"
val mapper = ObjectMapper()
.disable(MapperFeature.AUTO_DETECT_SETTERS)
return try {
val data = mapper.readValue(jsonString, MenuData::class.java)
// parse footer content
val footer = fullString.substringAfter("footerMarkup", "")
.substringAfter("{", "")
.substringBefore("}", "")
val doc = Jsoup.parseBodyFragment(
StringEscapeUtils.unescapeEcmaScript(
StringEscapeUtils.unescapeEcmaScript(footer)
)
)
val footerData = mutableListOf<MenuFooterItem>()
val footerSmallData = mutableListOf<MenuFooterItem>()
doc.select("a[href]").forEach {
val text = it.text()
it.parent()
if (text.isEmpty()) return@forEach
val href = it.attr("href").formattedFbUrl
val item = MenuFooterItem(name = text, url = href)
if (it.parent().tag().name == "span")
footerSmallData.add(item)
else
footerData.add(item)
}
return data.copy(footer = MenuFooter(footerData, footerSmallData))
} catch (e: IOException) {
L.e(e) { "Menu parse fail" }
null
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class MenuData(
val data: List<MenuHeader> = emptyList(),
val footer: MenuFooter = MenuFooter()
) {
@JsonCreator
constructor(
@JsonProperty("data") data: List<MenuHeader>?
) : this(data ?: emptyList(), MenuFooter())
fun flatMapValid(): List<MenuItemData> {
val items = mutableListOf<MenuItemData>()
data.forEach {
if (it.isValid) items.add(it)
items.addAll(it.visible.filter(MenuItem::isValid))
}
items.addAll(footer.data.filter(MenuFooterItem::isValid))
items.addAll(footer.smallData.filter(MenuFooterItem::isValid))
return items
}
}
interface MenuItemData {
val isValid: Boolean
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class MenuHeader(
val id: String? = null,
val header: String? = null,
val visible: List<MenuItem> = emptyList(),
val all: List<MenuItem> = emptyList()
) : MenuItemData {
@JsonCreator
constructor(
@JsonProperty("id") id: String?,
@JsonProperty("header") header: String?,
@JsonProperty("visible") visible: List<MenuItem>?,
@JsonProperty("all") all: List<MenuItem>?,
@JsonProperty("fake") fake: Boolean?
) : this(id, header, visible ?: emptyList(), all ?: emptyList())
override val isValid: Boolean
get() = !header.isNullOrBlank()
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class MenuItem(
val id: String? = null,
val name: String? = null,
val pic: String? = null,
val url: String? = null,
val badge: String? = null,
val countDetails: String? = null
) : MenuItemData {
@JsonCreator
constructor(
@JsonProperty("id") id: String?,
@JsonProperty("name") name: String?,
@JsonProperty("pic") pic: String?,
@JsonProperty("url") url: String?,
@JsonProperty("count") badge: String?,
@JsonProperty("count_details") countDetails: String?,
@JsonProperty("fake") fake: Boolean?
) : this(
id, name, pic?.formattedFbUrl,
url?.formattedFbUrl,
if (badge == "0") null else badge,
countDetails
)
override val isValid: Boolean
get() = !name.isNullOrBlank() && !url.isNullOrBlank()
}
data class MenuFooter(
val data: List<MenuFooterItem> = emptyList(),
val smallData: List<MenuFooterItem> = emptyList()
) {
val hasContent
get() = data.isNotEmpty() || smallData.isNotEmpty()
}
data class MenuFooterItem(
val name: String? = null,
val url: String? = null,
val isSmall: Boolean = false
) : MenuItemData {
override val isValid: Boolean
get() = name != null && url != null
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.facebook.requests
import com.pitchedapps.frost.facebook.FB_URL_BASE
import okhttp3.Call
/**
* Created by Allan Wang on 07/01/18.
*/
fun RequestAuth.sendMessage(group: String, content: String): FrostRequest<Boolean> {
// todo test more; only tested against tids=cid...
val body = listOf(
"tids" to group,
"body" to content,
"fb_dtsg" to fb_dtsg,
"__user" to userId
).withEmptyData("m_sess", "__dyn", "__req", "__ajax__")
return frostRequest(::validateMessage) {
url("${FB_URL_BASE}messages/send")
post(body.toForm())
}
}
/**
* Messages are a bit weird with their responses
*/
private fun validateMessage(call: Call): Boolean {
val body = call.execute().body() ?: return false
// todo
return true
}

View File

@ -1,38 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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())
}
}

View File

@ -67,7 +67,7 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna
data: FbItem,
position: Int
): BaseFragment {
val fragment = if (useFallback || !Prefs.nativeUi) WebFragment() else base()
val fragment = if (useFallback) WebFragment() else base()
val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data
fragment.withArguments(
ARG_URL to d.url,

View File

@ -16,32 +16,20 @@
*/
package com.pitchedapps.frost.fragments
import com.mikepenz.fastadapter.IItem
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.parsers.FrostNotifs
import com.pitchedapps.frost.facebook.parsers.NotifParser
import com.pitchedapps.frost.facebook.parsers.ParseResponse
import com.pitchedapps.frost.facebook.requests.MenuFooterItem
import com.pitchedapps.frost.facebook.requests.MenuHeader
import com.pitchedapps.frost.facebook.requests.MenuItem
import com.pitchedapps.frost.facebook.requests.MenuItemData
import com.pitchedapps.frost.facebook.requests.fbAuth
import com.pitchedapps.frost.facebook.requests.getMenuData
import com.pitchedapps.frost.iitems.ClickableIItemContract
import com.pitchedapps.frost.iitems.MenuContentIItem
import com.pitchedapps.frost.iitems.MenuFooterIItem
import com.pitchedapps.frost.iitems.MenuFooterSmallIItem
import com.pitchedapps.frost.iitems.MenuHeaderIItem
import com.pitchedapps.frost.iitems.NotificationIItem
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.views.FrostRecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 27/12/17.
*
* Retained as an example. Deletion made at https://github.com/AllanWang/Frost-for-Facebook/pull/1542
*/
@Deprecated(message = "Retained as an example; currently does not support marking a notification as read")
class NotificationFragment : FrostParserFragment<FrostNotifs, NotificationIItem>() {
override val parser = NotifParser
@ -55,33 +43,3 @@ class NotificationFragment : FrostParserFragment<FrostNotifs, NotificationIItem>
NotificationIItem.bindEvents(adapter)
}
}
class MenuFragment : GenericRecyclerFragment<MenuItemData, IItem<*, *>>() {
override fun mapper(data: MenuItemData): IItem<*, *> = when (data) {
is MenuHeader -> MenuHeaderIItem(data)
is MenuItem -> MenuContentIItem(data)
is MenuFooterItem ->
if (data.isSmall) MenuFooterSmallIItem(data)
else MenuFooterIItem(data)
else -> throw IllegalArgumentException("Menu item in fragment has invalid type ${data::class.java.simpleName}")
}
override fun bindImpl(recyclerView: FrostRecyclerView) {
ClickableIItemContract.bindEvents(adapter)
}
override suspend fun reloadImpl(progress: (Int) -> Unit): List<MenuItemData>? =
withContext(Dispatchers.IO) {
val cookie = FbCookie.webCookie ?: return@withContext null
progress(10)
val auth = fbAuth.fetch(cookie).await()
progress(30)
val data = auth.getMenuData().invoke() ?: return@withContext null
if (data.data.isEmpty()) return@withContext null
progress(70)
val items = data.flatMapValid()
progress(90)
return@withContext items
}
}

View File

@ -1,87 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.iitems
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import ca.allanwang.kau.iitems.KauIItem
import ca.allanwang.kau.ui.createSimpleRippleDrawable
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.gone
import ca.allanwang.kau.utils.visible
import com.mikepenz.fastadapter.FastAdapter
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.requests.MenuFooterItem
import com.pitchedapps.frost.facebook.requests.MenuHeader
import com.pitchedapps.frost.facebook.requests.MenuItem
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.utils.Prefs
/**
* Created by Allan Wang on 30/12/17.
*/
class MenuContentIItem(val data: MenuItem) :
KauIItem<MenuContentIItem, MenuContentIItem.ViewHolder>(R.layout.iitem_menu, ::ViewHolder),
ClickableIItemContract {
override val url: String?
get() = data.url
class ViewHolder(itemView: View) : FastAdapter.ViewHolder<MenuContentIItem>(itemView) {
val frame: ViewGroup by bindView(R.id.item_frame)
val icon: ImageView by bindView(R.id.item_icon)
val content: TextView by bindView(R.id.item_content)
val badge: TextView by bindView(R.id.item_badge)
override fun bindView(item: MenuContentIItem, payloads: MutableList<Any>) {
frame.background = createSimpleRippleDrawable(Prefs.textColor, Prefs.nativeBgColor)
content.setTextColor(Prefs.textColor)
badge.setTextColor(Prefs.textColor)
val iconUrl = item.data.pic
if (iconUrl != null)
GlideApp.with(itemView)
.load(iconUrl)
.transform(FrostGlide.circleCrop)
.into(icon.visible())
else
icon.gone()
content.text = item.data.name
badge.text = item.data.badge
}
override fun unbindView(item: MenuContentIItem) {
GlideApp.with(itemView).clear(icon)
content.text = null
badge.text = null
}
}
}
class MenuHeaderIItem(val data: MenuHeader) : HeaderIItem(
data.header,
itemId = R.id.item_menu_header
)
class MenuFooterIItem(val data: MenuFooterItem) :
TextIItem(data.name, data.url, R.id.item_menu_footer)
class MenuFooterSmallIItem(val data: MenuFooterItem) :
TextIItem(data.name, data.url, R.id.item_menu_footer_small)

View File

@ -34,7 +34,6 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.parsers.FrostNotif
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.services.FrostRunnable
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.isIndependent
import com.pitchedapps.frost.utils.launchWebOverlay
@ -53,7 +52,6 @@ class NotificationIItem(val notification: FrostNotif, val cookie: String) :
.withOnClickListener { v, _, item, position ->
val notif = item.notification
if (notif.unread) {
FrostRunnable.markNotificationRead(v!!.context, notif.id, item.cookie)
adapter.set(
position,
NotificationIItem(notif.copy(unread = false), item.cookie)

View File

@ -76,11 +76,7 @@ enum class NotificationType(
FbItem.NOTIFICATIONS,
NotifParser,
Prefs::notificationRingtone
) {
override fun bindRequest(content: NotificationContent, cookie: String) =
FrostRunnable.prepareMarkNotificationRead(content.id, cookie)
},
),
MESSAGE(
NOTIF_CHANNEL_MESSAGES,

View File

@ -1,200 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.services
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.BaseBundle
import android.os.PersistableBundle
import com.pitchedapps.frost.facebook.requests.RequestAuth
import com.pitchedapps.frost.facebook.requests.fbAuth
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
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 28/12/17.
*/
/**
* Private helper data
*/
private enum class FrostRequestCommands : EnumBundle<FrostRequestCommands> {
NOTIF_READ {
override fun invoke(auth: RequestAuth, bundle: PersistableBundle) {
val id = bundle.getLong(ARG_0, -1L)
val success = auth.markNotificationRead(id).invoke()
L.d { "Marked notif $id as read: $success" }
}
override fun propagate(bundle: BaseBundle) =
FrostRunnable.prepareMarkNotificationRead(
bundle.getLong(ARG_0),
bundle.getCookie()
)
};
override val bundleContract: EnumBundleCompanion<FrostRequestCommands>
get() = Companion
/**
* Call request with arguments inside bundle
*/
abstract fun invoke(auth: RequestAuth, bundle: PersistableBundle)
/**
* Return bundle builder given arguments in the old bundle
* Must not write to old bundle!
*/
abstract fun propagate(bundle: BaseBundle): BaseBundle.() -> Unit
companion object : EnumCompanion<FrostRequestCommands>("frost_arg_commands", values())
}
private const val ARG_COMMAND = "frost_request_command"
private const val ARG_COOKIE = "frost_request_cookie"
private const val ARG_0 = "frost_request_arg_0"
private const val ARG_1 = "frost_request_arg_1"
private const val ARG_2 = "frost_request_arg_2"
private const val ARG_3 = "frost_request_arg_3"
private fun BaseBundle.getCookie(): String = getString(ARG_COOKIE)!!
private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie)
/**
* Singleton handler for running requests in [FrostRequestService]
* Requests are typically completely decoupled from the UI,
* and are optional enhancers.
*
* Nothing guarantees the completion time, or whether it even executes at all
*
* Design:
* prepare function - creates a bundle binder
* actor function - calls the service with the given arguments
*
* Global:
* propagator - given a bundle with a command, extracts and executes the requests
*/
object FrostRunnable {
fun prepareMarkNotificationRead(id: Long, cookie: String): BaseBundle.() -> Unit = {
FrostRequestCommands.NOTIF_READ.put(this)
putLong(ARG_0, id)
putCookie(cookie)
}
fun markNotificationRead(context: Context, id: Long, cookie: String): Boolean {
if (id <= 0) {
L.d { "Invalid notification id $id for marking as read" }
return false
}
return schedule(
context, FrostRequestCommands.NOTIF_READ,
prepareMarkNotificationRead(id, cookie)
)
}
fun propagate(context: Context, intent: Intent?) {
val extras = intent?.extras ?: return
val command = FrostRequestCommands[intent] ?: return
intent.removeExtra(ARG_COMMAND) // reset
L.d { "Propagating command ${command.name}" }
val builder = command.propagate(extras)
schedule(context, command, builder)
}
private fun schedule(
context: Context,
command: FrostRequestCommands,
bundleBuilder: PersistableBundle.() -> Unit
): Boolean {
val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(context, FrostRequestService::class.java)
val bundle = PersistableBundle()
bundle.bundleBuilder()
bundle.putString(ARG_COMMAND, command.name)
if (bundle.getCookie().isNullOrBlank()) {
L.e { "Scheduled frost request with empty cookie" }
return false
}
val builder = JobInfo.Builder(REQUEST_SERVICE_BASE + command.ordinal, serviceComponent)
.setMinimumLatency(0L)
.setExtras(bundle)
.setOverrideDeadline(2000L)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
val result = scheduler.schedule(builder.build())
if (result <= 0) {
L.eThrow("FrostRequestService scheduler failed for ${command.name}")
return false
}
L.d { "Scheduled ${command.name}" }
return true
}
}
class FrostRequestService : BaseJobService() {
override fun onStartJob(params: JobParameters?): Boolean {
super.onStartJob(params)
if (!Prefs.authRequests) {
L.i { "Auth requests disabled; skipping request service" }
return false
}
val bundle = params?.extras
if (bundle == null) {
L.eThrow("Launched ${this::class.java.simpleName} without param data")
return false
}
val cookie = bundle.getCookie()
if (cookie.isBlank()) {
L.eThrow("Launched ${this::class.java.simpleName} without cookie")
return false
}
val command = FrostRequestCommands[bundle]
if (command == null) {
L.eThrow("Launched ${this::class.java.simpleName} without command")
return false
}
launch(Dispatchers.IO) {
try {
val auth = fbAuth.fetch(cookie).await()
command.invoke(auth, bundle)
L.d {
"Finished frost service for ${command.name} in ${System.currentTimeMillis() - startTime} ms"
}
} catch (e: Exception) {
L.e(e) { "Failed frost service for ${command.name} in ${System.currentTimeMillis() - startTime} ms" }
} finally {
jobFinished(params, false)
}
}
return true
}
}

View File

@ -79,13 +79,6 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
}
}
checkbox(R.string.native_ui, Prefs::nativeUi, {
Prefs.nativeUi = it
shouldRestartMain()
}) {
descRes = R.string.native_ui_desc
}
checkbox(R.string.exit_confirmation, Prefs::exitConfirmation, { Prefs.exitConfirmation = it }) {
descRes = R.string.exit_confirmation_desc
}

View File

@ -35,10 +35,6 @@ fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = {
// Experimental content starts here ------------------
checkbox(R.string.auth_requests, Prefs::authRequests, { Prefs.authRequests = it }) {
descRes = R.string.auth_requests_desc
}
// Experimental content ends here --------------------
checkbox(R.string.verbose_logging, Prefs::verboseLogging, {

View File

@ -84,4 +84,4 @@ fun KauLoggerExtension.test(message: () -> Any?) {
if (BuildConfig.DEBUG) {
d { "Test1234 ${message()}" }
}
}
}

View File

@ -191,10 +191,6 @@ object Prefs : KPref() {
var showCreateFab: Boolean by kpref("show_create_fab", true)
var authRequests: Boolean by kpref("web_requests", false)
var nativeUi: Boolean by kpref("native_ui", true)
inline val mainActivityLayout: MainActivityLayout
get() = MainActivityLayout(mainActivityLayoutType)

View File

@ -1,4 +1,4 @@
v2.4.0
* Removed web only mode for auth requests. Marking notifications as read is now disabled by default to deal with phishing accusations.
* Removed request services, which potentially caused phishing warnings.
* Save images with the correct extensions.

View File

@ -17,8 +17,6 @@
<string name="search_bar_desc">Enable the search bar instead of a search overlay</string>
<string name="force_message_bottom">Force Message Bottom</string>
<string name="force_message_bottom_desc">When loading a message thread, trigger a scroll to the bottom of the page rather than loading the page as is.</string>
<string name="native_ui">Native UI</string>
<string name="native_ui_desc">Enables native UI for certain options in the main activity. Done through parsing.</string>
<string name="enable_pip">Enable PIP</string>
<string name="enable_pip_desc">Enable picture in picture videos</string>
<string name="autoplay_settings">Autoplay Settings</string>

View File

@ -7,7 +7,4 @@
<string name="restart_frost">Restart Frost</string>
<string name="restart_frost_desc">Launch a cold restart for the application.</string>
<!-- Debugging phishing warnings -->
<string name="auth_requests" translatable="false">Auth Requests</string>
<string name="auth_requests_desc" translatable="false">Enables post features such as marking notifications as read. Known to potentially cause phishing responses.</string>
</resources>

View File

@ -7,7 +7,7 @@
-->
<version title="v2.4.0" />
<item text="Removed web only mode for auth requests. Marking notifications as read is now disabled by default to deal with phishing accusations." />
<item text="Removed request services, which potentially caused phishing warnings." />
<item text="Save images with the correct extensions." />
<item text="" />

View File

@ -1,93 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.facebook.requests
import com.fasterxml.jackson.databind.ObjectMapper
import com.pitchedapps.frost.internal.AUTH
import com.pitchedapps.frost.internal.COOKIE
import com.pitchedapps.frost.internal.USER_ID
import com.pitchedapps.frost.internal.authDependent
import okhttp3.Call
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
/**
* Created by Allan Wang on 21/12/17.
*/
class FbRequestTest {
companion object {
@BeforeClass
@JvmStatic
fun before() {
authDependent()
}
}
/**
* Used to emulate [executeForNoError]
* Must be consistent with that method
*/
private fun Call.assertNoError() {
val data = execute().body()?.string() ?: fail("Content was null")
println("Call response: $data")
assertTrue(data.isNotEmpty(), "Content was empty")
assertFalse(data.contains("error"), "Content had error")
}
@Test
fun auth() {
val auth = COOKIE.getAuth()
assertNotNull(auth)
assertEquals(USER_ID, auth.userId)
assertEquals(COOKIE, auth.cookie)
println("Test auth: ${auth.fb_dtsg}")
}
@Test
@Ignore("Post requests are now experimental")
fun markNotification() {
val notifId = 1514443903880
AUTH.markNotificationRead(notifId).call.assertNoError()
}
@Ignore("Broken as of 2019/01/03; however, this was never used in production to begin with")
@Test
fun fullSizeImage() {
val fbid = 10150706277522838L // google's current cover photo
val url = AUTH.getFullSizedImage(fbid).invoke()
println(url)
assertEquals(true, url?.startsWith("https://scontent"), "Bad start for url $url")
}
@Test
fun testMenu() {
val data = AUTH.getMenuData().invoke()
assertNotNull(data)
println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(data))
assertTrue(data.data.isNotEmpty())
assertTrue(data.footer.hasContent, "Footer may be badly parsed")
val items = data.flatMapValid()
assertTrue(items.size > 15, "Something may be badly parsed")
}
}

View File

@ -19,8 +19,6 @@ package com.pitchedapps.frost.internal
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.get
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
@ -53,11 +51,6 @@ val PROPS: Properties by lazy {
val COOKIE: String by lazy { PROPS.getProperty("COOKIE") ?: "" }
val USER_ID: Long by lazy { FB_USER_MATCHER.find(COOKIE)[1]?.toLong() ?: -1 }
val AUTH: RequestAuth by lazy {
COOKIE.getAuth().apply {
println("Auth:\nuser:$userId\nfb_dtsg: $fb_dtsg\nrev: $rev\ncomplete: $isComplete")
}
}
private val VALID_COOKIE: Boolean by lazy {
val data = testJsoup(FbItem.SETTINGS.url)

View File

@ -1,7 +1,7 @@
# Changelog
## v2.4.0
* Removed web only mode for auth requests. Marking notifications as read is now disabled by default to deal with phishing accusations.
* Removed request services, which potentially caused phishing warnings.
* Save images with the correct extensions.
## v2.3.2