oxtoacart/reactive_exit_node_bak
Percy Wegmann 2 months ago
parent a1e67ff1e9
commit dfd43a4195
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -8,7 +8,6 @@ import com.tailscale.ipn.ui.model.BugReportID
import com.tailscale.ipn.ui.model.Errors
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnState
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -44,7 +43,6 @@ private object Endpoint {
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
}
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
@ -53,10 +51,6 @@ typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
* corresponding method on this Client.
*/
class Client(private val scope: CoroutineScope) {
fun status(responseHandler: StatusResponseHandler) {
get(Endpoint.STATUS, responseHandler = responseHandler)
}
fun bugReportId(responseHandler: BugReportIdHandler) {
post(Endpoint.BUG_REPORT, responseHandler = responseHandler)
}

@ -1,30 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable
class Dns {
@Serializable
data class HostEntry(val addr: Addr?, val hosts: List<String>?)
@Serializable
data class OSConfig(
val hosts: List<HostEntry>? = null,
val nameservers: List<Addr>? = null,
val searchDomains: List<String>? = null,
val matchDomains: List<String>? = null,
) {
val isEmpty: Boolean
get() = (hosts.isNullOrEmpty()) &&
(nameservers.isNullOrEmpty()) &&
(searchDomains.isNullOrEmpty()) &&
(matchDomains.isNullOrEmpty())
}
}
class DnsType {
@Serializable
data class Resolver(var Addr: String? = null, var BootstrapResolution: List<Addr>? = null)
}

@ -9,13 +9,9 @@ class Ipn {
// Represents the overall state of the Tailscale engine.
enum class State(val value: Int) {
NoState(0),
InUseOtherUser(1),
NeedsLogin(2),
NeedsMachineAuth(3),
Stopped(4),
Starting(5),
Running(6);
NoState(0), InUseOtherUser(1), NeedsLogin(2), NeedsMachineAuth(3), Stopped(4), Starting(5), Running(
6
);
companion object {
fun fromInt(value: Int): State {
@ -28,20 +24,20 @@ class Ipn {
// on which NotifyWatchOpts were set when the Notifier was created.
@Serializable
data class Notify(
val Version: String? = null,
val ErrMessage: String? = null,
val LoginFinished: Empty.Message? = null,
val FilesWaiting: Empty.Message? = null,
val State: Int? = null,
var Prefs: Prefs? = null,
var NetMap: Netmap.NetworkMap? = null,
var Engine: EngineStatus? = null,
var BrowseToURL: String? = null,
var BackendLogId: String? = null,
var LocalTCPPort: Int? = null,
var IncomingFiles: List<PartialFile>? = null,
var ClientVersion: Tailcfg.ClientVersion? = null,
var TailFSShares: Map<String, String>? = null,
val Version: String? = null,
val ErrMessage: String? = null,
val LoginFinished: Empty.Message? = null,
val FilesWaiting: Empty.Message? = null,
val State: Int? = null,
var Prefs: Prefs? = null,
var NetMap: NetworkMap? = null,
var Engine: EngineStatus? = null,
var BrowseToURL: String? = null,
var BackendLogId: String? = null,
var LocalTCPPort: Int? = null,
var IncomingFiles: List<PartialFile>? = null,
// var ClientVersion: Tailcfg.ClientVersion? = null, // Currently unused
var TailFSShares: Map<String, String>? = null,
)
@Serializable
@ -65,15 +61,15 @@ class Ipn {
@Serializable
data class MaskedPrefs(
var RouteAllSet: Boolean? = null,
var CorpDNSSet: Boolean? = null,
var ExitNodeIDSet: Boolean? = null,
var ExitNodeAllowLANAccessSet: Boolean? = null,
var WantRunningSet: Boolean? = null,
var ShieldsUpSet: Boolean? = null,
var AdvertiseRoutesSet: Boolean? = null,
var ForceDaemonSet: Boolean? = null,
var HostnameSet: Boolean? = null,
var RouteAllSet: Boolean? = null,
var CorpDNSSet: Boolean? = null,
var ExitNodeIDSet: Boolean? = null,
var ExitNodeAllowLANAccessSet: Boolean? = null,
var WantRunningSet: Boolean? = null,
var ShieldsUpSet: Boolean? = null,
var AdvertiseRoutesSet: Boolean? = null,
var ForceDaemonSet: Boolean? = null,
var HostnameSet: Boolean? = null,
) {
var RouteAll: Boolean? = null
set(value) {
@ -124,39 +120,36 @@ class Ipn {
@Serializable
data class AutoUpdatePrefs(
var Check: Boolean? = null,
var Apply: Boolean? = null,
var Check: Boolean? = null,
var Apply: Boolean? = null,
)
@Serializable
data class EngineStatus(
val RBytes: Long,
val WBytes: Long,
val NumLive: Int,
val LivePeers: Map<String, IpnState.PeerStatusLite>,
val RBytes: Long,
val WBytes: Long,
val NumLive: Int,
// val LivePeers: Map<String, IpnState.PeerStatusLite>,
)
@Serializable
data class PartialFile(
val Name: String,
val Started: String,
val DeclaredSize: Long,
val Received: Long,
val PartialPath: String? = null,
var FinalPath: String? = null,
val Done: Boolean? = null,
val Name: String,
val Started: String,
val DeclaredSize: Long,
val Received: Long,
val PartialPath: String? = null,
var FinalPath: String? = null,
val Done: Boolean? = null,
)
}
class Persist {
@Serializable
data class Persist(
var PrivateMachineKey: String =
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
var PrivateNodeKey: String =
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
var OldPrivateNodeKey: String =
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
var Provider: String = "",
var PrivateMachineKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
var PrivateNodeKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
var OldPrivateNodeKey: String = "privkey:0000000000000000000000000000000000000000000000000000000000000000",
var Provider: String = "",
)
}

@ -5,114 +5,15 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable
class IpnState {
@Serializable
data class PeerStatusLite(
val RxBytes: Long,
val TxBytes: Long,
val LastHandshake: String,
val NodeKey: String,
)
@Serializable
data class PeerStatus(
val ID: StableNodeID,
val HostName: String,
val DNSName: String,
val TailscaleIPs: List<Addr>? = null,
val Tags: List<String>? = null,
val PrimaryRoutes: List<String>? = null,
val Addrs: List<String>? = null,
val Online: Boolean,
val ExitNode: Boolean,
val ExitNodeOption: Boolean,
val Active: Boolean,
val PeerAPIURL: List<String>? = null,
val Capabilities: List<String>? = null,
val SSH_HostKeys: List<String>? = null,
val ShareeNode: Boolean? = null,
val Expired: Boolean? = null,
val Location: Tailcfg.Location? = null,
) {
fun computedName(status: Status): String {
val name = DNSName
val suffix = status.CurrentTailnet?.MagicDNSSuffix
suffix ?: return name
if (!(name.endsWith("." + suffix + "."))) {
return name
}
return name.dropLast(suffix.count() + 2)
}
}
@Serializable
data class ExitNodeStatus(
val ID: StableNodeID,
val Online: Boolean,
val TailscaleIPs: List<Prefix>? = null,
)
@Serializable
data class TailnetStatus(
val Name: String,
val MagicDNSSuffix: String,
val MagicDNSEnabled: Boolean,
)
@Serializable
data class Status(
val Version: String,
val TUN: Boolean,
val BackendState: String,
val AuthURL: String,
val TailscaleIPs: List<Addr>? = null,
val Self: PeerStatus? = null,
val ExitNodeStatus: ExitNodeStatus? = null,
val Health: List<String>? = null,
val CurrentTailnet: TailnetStatus? = null,
val CertDomains: List<String>? = null,
val Peer: Map<String, PeerStatus>? = null,
val User: Map<String, Tailcfg.UserProfile>? = null,
val ClientVersion: Tailcfg.ClientVersion? = null,
)
@Serializable
data class NetworkLockStatus(
var Enabled: Boolean,
var PublicKey: String,
var NodeKey: String,
var NodeKeySigned: Boolean,
var FilteredPeers: List<TKAFilteredPeer>? = null,
var StateID: ULong? = null,
)
@Serializable
data class TKAFilteredPeer(
var Name: String,
var TailscaleIPs: List<Addr>,
var NodeKey: String,
)
@Serializable
data class PingResult(
var IP: Addr,
var Err: String,
var LatencySeconds: Double,
)
}
class IpnLocal {
@Serializable
data class LoginProfile(
var ID: String,
val Name: String,
val Key: String,
val UserProfile: Tailcfg.UserProfile,
val NetworkProfile: Tailcfg.NetworkProfile? = null,
val LocalUserID: String,
var ID: String,
val Name: String,
val Key: String,
val UserProfile: Tailcfg.UserProfile,
val NetworkProfile: Tailcfg.NetworkProfile? = null,
val LocalUserID: String,
) {
fun isEmpty(): Boolean {
return ID.isEmpty()

@ -5,51 +5,49 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable
class Netmap {
@Serializable
data class NetworkMap(
var SelfNode: Tailcfg.Node,
var NodeKey: KeyNodePublic,
var Peers: List<Tailcfg.Node>? = null,
var Expiry: Time,
var Domain: String,
var UserProfiles: Map<String, Tailcfg.UserProfile>,
var TKAEnabled: Boolean,
var DNS: Tailcfg.DNSConfig? = null
) {
// Keys are tailcfg.UserIDs thet get stringified
// Helpers
fun currentUserProfile(): Tailcfg.UserProfile? {
return userProfile(User())
}
@Serializable
data class NetworkMap(
var SelfNode: Tailcfg.Node,
// var NodeKey: KeyNodePublic, // Currently unused
var Peers: List<Tailcfg.Node>? = null,
// var Expiry: Time, // Currently unused
// var Domain: String, // Currently unused
var UserProfiles: Map<String, Tailcfg.UserProfile>,
// var TKAEnabled: Boolean, // Currently unused
// var DNS: Tailcfg.DNSConfig? = null // Currently unused
) {
// Keys are tailcfg.UserIDs thet get stringified
// Helpers
fun currentUserProfile(): Tailcfg.UserProfile? {
return userProfile(User())
}
fun User(): UserID {
return SelfNode.User
}
fun User(): UserID {
return SelfNode.User
}
fun userProfile(id: Long): Tailcfg.UserProfile? {
return UserProfiles[id.toString()]
}
fun userProfile(id: Long): Tailcfg.UserProfile? {
return UserProfiles[id.toString()]
}
fun getPeer(id: StableNodeID): Tailcfg.Node? {
if(id == SelfNode.StableID) {
return SelfNode
}
return Peers?.find { it.StableID == id }
fun getPeer(id: StableNodeID): Tailcfg.Node? {
if (id == SelfNode.StableID) {
return SelfNode
}
return Peers?.find { it.StableID == id }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NetworkMap) return false
return SelfNode == other.SelfNode &&
NodeKey == other.NodeKey &&
Peers == other.Peers &&
Expiry == other.Expiry &&
User() == other.User() &&
Domain == other.Domain &&
UserProfiles == other.UserProfiles &&
TKAEnabled == other.TKAEnabled
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NetworkMap) return false
return SelfNode == other.SelfNode &&
// NodeKey == other.NodeKey &&
Peers == other.Peers &&
// Expiry == other.Expiry &&
User() == other.User() &&
// Domain == other.Domain &&
UserProfiles == other.UserProfiles
// TKAEnabled == other.TKAEnabled
}
}

@ -6,22 +6,23 @@ package com.tailscale.ipn.ui.model
import kotlinx.serialization.Serializable
class Tailcfg {
@Serializable
data class ClientVersion(
var RunningLatest: Boolean? = null,
var LatestVersion: String? = null,
var UrgentSecurityUpdate: Boolean? = null,
var Notify: Boolean? = null,
var NotifyURL: String? = null,
var NotifyText: String? = null
)
// Currently unused
// @Serializable
// data class ClientVersion(
// var RunningLatest: Boolean? = null,
// var LatestVersion: String? = null,
// var UrgentSecurityUpdate: Boolean? = null,
// var Notify: Boolean? = null,
// var NotifyURL: String? = null,
// var NotifyText: String? = null
// )
@Serializable
data class UserProfile(
val ID: Long,
val DisplayName: String,
val LoginName: String,
val ProfilePicURL: String? = null,
val ID: Long,
val DisplayName: String,
val LoginName: String,
val ProfilePicURL: String? = null,
) {
fun isTaggedDevice(): Boolean {
return LoginName == "tagged-devices"
@ -30,74 +31,80 @@ class Tailcfg {
@Serializable
data class Hostinfo(
var IPNVersion: String? = null,
var FrontendLogID: String? = null,
var BackendLogID: String? = null,
var OS: String? = null,
var OSVersion: String? = null,
var Env: String? = null,
var Distro: String? = null,
var DistroVersion: String? = null,
var DistroCodeName: String? = null,
var Desktop: Boolean? = null,
var Package: String? = null,
var DeviceModel: String? = null,
var ShareeNode: Boolean? = null,
var Hostname: String? = null,
var ShieldsUp: Boolean? = null,
var NoLogsNoSupport: Boolean? = null,
var Machine: String? = null,
var RoutableIPs: List<Prefix>? = null,
var Services: List<Service>? = null,
var Location: Location? = null,
// var IPNVersion: String? = null, // Currently unused
// var FrontendLogID: String? = null, // Currently unused
// var BackendLogID: String? = null, // Currently unused
var OS: String? = null,
// var OSVersion: String? = null, // Currently unused
// var Env: String? = null, // Currently unused
// var Distro: String? = null, // Currently unused
// var DistroVersion: String? = null, // Currently unused
// var DistroCodeName: String? = null, // Currently unused
// var Desktop: Boolean? = null, // Currently unused
// var Package: String? = null, // Currently unused
// var DeviceModel: String? = null, // Currently unused
// var ShareeNode: Boolean? = null, // Currently unused
// var Hostname: String? = null, // Currently unused
// var ShieldsUp: Boolean? = null, // Currently unused
// var NoLogsNoSupport: Boolean? = null, // Currently unused
// var Machine: String? = null, // Currently unused
// var RoutableIPs: List<Prefix>? = null, // Currently unused
// var Services: List<Service>? = null, // Currently unused
var Location: Location? = null,
)
@Serializable
data class Node(
var ID: NodeID,
var StableID: StableNodeID,
var Name: String,
var User: UserID,
var Sharer: UserID? = null,
var Key: KeyNodePublic,
var KeyExpiry: String,
var Machine: MachineKey,
var Addresses: List<Prefix>? = null,
var AllowedIPs: List<Prefix>? = null,
var Endpoints: List<String>? = null,
var Hostinfo: Hostinfo,
var Created: Time,
var LastSeen: Time? = null,
var Online: Boolean? = null,
var Capabilities: List<String>? = null,
var ComputedName: String,
var ComputedNameWithHost: String
// var ID: NodeID, // Currently unused
var StableID: StableNodeID,
var Name: String,
var User: UserID,
// var Sharer: UserID? = null, // Currently unused
// var Key: KeyNodePublic, // Currently unused
var KeyExpiry: String,
// var Machine: MachineKey, // Currently unused
var Addresses: List<Prefix>? = null,
var AllowedIPs: List<Prefix>? = null,
// var Endpoints: List<String>? = null, // Currently unused
var Hostinfo: Hostinfo,
// var Created: Time, // Currently unused
// var LastSeen: Time? = null, // Currently unused
var Online: Boolean? = null,
var Capabilities: List<String>? = null,
var ComputedName: String,
// var ComputedNameWithHost: String // Currently unused
) {
val isAdmin: Boolean
get() = (Capabilities ?: emptyList()).contains("https://tailscale.com/cap/is-admin")
// isExitNode reproduces the Go logic in local.go peerStatusFromNode
val isExitNode: Boolean =
AllowedIPs?.contains("0.0.0.0/0") ?: false && AllowedIPs?.contains("::/0") ?: false
}
@Serializable
data class Service(var Proto: String, var Port: Int, var Description: String? = null)
// Currently unused
// @Serializable
// data class Service(var Proto: String, var Port: Int, var Description: String? = null)
@Serializable
data class NetworkProfile(var MagicDNSName: String? = null, var DomainName: String? = null)
@Serializable
data class Location(
var Country: String? = null,
var CountryCode: String? = null,
var City: String? = null,
var CityCode: String? = null,
var Priority: Int? = null
var Country: String? = null,
var CountryCode: String? = null,
var City: String? = null,
// var CityCode: String? = null, // Currently unused
var Priority: Int? = null
)
@Serializable
data class DNSConfig(
var Resolvers: List<DnsType.Resolver>? = null,
var Routes: Map<String, List<DnsType.Resolver>?>? = null,
var FallbackResolvers: List<DnsType.Resolver>? = null,
var Domains: List<String>? = null,
var Nameservers: List<Addr>? = null
)
// Currently unused
// @Serializable
// data class DNSConfig(
// var Resolvers: List<DnsType.Resolver>? = null,
// var Routes: Map<String, List<DnsType.Resolver>?>? = null,
// var FallbackResolvers: List<DnsType.Resolver>? = null,
// var Domains: List<String>? = null,
// var Nameservers: List<Addr>? = null
// )
}

@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
typealias Addr = String
typealias Prefix = String
typealias NodeID = Long
typealias KeyNodePublic = String
typealias MachineKey = String
//typealias NodeID = Long // Currently unused
//typealias KeyNodePublic = String // Currently unused
//typealias MachineKey = String // Currently unused
typealias UserID = Long
typealias Time = String
//typealias Time = String // Currently unused
typealias StableNodeID = String
typealias BugReportID = String

@ -6,7 +6,7 @@ package com.tailscale.ipn.ui.notifier
import android.util.Log
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.Notify
import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.NetworkMap
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@ -32,7 +32,7 @@ object Notifier {
private val isReady = CompletableDeferred<Boolean>()
val state: StateFlow<Ipn.State> = MutableStateFlow(Ipn.State.NoState)
val netmap: StateFlow<Netmap.NetworkMap?> = MutableStateFlow(null)
val netmap: StateFlow<NetworkMap?> = MutableStateFlow(null)
val prefs: StateFlow<Ipn.Prefs?> = MutableStateFlow(null)
val engineStatus: StateFlow<Ipn.EngineStatus?> = MutableStateFlow(null)
val tailFSShares: StateFlow<Map<String, String>?> = MutableStateFlow(null)

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.util
import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.NetworkMap
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.notifier.Notifier
@ -37,7 +37,7 @@ class PeerCategorizer(scope: CoroutineScope) {
}
}
private fun regenerateGroupedPeers(netmap: Netmap.NetworkMap): List<PeerSet> {
private fun regenerateGroupedPeers(netmap: NetworkMap): List<PeerSet> {
val peers: List<Tailcfg.Node> = netmap.Peers ?: return emptyList()
val selfNode = netmap.SelfNode
var grouped = mutableMapOf<UserID, MutableList<Tailcfg.Node>>()

@ -4,17 +4,20 @@
package com.tailscale.ipn.ui.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.TreeMap
data class ExitNodePickerNav(
@ -52,66 +55,66 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
val anyActive: StateFlow<Boolean> = MutableStateFlow(false)
init {
Client(viewModelScope).status { result ->
result.onFailure {
Log.e(TAG, "getStatus: ${it.message}")
}.onSuccess {
it.Peer?.values?.let { peers ->
val allNodes = peers.filter { it.ExitNodeOption }.map {
ExitNode(
id = it.ID,
label = it.DNSName,
online = it.Online,
selected = it.ExitNode,
mullvad = it.DNSName.endsWith(".mullvad.ts.net."),
priority = it.Location?.Priority ?: 0,
countryCode = it.Location?.CountryCode ?: "",
country = it.Location?.Country ?: "",
city = it.Location?.City ?: "",
)
}
viewModelScope.launch {
Notifier.netmap.combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) }
.stateIn(viewModelScope).collect { (netmap, prefs) ->
val exitNodeId = prefs?.ExitNodeID
netmap?.Peers?.let { peers ->
val allNodes = peers.filter { it.isExitNode }.map {
ExitNode(
id = it.StableID,
label = it.Name,
online = it.Online ?: false,
selected = it.StableID == exitNodeId,
mullvad = it.Name.endsWith(".mullvad.ts.net."),
priority = it.Hostinfo?.Location?.Priority ?: 0,
countryCode = it.Hostinfo?.Location?.CountryCode ?: "",
country = it.Hostinfo?.Location?.Country ?: "",
city = it.Hostinfo?.Location?.City ?: "",
)
}
val tailnetNodes = allNodes.filter { !it.mullvad }
tailnetExitNodes.set(tailnetNodes.sortedWith { a, b ->
a.label.compareTo(
b.label
)
})
val tailnetNodes = allNodes.filter { !it.mullvad }
tailnetExitNodes.set(tailnetNodes.sortedWith { a, b ->
a.label.compareTo(
b.label
)
})
val mullvadExitNodes = allNodes.filter {
// Pick all mullvad nodes that are online or the currently selected
it.mullvad && (it.selected || it.online)
}.groupBy {
// Group by countryCode
it.countryCode
}.mapValues { (_, nodes) ->
// Group by city
nodes.groupBy {
it.city
val mullvadExitNodes = allNodes.filter {
// Pick all mullvad nodes that are online or the currently selected
it.mullvad && (it.selected || it.online)
}.groupBy {
// Group by countryCode
it.countryCode
}.mapValues { (_, nodes) ->
// Pick one node per city, either the selected one or the best
// available
nodes.sortedWith { a, b ->
if (a.selected && !b.selected) {
-1
} else if (b.selected && !a.selected) {
1
} else {
b.priority.compareTo(a.priority)
}
}.first()
}.values.sortedBy { it.city.lowercase() }
}
mullvadExitNodesByCountryCode.set(mullvadExitNodes)
// Group by city
nodes.groupBy {
it.city
}.mapValues { (_, nodes) ->
// Pick one node per city, either the selected one or the best
// available
nodes.sortedWith { a, b ->
if (a.selected && !b.selected) {
-1
} else if (b.selected && !a.selected) {
1
} else {
b.priority.compareTo(a.priority)
}
}.first()
}.values.sortedBy { it.city.lowercase() }
}
mullvadExitNodesByCountryCode.set(mullvadExitNodes)
val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) ->
nodes.minByOrNull { -1 * it.priority }!!
}
mullvadBestAvailableByCountry.set(bestAvailableByCountry)
val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) ->
nodes.minByOrNull { -1 * it.priority }!!
}
mullvadBestAvailableByCountry.set(bestAvailableByCountry)
anyActive.set(allNodes.any { it.selected })
anyActive.set(allNodes.any { it.selected })
}
}
}
}
}

Loading…
Cancel
Save