mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI.git
synced 2024-12-23 00:57:13 +00:00
improve class casts including filtering
This commit is contained in:
parent
6f60ecbf2e
commit
f153b31464
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kotlin = "1.8.21"
|
kotlin = "1.8.22"
|
||||||
kotlin-serialization = "1.5.1"
|
kotlin-serialization = "1.5.1"
|
||||||
kotlin-coroutines = "1.6.4"
|
kotlin-coroutines = "1.6.4"
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ korlibs = "4.0.3"
|
|||||||
uuid = "0.7.1"
|
uuid = "0.7.1"
|
||||||
ktor = "2.3.1"
|
ktor = "2.3.1"
|
||||||
|
|
||||||
ksp = "1.8.21-1.0.11"
|
ksp = "1.8.22-1.0.11"
|
||||||
kotlin-poet = "1.14.2"
|
kotlin-poet = "1.14.2"
|
||||||
|
|
||||||
microutils = "0.19.2"
|
microutils = "0.19.2"
|
||||||
|
@ -8,7 +8,7 @@ import dev.inmo.tgbotapi.types.chat.User
|
|||||||
*
|
*
|
||||||
* @see FromUser
|
* @see FromUser
|
||||||
*/
|
*/
|
||||||
@ClassCastsIncluded
|
@ClassCastsIncluded(excludeRegex = ".*Impl")
|
||||||
interface WithUser {
|
interface WithUser {
|
||||||
val user: User
|
val user: User
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable(InlineQueryResultSerializer::class)
|
@Serializable(InlineQueryResultSerializer::class)
|
||||||
@ClassCastsIncluded
|
@ClassCastsIncluded(excludeRegex = ".*Impl")
|
||||||
interface InlineQueryResult {
|
interface InlineQueryResult {
|
||||||
val type: String
|
val type: String
|
||||||
val id: InlineQueryIdentifier
|
val id: InlineQueryIdentifier
|
||||||
|
@ -49,7 +49,7 @@ sealed interface AbleToAddInAttachmentMenuChat : Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable(PreviewChatSerializer::class)
|
@Serializable(PreviewChatSerializer::class)
|
||||||
@ClassCastsIncluded(excludeRegex = ".*Impl.kt")
|
@ClassCastsIncluded(excludeRegex = ".*Impl")
|
||||||
sealed interface Chat {
|
sealed interface Chat {
|
||||||
val id: IdChatIdentifier
|
val id: IdChatIdentifier
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import dev.inmo.tgbotapi.types.*
|
|||||||
import dev.inmo.tgbotapi.types.message.abstracts.Message
|
import dev.inmo.tgbotapi.types.message.abstracts.Message
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializer
|
import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializer
|
||||||
import dev.inmo.tgbotapi.utils.RiskFeature
|
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ExtendedChannelChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ExtendedChannelChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ExtendedChannelChatImpl(
|
data class ExtendedChannelChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
@ -36,6 +38,7 @@ data class ExtendedChannelChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ExtendedGroupChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ExtendedGroupChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ExtendedGroupChatImpl(
|
data class ExtendedGroupChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
@ -58,6 +61,7 @@ data class ExtendedGroupChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ExtendedPrivateChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ExtendedPrivateChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ExtendedPrivateChatImpl(
|
data class ExtendedPrivateChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: UserId,
|
override val id: UserId,
|
||||||
@ -85,6 +89,7 @@ typealias ExtendedUser = ExtendedPrivateChatImpl
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ExtendedSupergroupChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ExtendedSupergroupChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ExtendedSupergroupChatImpl(
|
data class ExtendedSupergroupChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
@ -127,6 +132,7 @@ data class ExtendedSupergroupChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ExtendedForumChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ExtendedForumChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ExtendedForumChatImpl(
|
data class ExtendedForumChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: IdChatIdentifier,
|
override val id: IdChatIdentifier,
|
||||||
|
@ -5,11 +5,13 @@ import dev.inmo.micro_utils.language_codes.IetfLanguageCodeSerializer
|
|||||||
import dev.inmo.tgbotapi.types.*
|
import dev.inmo.tgbotapi.types.*
|
||||||
import dev.inmo.tgbotapi.types.abstracts.WithOptionalLanguageCode
|
import dev.inmo.tgbotapi.types.abstracts.WithOptionalLanguageCode
|
||||||
import dev.inmo.tgbotapi.utils.RiskFeature
|
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use GroupChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use GroupChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class GroupChatImpl(
|
data class GroupChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
@ -19,6 +21,7 @@ data class GroupChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use PrivateChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use PrivateChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class PrivateChatImpl(
|
data class PrivateChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: UserId,
|
override val id: UserId,
|
||||||
@ -32,6 +35,7 @@ data class PrivateChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use SupergroupChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use SupergroupChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class SupergroupChatImpl(
|
data class SupergroupChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
@ -43,6 +47,7 @@ data class SupergroupChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ForumChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ForumChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ForumChatImpl(
|
data class ForumChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: IdChatIdentifier,
|
override val id: IdChatIdentifier,
|
||||||
@ -54,6 +59,7 @@ data class ForumChatImpl(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@RiskFeature("This class is a subject of changes. It is better to use ChannelChat due")
|
@RiskFeature("This class is a subject of changes. It is better to use ChannelChat due")
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ChannelChatImpl(
|
data class ChannelChatImpl(
|
||||||
@SerialName(idField)
|
@SerialName(idField)
|
||||||
override val id: ChatId,
|
override val id: ChatId,
|
||||||
|
@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
|
|||||||
|
|
||||||
import dev.inmo.tgbotapi.types.*
|
import dev.inmo.tgbotapi.types.*
|
||||||
import dev.inmo.tgbotapi.types.chat.User
|
import dev.inmo.tgbotapi.types.chat.User
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ClassCastsExcluded
|
||||||
data class AdministratorChatMemberImpl(
|
data class AdministratorChatMemberImpl(
|
||||||
@SerialName(userField)
|
@SerialName(userField)
|
||||||
override val user: User,
|
override val user: User,
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package dev.inmo.tgbotapi.types.chat.member
|
package dev.inmo.tgbotapi.types.chat.member
|
||||||
|
|
||||||
import dev.inmo.tgbotapi.types.*
|
import dev.inmo.tgbotapi.types.*
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ClassCastsExcluded
|
||||||
data class ChatAdministratorRightsImpl(
|
data class ChatAdministratorRightsImpl(
|
||||||
@SerialName(canChangeInfoField)
|
@SerialName(canChangeInfoField)
|
||||||
override val canChangeInfo: Boolean = false,
|
override val canChangeInfo: Boolean = false,
|
||||||
|
@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
|
|||||||
|
|
||||||
import dev.inmo.tgbotapi.types.*
|
import dev.inmo.tgbotapi.types.*
|
||||||
import dev.inmo.tgbotapi.types.chat.User
|
import dev.inmo.tgbotapi.types.chat.User
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ClassCastsExcluded
|
||||||
data class LeftChatMemberImpl(@SerialName(userField) override val user: User) : LeftChatMember {
|
data class LeftChatMemberImpl(@SerialName(userField) override val user: User) : LeftChatMember {
|
||||||
@SerialName(statusField)
|
@SerialName(statusField)
|
||||||
@Required
|
@Required
|
||||||
|
@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
|
|||||||
|
|
||||||
import dev.inmo.tgbotapi.types.*
|
import dev.inmo.tgbotapi.types.*
|
||||||
import dev.inmo.tgbotapi.types.chat.User
|
import dev.inmo.tgbotapi.types.chat.User
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ClassCastsExcluded
|
||||||
data class MemberChatMemberImpl(@SerialName(userField) override val user: User) : MemberChatMember {
|
data class MemberChatMemberImpl(@SerialName(userField) override val user: User) : MemberChatMember {
|
||||||
@SerialName(statusField)
|
@SerialName(statusField)
|
||||||
@Required
|
@Required
|
||||||
|
@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.*
|
|||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
@ClassCastsIncluded
|
@ClassCastsIncluded(excludeRegex = ".*Impl")
|
||||||
interface Message : WithChat {
|
interface Message : WithChat {
|
||||||
val messageId: MessageId
|
val messageId: MessageId
|
||||||
val date: DateTime
|
val date: DateTime
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
package dev.inmo.tgbotapi.utils.internal
|
package dev.inmo.tgbotapi.utils.internal
|
||||||
|
|
||||||
|
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||||
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
internal annotation class ClassCastsIncluded(val typesRegex: String = ".*", val excludeRegex: String = "")
|
@RiskFeature("It is internal API in tgbotapi.core and should not be used outside")
|
||||||
|
annotation class ClassCastsIncluded(val typesRegex: String = "", val excludeRegex: String = "")
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@RiskFeature("It is internal API in tgbotapi.core and should not be used outside")
|
||||||
|
annotation class ClassCastsExcluded()
|
||||||
|
@ -9,4 +9,5 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.kotlin.poet
|
implementation libs.kotlin.poet
|
||||||
implementation libs.ksp
|
implementation libs.ksp
|
||||||
|
implementation project(":tgbotapi.core")
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
package dev.inmo.tgbotapi.ksp.processor
|
package dev.inmo.tgbotapi.ksp.processor
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.KspExperimental
|
||||||
import com.google.devtools.ksp.getAllSuperTypes
|
import com.google.devtools.ksp.getAllSuperTypes
|
||||||
import com.google.devtools.ksp.getAnnotationsByType
|
import com.google.devtools.ksp.getAnnotationsByType
|
||||||
|
import com.google.devtools.ksp.isAnnotationPresent
|
||||||
import com.google.devtools.ksp.processing.*
|
import com.google.devtools.ksp.processing.*
|
||||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
import com.squareup.kotlinpoet.AnnotationSpec
|
import com.squareup.kotlinpoet.AnnotationSpec
|
||||||
import com.squareup.kotlinpoet.ClassName
|
import com.squareup.kotlinpoet.ClassName
|
||||||
import com.squareup.kotlinpoet.FileSpec
|
import com.squareup.kotlinpoet.FileSpec
|
||||||
|
import com.squareup.kotlinpoet.asClassName
|
||||||
import com.squareup.kotlinpoet.ksp.toClassName
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
import com.squareup.kotlinpoet.ksp.writeTo
|
import com.squareup.kotlinpoet.ksp.writeTo
|
||||||
|
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
|
||||||
|
import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class TelegramBotAPISymbolProcessor(
|
class TelegramBotAPISymbolProcessor(
|
||||||
@ -18,35 +24,33 @@ class TelegramBotAPISymbolProcessor(
|
|||||||
private val outputFile: String = "Output",
|
private val outputFile: String = "Output",
|
||||||
private val outputFolder: String? = null
|
private val outputFolder: String? = null
|
||||||
) : SymbolProcessor {
|
) : SymbolProcessor {
|
||||||
private val classCastsIncludedClassName = ClassName("dev.inmo.tgbotapi.utils.internal", "ClassCastsIncluded")
|
private val classCastsIncludedClassName = ClassCastsIncluded::class.asClassName()
|
||||||
|
@OptIn(KspExperimental::class, RiskFeature::class)
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
val classes = resolver.getSymbolsWithAnnotation(classCastsIncludedClassName.canonicalName).filterIsInstance<KSClassDeclaration>()
|
val classes = resolver.getSymbolsWithAnnotation(classCastsIncludedClassName.canonicalName).filterIsInstance<KSClassDeclaration>()
|
||||||
val classesRegexes: Map<KSClassDeclaration, Pair<Regex, Regex?>> = classes.mapNotNull {
|
val classesRegexes: Map<KSClassDeclaration, Pair<Regex?, Regex?>> = classes.mapNotNull {
|
||||||
it to (it.annotations.firstNotNullOfOrNull {
|
it to (it.getAnnotationsByType(ClassCastsIncluded::class).firstNotNullOfOrNull {
|
||||||
runCatching {
|
it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex)
|
||||||
if (it.annotationType.resolve().toClassName() == classCastsIncludedClassName) {
|
|
||||||
val regex = it.arguments.first().value as? String ?: return@runCatching null
|
|
||||||
val negativeRegex = (it.arguments.first().value as? String) ?.takeIf { it.isNotEmpty() }
|
|
||||||
Regex(regex) to (negativeRegex ?.let(::Regex))
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
} ?: return@mapNotNull null)
|
} ?: return@mapNotNull null)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
|
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
|
||||||
|
|
||||||
resolver.getAllFiles().forEach {
|
resolver.getAllFiles().forEach {
|
||||||
it.declarations.forEach { potentialSubtype ->
|
it.declarations.forEach { potentialSubtype ->
|
||||||
if (potentialSubtype is KSClassDeclaration) {
|
if (
|
||||||
|
potentialSubtype is KSClassDeclaration
|
||||||
|
&& potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class).not()
|
||||||
|
) {
|
||||||
val allSupertypes = potentialSubtype.getAllSuperTypes().map { it.declaration }
|
val allSupertypes = potentialSubtype.getAllSuperTypes().map { it.declaration }
|
||||||
|
|
||||||
for (currentClass in classes) {
|
for (currentClass in classes) {
|
||||||
val regexes = classesRegexes[currentClass]
|
val regexes = classesRegexes[currentClass]
|
||||||
|
val simpleName = potentialSubtype.simpleName.getShortName()
|
||||||
when {
|
when {
|
||||||
currentClass in allSupertypes
|
currentClass !in allSupertypes
|
||||||
&& regexes ?.first ?.matches(potentialSubtype.simpleName.toString()) != false
|
|| regexes ?.first ?.matches(simpleName) == false
|
||||||
&& regexes ?.second ?.matches(potentialSubtype.simpleName.toString()) != true-> {
|
|| regexes ?.second ?.matches(simpleName) == true -> continue
|
||||||
|
else -> {
|
||||||
classesSubtypes.getOrPut(currentClass) { mutableSetOf() }.add(potentialSubtype)
|
classesSubtypes.getOrPut(currentClass) { mutableSetOf() }.add(potentialSubtype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,7 +59,16 @@ class TelegramBotAPISymbolProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun fillWithSealeds(source: KSClassDeclaration, current: KSClassDeclaration = source) {
|
fun fillWithSealeds(source: KSClassDeclaration, current: KSClassDeclaration = source) {
|
||||||
|
val regexes = classesRegexes[source]
|
||||||
current.getSealedSubclasses().forEach {
|
current.getSealedSubclasses().forEach {
|
||||||
|
val simpleName = it.simpleName.getShortName()
|
||||||
|
if (
|
||||||
|
regexes ?.first ?.matches(simpleName) == false
|
||||||
|
|| regexes ?.second ?.matches(simpleName) == true
|
||||||
|
|| it.isAnnotationPresent(ClassCastsExcluded::class)
|
||||||
|
) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
classesSubtypes.getOrPut(source) { mutableSetOf() }.add(it)
|
classesSubtypes.getOrPut(source) { mutableSetOf() }.add(it)
|
||||||
fillWithSealeds(source, it)
|
fillWithSealeds(source, it)
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user