/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai
*
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.group
import androidx.core.net.toUri
import com.google.gson.JsonObject
import io.nekohasekai.sagernet.ExtraType
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.database.*
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocksConfig
import io.nekohasekai.sagernet.ktx.*
import libexclavecore.Libexclavecore
object SIP008Updater : GroupUpdater() {
override suspend fun doUpdate(
proxyGroup: ProxyGroup,
subscription: SubscriptionBean,
userInterface: GroupManager.Interface?,
byUser: Boolean
) {
val link = subscription.link
val sip008Response: JsonObject
if (link.startsWith("content://", ignoreCase = true)) {
val contentText = app.contentResolver.openInputStream(link.toUri())
?.bufferedReader()
?.readText()
sip008Response = contentText?.let { parseJson(contentText).asJsonObject }
?: error(app.getString(R.string.no_proxies_found_in_subscription))
} else {
val response = Libexclavecore.newHttpClient().apply {
if (SagerNet.started && DataStore.startedProfile > 0) {
useUDS(SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock")
}
}.newRequest().apply {
setURL(subscription.link)
if (subscription.customUserAgent.isNotEmpty()) {
setUserAgent(subscription.customUserAgent)
} else {
setUserAgent(USER_AGENT)
if (subscription.httpHeaders.isNotEmpty()) {
for (header in subscription.httpHeaders.replace("\r\n", "\n").split("\n")) {
if (header.isEmpty()) continue
if (!header.contains(":")) error("invalid http header")
setHeader(header.substringBefore(":"), header.substringAfter(":").trimStart())
}
}
}
}.execute()
sip008Response = parseJson(response.contentString).asJsonObject
}
subscription.bytesUsed = sip008Response.getLong("bytes_used") ?: -1
subscription.bytesRemaining = sip008Response.getLong("bytes_remaining") ?: -1
subscription.applyDefaultValues()
val servers = sip008Response.getArray("servers")
var profiles = mutableListOf()
if (servers != null) {
for (profile in servers) {
val bean = parseShadowsocksConfig(profile) ?: continue
appendExtraInfo(profile, bean)
profiles.add(bean)
}
}
profiles.forEach { it.applyDefaultValues() }
if (subscription.nameFilter.isNotEmpty()) {
val pattern = Regex(subscription.nameFilter)
profiles = profiles.filter { !pattern.containsMatchIn(it.name) }.toMutableList()
}
if (subscription.nameFilter1.isNotEmpty()) {
val pattern = Regex(subscription.nameFilter1)
profiles = profiles.filter { pattern.containsMatchIn(it.name) }.toMutableList()
}
val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
val duplicate = ArrayList()
if (subscription.deduplication) {
val uniqueProfiles = LinkedHashSet()
val uniqueNames = HashMap()
for (proxy in profiles) {
if (!uniqueProfiles.add(proxy)) {
val index = uniqueProfiles.indexOf(proxy)
if (uniqueNames.containsKey(proxy)) {
val name = uniqueNames[proxy]!!.replace(" ($index)", "")
if (name.isNotEmpty()) {
duplicate.add("$name ($index)")
uniqueNames[proxy] = ""
}
}
duplicate.add(proxy.displayName() + " ($index)")
} else {
uniqueNames[proxy] = proxy.displayName()
}
}
uniqueProfiles.retainAll(uniqueNames.keys)
profiles = uniqueProfiles.toMutableList()
}
val profileMap = profiles.associateBy { it.profileId }
val toDelete = ArrayList()
val toReplace = exists.mapNotNull { entity ->
val profileId = entity.requireBean().profileId
if (profileMap.contains(profileId)) profileId to entity else let {
toDelete.add(entity)
null
}
}.toMap()
val toUpdate = ArrayList()
val added = mutableListOf()
val updated = mutableMapOf()
val deleted = toDelete.map { it.displayName() }
var userOrder = 1L
var changed = toDelete.size
for ((profileId, bean) in profileMap.entries) {
val name = bean.displayName()
if (toReplace.contains(profileId)) {
val entity = toReplace[profileId]!!
val existsBean = entity.requireBean()
existsBean.applyFeatureSettings(bean)
when {
existsBean != bean -> {
changed++
entity.putBean(bean)
toUpdate.add(entity)
updated[entity.displayName()] = name
}
entity.userOrder != userOrder -> {
entity.putBean(bean)
toUpdate.add(entity)
entity.userOrder = userOrder
}
}
} else {
changed++
SagerDatabase.proxyDao.addProxy(ProxyEntity(
groupId = proxyGroup.id, userOrder = userOrder
).apply {
putBean(bean)
})
added.add(name)
}
userOrder++
}
SagerDatabase.proxyDao.updateProxy(toUpdate)
SagerDatabase.proxyDao.deleteProxy(toDelete)
subscription.lastUpdated = System.currentTimeMillis() / 1000
SagerDatabase.groupDao.updateGroup(proxyGroup)
finishUpdate(proxyGroup)
if (byUser && userInterface != null) {
userInterface.onUpdateSuccess(proxyGroup, changed, added, updated, deleted, duplicate)
}
}
fun appendExtraInfo(profile: JsonObject, bean: AbstractBean) {
bean.extraType = ExtraType.SIP008
bean.profileId = profile.getString("id")
}
}