From 229377bd6e97a5d7ef4ed1d7e361133626793809 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 19 Oct 2023 22:47:42 +0200 Subject: [PATCH] Subscriptions ratelimit and warnings, Nebula login requirement, Subscription fetch setting, -1 sub hide --- app/build.gradle | 2 +- .../java/com/futo/platformplayer/Settings.kt | 6 ++-- .../activities/ExceptionActivity.kt | 4 ++- .../exceptions/RateLimitException.kt | 9 +++++ .../channel/tab/ChannelAboutFragment.kt | 2 +- .../mainactivity/main/ChannelFragment.kt | 2 +- .../main/SubscriptionsFeedFragment.kt | 36 +++++++++++++++++++ .../mainactivity/main/VideoDetailView.kt | 6 ++-- .../platformplayer/models/Subscription.kt | 3 ++ .../futo/platformplayer/states/StateApp.kt | 24 ++++++------- app/src/unstable/assets/sources/nebula | 2 +- 11 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/exceptions/RateLimitException.kt diff --git a/app/build.gradle b/app/build.gradle index d9922658..e4b0d364 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,7 +95,7 @@ android { } defaultConfig { - minSdk 29 + minSdk 28 targetSdk 33 versionCode gitVersionCode versionName gitVersionName diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index ca27483d..ba5207f3 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.webkit.CookieManager import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.* @@ -63,7 +64,8 @@ class Settings : FragmentedStorageFileJson() { try { val i = Intent(Intent.ACTION_VIEW); val subject = "Feedback Grayjay"; - val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n\n"; + val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n" + + "Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n"; val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body)); i.data = data; @@ -217,7 +219,7 @@ class Settings : FragmentedStorageFileJson() { fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate()); - @FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "Auto-rotate deadzone in degrees", 5) + @FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5) @DropdownFieldOptionsId(R.array.auto_rotate_dead_zone) var autoRotateDeadZone: Int = 0; diff --git a/app/src/main/java/com/futo/platformplayer/activities/ExceptionActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/ExceptionActivity.kt index 55d222d1..7e4967fa 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/ExceptionActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/ExceptionActivity.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.activities import android.content.Intent +import android.os.Build import android.os.Bundle import android.widget.LinearLayout import android.widget.TextView @@ -40,7 +41,8 @@ class ExceptionActivity : AppCompatActivity() { val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context"; val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?"; - val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n\n" + + val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" + + "Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" + Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack"); try { val file = File(filesDir, "log.txt"); diff --git a/app/src/main/java/com/futo/platformplayer/exceptions/RateLimitException.kt b/app/src/main/java/com/futo/platformplayer/exceptions/RateLimitException.kt new file mode 100644 index 00000000..c59910de --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/exceptions/RateLimitException.kt @@ -0,0 +1,9 @@ +package com.futo.platformplayer.exceptions + +class RateLimitException : Throwable { + val pluginIds: List; + + constructor(pluginIds: List): super() { + this.pluginIds = pluginIds ?: listOf(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt index 9e28d6f9..bd027ba6 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt @@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment { }; _textName?.text = channel.name; - val metadata = "${channel.subscribers.toHumanNumber()} subscribers"; + val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else ""; _textMetadata?.text = metadata; _lastChannel = channel; setLinks(channel.links, channel.name); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 6e011d64..c17a0af0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -361,7 +361,7 @@ class ChannelFragment : MainFragment() { _buttonSubscribe.setSubscribeChannel(channel); _textChannel.text = channel.name; - _textChannelSub.text = "${channel.subscribers.toHumanNumber()} subscribers"; + _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else ""; _creatorThumbnail.setThumbnail(channel.thumbnail, true); Glide.with(_imageBanner) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index c3f62734..061e42e8 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -12,13 +12,16 @@ import com.futo.platformplayer.* import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.video.IPlatformVideo +import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.exceptions.ChannelException +import com.futo.platformplayer.exceptions.RateLimitException import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorageFileJson @@ -31,6 +34,7 @@ import com.futo.platformplayer.views.subscriptions.SubscriptionBar import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -160,8 +164,17 @@ class SubscriptionsFeedFragment : MainFragment() { private val _filterLock = Object(); private val _filterSettings = FragmentedStorage.get("subFeedFilter"); + private var _bypassRateLimit = false; private val _lastExceptions: List? = null; private val _taskGetPager = TaskHandler>({StateApp.instance.scope}, { withRefresh -> + if(!_bypassRateLimit) { + val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(); + val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n"); + val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true } + Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr); + if(rateLimitPlugins.any()) + throw RateLimitException(rateLimitPlugins.map { it.key.id }); + } val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh); val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions; @@ -171,6 +184,29 @@ class SubscriptionsFeedFragment : MainFragment() { return@TaskHandler resp; }) .success { loadedResult(it); } + .exception { + fragment.lifecycleScope.launch(Dispatchers.IO) { + val subs = StateSubscriptions.instance.getSubscriptions(); + val subsByLimited = it.pluginIds.map{ StatePlatform.instance.getClientOrNull(it) } + .filterIsInstance() + .associateWith { client -> subs.filter { it.getClient() == client } } + .map { Pair(it.key, it.value) } + + withContext(Dispatchers.Main) { + UIDialogs.showDialog(context, R.drawable.ic_security_pred, + "Rate Limit Warning", "This is a temporary measure to prevent people from hitting rate limit until we have better support for lots of subscriptions." + + "\n\nYou have too many subscriptions for the following plugins:\n", + subsByLimited.map { "${it.first.config.name}: ${it.second.size} Subscriptions" } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", { + _bypassRateLimit = true; + loadResults(); + }, UIDialogs.ActionStyle.DANGEROUS_TEXT), + UIDialogs.Action("OK", { + finishRefreshLayoutLoader(); + setLoading(false); + }, UIDialogs.ActionStyle.PRIMARY)); + } + } + } .exception { Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); if(it !is CancellationException) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index fccc88ec..a83da8aa 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1610,10 +1610,10 @@ class VideoDetailView : ConstraintLayout { _lastSubtitleSource = toSet; } - private fun handleUnavailableVideo() { + private fun handleUnavailableVideo(msg: String? = null) { if (!nextVideo()) { if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1)) - UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", "This video is unavailable.", null, 0, + UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", msg ?: "This video is unavailable.", null, 0, UIDialogs.Action("Back", { this@VideoDetailView.onClose.emit(); }, UIDialogs.ActionStyle.PRIMARY)); @@ -2092,7 +2092,7 @@ class VideoDetailView : ConstraintLayout { } .exception { Logger.w(TAG, "exception", it); - handleUnavailableVideo(); + handleUnavailableVideo(it.message); } .exception { Logger.w(TAG, "exception", it) diff --git a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt index 0e869f15..d2db99ab 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt @@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.serializers.OffsetDateTimeSerializer +import com.futo.platformplayer.states.StatePlatform import java.time.OffsetDateTime @kotlinx.serialization.Serializable @@ -42,6 +43,8 @@ class Subscription { fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14; fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2; + fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url); + fun updateChannel(channel: IPlatformChannel) { this.channel = SerializedChannel.fromChannel(channel); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index eb3c50bb..6d6216a2 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -434,21 +434,19 @@ class StateApp { ).flatten(), 0); if(Settings.instance.subscriptions.fetchOnAppBoot) { - val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(); - val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n"); - if (!subRequestCounts.any { clientCount -> - clientCount.key.config.subscriptionRateLimit - ?.let { rateLimit -> clientCount.value > rateLimit } == true - }) { - Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}"); - scope.launch { - delay(5000); - StateSubscriptions.instance.updateSubscriptionFeed(scope, false); + scope.launch(Dispatchers.IO) { + val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(); + val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n"); + val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }; + if (isRateLimitReached) { + Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}"); + delay(5000); + StateSubscriptions.instance.updateSubscriptionFeed(scope, false); + } + else + Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}"); } } - else - Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}"); - } val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes(); scheduleBackgroundWork(context, interval != 0, interval); diff --git a/app/src/unstable/assets/sources/nebula b/app/src/unstable/assets/sources/nebula index aa2a4f29..8ea93936 160000 --- a/app/src/unstable/assets/sources/nebula +++ b/app/src/unstable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit aa2a4f297027903b6182254def885fd82e1bb5c4 +Subproject commit 8ea9393634b13b478916e4dade664427d8f9a0fe