mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-29 22:24:29 +02:00
WIP SubsExchange
This commit is contained in:
parent
7bd687331b
commit
034b8b15ae
@ -294,6 +294,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||||
|
var useSubscriptionExchange: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@ -18,6 +19,7 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
|||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
@ -67,10 +69,24 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
private val _subsExchangeServer = "https://exchange.grayjay.app/";
|
||||||
|
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
|
||||||
|
|
||||||
init {
|
init {
|
||||||
global.onUpdateProgress.subscribe { progress, total ->
|
global.onUpdateProgress.subscribe { progress, total ->
|
||||||
onFeedProgress.emit(null, progress, total);
|
onFeedProgress.emit(null, progress, total);
|
||||||
}
|
}
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
generateNewSubsExchangeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateNewSubsExchangeKey(){
|
||||||
|
_subscriptionKey.setAndSave(SubsExchangeClient.createPrivateKey());
|
||||||
|
}
|
||||||
|
fun getSubsExchangeClient(): SubsExchangeClient {
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
throw IllegalStateException("No valid subscription exchange key set");
|
||||||
|
return SubsExchangeClient(_subsExchangeServer, _subscriptionKey.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOldestUpdateTime(): OffsetDateTime {
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
@ -359,7 +375,8 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
val exchangeClient = if(Settings.instance.subscriptions.useSubscriptionExchange) getSubsExchangeClient() else null;
|
||||||
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool, exchangeClient);
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
@ -15,8 +16,9 @@ class SmartSubscriptionAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
threadPool: ForkJoinPool? = null
|
threadPool: ForkJoinPool? = null,
|
||||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
subsExchangeClient: SubsExchangeClient? = null
|
||||||
|
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool, subsExchangeClient) {
|
||||||
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
||||||
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
||||||
val sub = entry.key;
|
val sub = entry.key;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@ -33,11 +34,11 @@ abstract class SubscriptionFetchAlgorithm(
|
|||||||
companion object {
|
companion object {
|
||||||
public val TAG = "SubscriptionAlgorithm";
|
public val TAG = "SubscriptionAlgorithm";
|
||||||
|
|
||||||
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
|
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null, withExchangeClient: SubsExchangeClient? = null): SubscriptionFetchAlgorithm {
|
||||||
return when(algo) {
|
return when(algo) {
|
||||||
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
||||||
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, withExchangeClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
@ -10,6 +11,7 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
|
|||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
@ -24,6 +26,8 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
@ -35,7 +39,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
_threadPool: ForkJoinPool? = null
|
_threadPool: ForkJoinPool? = null,
|
||||||
|
private val subsExchangeClient: SubsExchangeClient? = null
|
||||||
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
val tasks = getSubscriptionTasks(subs);
|
var tasks = getSubscriptionTasks(subs).toMutableList()
|
||||||
|
|
||||||
val tasksGrouped = tasks.groupBy { it.client }
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
|
||||||
@ -70,6 +75,21 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val liveTasks = tasks.filter { !it.fromPeek && !it.fromCache };
|
||||||
|
val contract = subsExchangeClient?.requestContract(*liveTasks.map { ChannelRequest(it.url) }.toTypedArray());
|
||||||
|
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||||
|
if(contract != null && contract.provided.size > 0){
|
||||||
|
providedTasks = mutableListOf()
|
||||||
|
for(task in tasks.toList()){
|
||||||
|
if(!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
|
||||||
|
providedTasks.add(task);
|
||||||
|
tasks.remove(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val failedPlugins = mutableListOf<String>();
|
val failedPlugins = mutableListOf<String>();
|
||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
@ -104,6 +124,39 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Resolve Subscription Exchange
|
||||||
|
if(contract != null) {
|
||||||
|
try {
|
||||||
|
val resolve = subsExchangeClient?.resolveContract(
|
||||||
|
contract,
|
||||||
|
*taskResults.filter { it.pager != null }.map {
|
||||||
|
ChannelResolve(
|
||||||
|
it.task.url,
|
||||||
|
it.pager!!.getResults()
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
);
|
||||||
|
if (resolve != null) {
|
||||||
|
for(result in resolve){
|
||||||
|
val task = providedTasks?.find { it.url == result.channelUrl };
|
||||||
|
if(task != null) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
|
||||||
|
providedTasks?.remove(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (providedTasks != null) {
|
||||||
|
for(task in providedTasks) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
//TODO: fetch remainder after all?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
||||||
|
|
||||||
//Cache pagers grouped by channel
|
//Cache pagers grouped by channel
|
||||||
@ -173,6 +226,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Intercepts task.fromCache & task.fromPeek
|
||||||
synchronized(cachedChannels) {
|
synchronized(cachedChannels) {
|
||||||
if(task.fromCache || task.fromPeek) {
|
if(task.fromCache || task.fromPeek) {
|
||||||
finished++;
|
finished++;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelRequest(
|
||||||
|
var url: String
|
||||||
|
);
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelResolve(
|
||||||
|
var channelUrl: String,
|
||||||
|
var content: List<IPlatformContent>,
|
||||||
|
var channel: IPlatformChannel? = null
|
||||||
|
)
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelResult(
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var dateTime: OffsetDateTime,
|
||||||
|
var channelUrl: String,
|
||||||
|
var content: List<IPlatformContent>,
|
||||||
|
var channel: IPlatformChannel? = null
|
||||||
|
)
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ExchangeContract(
|
||||||
|
var id: String,
|
||||||
|
var requests: List<ChannelRequest>,
|
||||||
|
var provided: List<String> = listOf(),
|
||||||
|
var required: List<String> = listOf(),
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var expired: OffsetDateTime = OffsetDateTime.MIN,
|
||||||
|
var contractVersion: Int = 1
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExchangeContractResolve(
|
||||||
|
val publicKey: String,
|
||||||
|
val signature: String,
|
||||||
|
val data: String
|
||||||
|
)
|
@ -0,0 +1,118 @@
|
|||||||
|
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResult
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContractResolve
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.util.Base64
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
|
||||||
|
|
||||||
|
class SubsExchangeClient(private val server: String, private val privateKey: String) {
|
||||||
|
|
||||||
|
private val publicKey: String = extractPublicKey(privateKey)
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
|
||||||
|
// Endpoint: Contract
|
||||||
|
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
|
||||||
|
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||||
|
return Json.decodeFromString(data)
|
||||||
|
}
|
||||||
|
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
|
||||||
|
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||||
|
return Json.decodeFromString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint: Resolve
|
||||||
|
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||||
|
val contractResolve = convertResolves(*resolves)
|
||||||
|
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Json.encodeToString(contractResolve), "application/json")
|
||||||
|
return Json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||||
|
val contractResolve = convertResolves(*resolves)
|
||||||
|
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Json.encodeToString(contractResolve), "application/json")
|
||||||
|
return Json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun convertResolves(vararg resolves: ChannelResolve): ExchangeContractResolve {
|
||||||
|
val data = Json.encodeToString(resolves)
|
||||||
|
val signature = createSignature(data, privateKey)
|
||||||
|
|
||||||
|
return ExchangeContractResolve(
|
||||||
|
publicKey = publicKey,
|
||||||
|
signature = signature,
|
||||||
|
data = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO methods
|
||||||
|
private fun post(query: String, body: String, contentType: String): String {
|
||||||
|
val url = URL("$server$query")
|
||||||
|
with(url.openConnection() as HttpURLConnection) {
|
||||||
|
requestMethod = "POST"
|
||||||
|
setRequestProperty("Content-Type", contentType)
|
||||||
|
doOutput = true
|
||||||
|
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body) }
|
||||||
|
|
||||||
|
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
||||||
|
return it.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun postAsync(query: String, body: String, contentType: String): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
post(query, body, contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crypto methods
|
||||||
|
companion object {
|
||||||
|
fun createPrivateKey(): String {
|
||||||
|
val rsa = KeyFactory.getInstance("RSA")
|
||||||
|
val keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
val keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
return Base64.getEncoder().encodeToString(keyPair.private.encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractPublicKey(privateKey: String): String {
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||||
|
val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
val privateKeyObj = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||||
|
val publicKeyObj: RSAPublicKey = keyFactory.generatePublic(keySpec) as RSAPublicKey;
|
||||||
|
return Base64.getEncoder().encodeToString(publicKeyObj.encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSignature(data: String, privateKey: String): String {
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||||
|
val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
val rsaPrivateKey = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||||
|
|
||||||
|
val signature = Signature.getInstance("SHA256withRSA")
|
||||||
|
signature.initSign(rsaPrivateKey)
|
||||||
|
signature.update(data.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
|
val signatureBytes = signature.sign()
|
||||||
|
return Base64.getEncoder().encodeToString(signatureBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -168,7 +168,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:hint="Seach.." />
|
android:hint="Search.." />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -412,6 +412,8 @@
|
|||||||
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||||
<string name="subscription_group_menu">Groups</string>
|
<string name="subscription_group_menu">Groups</string>
|
||||||
<string name="show_subscription_group">Show Subscription Groups</string>
|
<string name="show_subscription_group">Show Subscription Groups</string>
|
||||||
|
<string name="use_subscription_exchange">Use Subscription Exchange (Experimental)</string>
|
||||||
|
<string name="use_subscription_exchange_description">Uses a centralized crowd-sourced server to significantly reduce the required requests for subscriptions, in exchange you submit your subscriptions to the server.</string>
|
||||||
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
|
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
|
||||||
<string name="preview_feed_items">Preview Feed Items</string>
|
<string name="preview_feed_items">Preview Feed Items</string>
|
||||||
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user