mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-05-15 09:50:41 +00:00
feat(network): Added handler for feature disabled proton error code.
This commit is contained in:
+32
-1
@@ -18,12 +18,18 @@
|
||||
|
||||
package me.proton.core.featureflag.dagger
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import dagger.Binds
|
||||
import dagger.BindsOptionalOf
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.core.featureflag.data.FeatureFlagManagerImpl
|
||||
import me.proton.core.featureflag.data.R
|
||||
import me.proton.core.featureflag.data.listener.FeatureDisabledListenerImpl
|
||||
import me.proton.core.featureflag.data.local.FeatureFlagLocalDataSourceImpl
|
||||
import me.proton.core.featureflag.data.remote.FeatureFlagRemoteDataSourceImpl
|
||||
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManagerImpl
|
||||
@@ -34,11 +40,36 @@ import me.proton.core.featureflag.domain.repository.FeatureFlagContextProvider
|
||||
import me.proton.core.featureflag.domain.repository.FeatureFlagLocalDataSource
|
||||
import me.proton.core.featureflag.domain.repository.FeatureFlagRemoteDataSource
|
||||
import me.proton.core.featureflag.domain.repository.FeatureFlagRepository
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.session.SessionProvider
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
public interface CoreFeatureFlagModule {
|
||||
public class CoreFeatureFlagModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public fun provideFeatureDisabledListener(
|
||||
@ApplicationContext appContext: Context,
|
||||
featureFlagRepositoryProvider: Provider<FeatureFlagRepository>,
|
||||
sessionProvider: SessionProvider,
|
||||
): FeatureDisabledListener = FeatureDisabledListenerImpl(
|
||||
featureFlagRepositoryProvider = featureFlagRepositoryProvider,
|
||||
sessionProvider = sessionProvider,
|
||||
minimumFetchInterval = appContext.resources.getInteger(
|
||||
R.integer.core_feature_feature_flag_on_feature_disabled_minimum_fetch_interval_seconds
|
||||
).toDuration(DurationUnit.SECONDS),
|
||||
monoClock = { SystemClock.elapsedRealtime() }
|
||||
)
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
public interface CoreFeatureFlagBindsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore 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.
|
||||
*
|
||||
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.featureflag.data.listener
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import me.proton.core.featureflag.domain.repository.FeatureFlagRepository
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
import me.proton.core.network.domain.session.SessionProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlin.time.Duration
|
||||
|
||||
public class FeatureDisabledListenerImpl @Inject constructor(
|
||||
private val featureFlagRepositoryProvider: Provider<FeatureFlagRepository>,
|
||||
private val sessionProvider: SessionProvider,
|
||||
private val minimumFetchInterval: Duration,
|
||||
private val monoClock: () -> Long,
|
||||
) : FeatureDisabledListener {
|
||||
private var lastFetchTimestampMs: Long? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
override suspend fun onFeatureDisabled(sessionId: SessionId?) {
|
||||
if (shouldFetch()) {
|
||||
featureFlagRepositoryProvider.get()?.getAll(sessionId?.let { sessionProvider.getUserId(sessionId) })
|
||||
updateLastFetchTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shouldFetch(): Boolean = getLastFetchTimestampMs()?.let { lastFetch ->
|
||||
monoClock() - lastFetch > minimumFetchInterval.inWholeMilliseconds
|
||||
} ?: true
|
||||
|
||||
private suspend fun getLastFetchTimestampMs(): Long? = mutex.withLock {
|
||||
lastFetchTimestampMs
|
||||
}
|
||||
|
||||
private suspend fun updateLastFetchTimestamp() {
|
||||
mutex.withLock {
|
||||
lastFetchTimestampMs = monoClock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@
|
||||
<resources>
|
||||
<integer name="core_feature_feature_flag_worker_repeat_interval_unauth_seconds">86400</integer>
|
||||
<integer name="core_feature_feature_flag_worker_repeat_interval_auth_seconds">21600</integer>
|
||||
<integer name="core_feature_feature_flag_on_feature_disabled_minimum_fetch_interval_seconds">600</integer>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<public name="core_feature_feature_flag_worker_repeat_interval_unauth_seconds" type="integer" />
|
||||
<public name="core_feature_feature_flag_worker_repeat_interval_auth_seconds" type="integer" />
|
||||
<public name="core_feature_feature_flag_on_feature_disabled_minimum_fetch_interval_seconds" type="integer"/>
|
||||
</resources>
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore 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.
|
||||
*
|
||||
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.featureflag.data.listener
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.core.featureflag.domain.repository.FeatureFlagRepository
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.session.SessionProvider
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import javax.inject.Provider
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class FeatureDisabledListenerTest(
|
||||
private val minimumFetchIntervalSeconds: Long,
|
||||
private val delayBetweenOnFeatureDisabledSeconds: Long,
|
||||
private val expectedRepositoryCalls: Int,
|
||||
) {
|
||||
private lateinit var featureDisabledListener: FeatureDisabledListener
|
||||
private val sessionProvider = mockk<SessionProvider>(relaxed = true)
|
||||
private val featureFlagRepository = mockk<FeatureFlagRepository>(relaxed = true)
|
||||
private val featureFlagRepositoryProvider = Provider { featureFlagRepository }
|
||||
|
||||
@Test
|
||||
fun testTwoCallsOnFeatureDisabled() = runTest {
|
||||
//Given
|
||||
featureDisabledListener = FeatureDisabledListenerImpl(
|
||||
featureFlagRepositoryProvider = featureFlagRepositoryProvider,
|
||||
sessionProvider = sessionProvider,
|
||||
minimumFetchInterval = minimumFetchIntervalSeconds.toDuration(DurationUnit.SECONDS),
|
||||
monoClock = { testScheduler.currentTime },
|
||||
)
|
||||
|
||||
//When
|
||||
featureDisabledListener.onFeatureDisabled(null)
|
||||
advanceTimeBy(delayBetweenOnFeatureDisabledSeconds.toDuration(DurationUnit.SECONDS))
|
||||
featureDisabledListener.onFeatureDisabled(null)
|
||||
|
||||
//Then
|
||||
coVerify(exactly = expectedRepositoryCalls) {
|
||||
featureFlagRepository.getAll(any())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@get:Parameterized.Parameters(
|
||||
"minimumFetchIntervalSeconds: {0}, delayBetweenOnFeatureDisabledSeconds: {1}, expectedRepositoryCalls: {2}"
|
||||
)
|
||||
@get:JvmStatic
|
||||
val data = listOf(
|
||||
arrayOf(3, 2, 1),
|
||||
arrayOf(3, 4, 2),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator
|
||||
import me.proton.core.network.domain.client.ExtraHeaderProvider
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
|
||||
import me.proton.core.network.domain.interceptor.InterceptorInfo
|
||||
@@ -100,6 +101,7 @@ public class CoreNetworkModule {
|
||||
deviceVerificationProvider: DeviceVerificationProvider,
|
||||
deviceVerificationListener: DeviceVerificationListener,
|
||||
missingScopeListener: MissingScopeListener,
|
||||
featureDisabledListener: FeatureDisabledListener,
|
||||
extraHeaderProvider: ExtraHeaderProvider,
|
||||
clientVersionValidator: ClientVersionValidator,
|
||||
dohAlternativesListener: DohAlternativesListener?,
|
||||
@@ -124,6 +126,7 @@ public class CoreNetworkModule {
|
||||
deviceVerificationProvider,
|
||||
deviceVerificationListener,
|
||||
missingScopeListener,
|
||||
featureDisabledListener,
|
||||
cookieStore,
|
||||
CoroutineScope(Job() + Dispatchers.Default),
|
||||
certificatePins,
|
||||
|
||||
@@ -40,8 +40,10 @@ import me.proton.core.network.domain.client.ClientVersionValidator
|
||||
import me.proton.core.network.domain.client.ExtraHeaderProvider
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.handlers.DeviceVerificationNeededHandler
|
||||
import me.proton.core.network.domain.handlers.DohApiHandler
|
||||
import me.proton.core.network.domain.handlers.FeatureDisabledHandler
|
||||
import me.proton.core.network.domain.handlers.HumanVerificationInvalidHandler
|
||||
import me.proton.core.network.domain.handlers.HumanVerificationNeededHandler
|
||||
import me.proton.core.network.domain.handlers.MissingScopeHandler
|
||||
@@ -87,6 +89,7 @@ class ApiManagerFactory(
|
||||
private val deviceVerificationProvider: DeviceVerificationProvider,
|
||||
private val deviceVerificationListener: DeviceVerificationListener,
|
||||
private val missingScopeListener: MissingScopeListener,
|
||||
private val featureDisabledListener: FeatureDisabledListener,
|
||||
private val cookieStore: ProtonCookieStore,
|
||||
scope: CoroutineScope,
|
||||
private val certificatePins: Array<String> = Constants.DEFAULT_SPKI_PINS,
|
||||
@@ -157,6 +160,8 @@ class ApiManagerFactory(
|
||||
HumanVerificationInvalidHandler<Api>(sessionId, clientIdProvider, humanVerificationListener)
|
||||
val deviceVerificationErrorHandler =
|
||||
DeviceVerificationNeededHandler<Api>(sessionId, sessionProvider, deviceVerificationListener)
|
||||
val featureDisabledErrorHandler =
|
||||
FeatureDisabledHandler<Api>(sessionId, featureDisabledListener)
|
||||
return listOf(
|
||||
dohApiHandler,
|
||||
missingScopeHandler,
|
||||
@@ -165,6 +170,7 @@ class ApiManagerFactory(
|
||||
humanVerificationInvalidHandler,
|
||||
humanVerificationNeededHandler,
|
||||
deviceVerificationErrorHandler,
|
||||
featureDisabledErrorHandler,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import me.proton.core.network.domain.client.ClientIdProvider
|
||||
import me.proton.core.network.domain.client.ClientVersionValidator
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.handlers.DohApiHandler
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
|
||||
@@ -96,6 +97,7 @@ internal class ApiManagerTests {
|
||||
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
|
||||
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
|
||||
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
|
||||
private val featureDisabledListener = mockk<FeatureDisabledListener>()
|
||||
|
||||
private lateinit var apiManagerFactory: ApiManagerFactory
|
||||
private lateinit var apiManager: ApiManager<TestRetrofitApi>
|
||||
@@ -163,6 +165,7 @@ internal class ApiManagerTests {
|
||||
deviceVerificationProvider,
|
||||
deviceVerificationListener,
|
||||
missingScopeListener,
|
||||
featureDisabledListener,
|
||||
mockk(),
|
||||
testScope,
|
||||
cache = { null },
|
||||
|
||||
@@ -47,6 +47,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator
|
||||
import me.proton.core.network.domain.client.CookieSessionId
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationDetails
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
|
||||
@@ -132,6 +133,7 @@ internal class HumanVerificationTests {
|
||||
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
|
||||
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
|
||||
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
|
||||
private val featureDisabledListener = mockk<FeatureDisabledListener>()
|
||||
private val clientVersionValidator = mockk<ClientVersionValidator> {
|
||||
every { validate(any()) } returns true
|
||||
}
|
||||
@@ -178,6 +180,7 @@ internal class HumanVerificationTests {
|
||||
deviceVerificationProvider,
|
||||
deviceVerificationListener,
|
||||
missingScopeListener,
|
||||
featureDisabledListener,
|
||||
cookieJar,
|
||||
scope,
|
||||
cache = { null },
|
||||
|
||||
@@ -58,6 +58,7 @@ import me.proton.core.network.domain.TimeoutOverride
|
||||
import me.proton.core.network.domain.client.ExtraHeaderProvider
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
|
||||
import me.proton.core.network.domain.scopes.MissingScopeListener
|
||||
@@ -117,6 +118,9 @@ internal class PinningTests {
|
||||
@BindValue
|
||||
internal val humanVerificationProvider: HumanVerificationProvider = mockk(relaxed = true)
|
||||
|
||||
@BindValue
|
||||
internal val featureDisabledListener: FeatureDisabledListener = mockk()
|
||||
|
||||
@BindValue
|
||||
internal val sessionListener: SessionListener = mockk()
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator
|
||||
import me.proton.core.network.domain.client.ExtraHeaderProvider
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationListener
|
||||
import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationDetails
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationListener
|
||||
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
|
||||
@@ -97,6 +98,7 @@ internal class ProtonApiBackendTests {
|
||||
private val deviceVerificationProvider = mockk<DeviceVerificationProvider>()
|
||||
private val deviceVerificationListener = mockk<DeviceVerificationListener>()
|
||||
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
|
||||
private val featureDisabledListener = mockk<FeatureDisabledListener>()
|
||||
private val clientVersionValidator = mockk<ClientVersionValidator> {
|
||||
every { validate(any()) } returns true
|
||||
}
|
||||
@@ -142,6 +144,7 @@ internal class ProtonApiBackendTests {
|
||||
deviceVerificationProvider,
|
||||
deviceVerificationListener,
|
||||
missingScopeListener,
|
||||
featureDisabledListener,
|
||||
cookieJar,
|
||||
scope,
|
||||
cache = { null },
|
||||
|
||||
@@ -30,6 +30,7 @@ object ResponseCodes {
|
||||
const val NOT_SAME_AS_FIELD = 2010
|
||||
const val NOT_ALLOWED = 2011
|
||||
const val BANNED = 2028
|
||||
const val FEATURE_DISABLED = 2032
|
||||
const val CURRENCY_FORMAT = 2053
|
||||
const val NOT_EXISTS = 2501
|
||||
const val APP_VERSION_BAD = 5003
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore 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.
|
||||
*
|
||||
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.network.domain.feature
|
||||
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
|
||||
interface FeatureDisabledListener {
|
||||
|
||||
suspend fun onFeatureDisabled(sessionId: SessionId?)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore 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.
|
||||
*
|
||||
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.network.domain.handlers
|
||||
|
||||
import me.proton.core.network.domain.ApiBackend
|
||||
import me.proton.core.network.domain.ApiErrorHandler
|
||||
import me.proton.core.network.domain.ApiManager
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import me.proton.core.network.domain.ResponseCodes
|
||||
import me.proton.core.network.domain.feature.FeatureDisabledListener
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
import me.proton.core.network.domain.session.SessionProvider
|
||||
|
||||
class FeatureDisabledHandler<Api>(
|
||||
private val sessionId: SessionId?,
|
||||
private val featureDisabledListener: FeatureDisabledListener,
|
||||
) : ApiErrorHandler<Api> {
|
||||
|
||||
override suspend fun <T> invoke(
|
||||
backend: ApiBackend<Api>,
|
||||
error: ApiResult.Error,
|
||||
call: ApiManager.Call<Api, T>
|
||||
): ApiResult<T> {
|
||||
if (error is ApiResult.Error.Http && error.proton?.code == ResponseCodes.FEATURE_DISABLED) {
|
||||
featureDisabledListener.onFeatureDisabled(
|
||||
sessionId = sessionId
|
||||
)
|
||||
}
|
||||
return error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user