mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-11 02:50:15 +08:00
Merge branch 'master' into command
# Conflicts: # backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt
This commit is contained in:
commit
f80c4b3fd1
@ -40,6 +40,8 @@ public annotation class ResolveContext(
|
|||||||
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION
|
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION
|
||||||
PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION
|
PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION
|
||||||
|
|
||||||
|
VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT // TODO
|
||||||
|
|
||||||
COMMAND_NAME, // ILLEGAL_COMMAND_NAME
|
COMMAND_NAME, // ILLEGAL_COMMAND_NAME
|
||||||
|
|
||||||
PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE
|
PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE
|
||||||
|
@ -182,7 +182,7 @@ internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update:
|
|||||||
while (true) {
|
while (true) {
|
||||||
val current = value
|
val current = value
|
||||||
if (condition(current)) {
|
if (condition(current)) {
|
||||||
if (compareAndSet(0, update(current))) {
|
if (compareAndSet(current, update(current))) {
|
||||||
return true
|
return true
|
||||||
} else continue
|
} else continue
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,10 @@ public data class PermissionId(
|
|||||||
@ResolveContext(PERMISSION_NAME) public val name: String,
|
@ResolveContext(PERMISSION_NAME) public val name: String,
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(!namespace.contains(' ')) {
|
require(namespace.none { it.isWhitespace() }) {
|
||||||
"' ' is not allowed in namespace"
|
"' ' is not allowed in namespace"
|
||||||
}
|
}
|
||||||
require(!name.contains(' ')) {
|
require(name.none { it.isWhitespace() }) {
|
||||||
"' ' is not allowed in name"
|
"' ' is not allowed in name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import net.mamoe.mirai.console.extensions.PermissionServiceProvider
|
|||||||
import net.mamoe.mirai.console.internal.permission.checkType
|
import net.mamoe.mirai.console.internal.permission.checkType
|
||||||
import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf
|
import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf
|
||||||
import net.mamoe.mirai.console.plugin.Plugin
|
import net.mamoe.mirai.console.plugin.Plugin
|
||||||
|
import net.mamoe.mirai.console.plugin.description
|
||||||
import net.mamoe.mirai.console.plugin.name
|
import net.mamoe.mirai.console.plugin.name
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -154,7 +155,7 @@ public interface PermissionService<P : Permission> {
|
|||||||
@ResolveContext(COMMAND_NAME) permissionName: String,
|
@ResolveContext(COMMAND_NAME) permissionName: String,
|
||||||
reason: PluginPermissionIdRequestType
|
reason: PluginPermissionIdRequestType
|
||||||
) = PermissionId(
|
) = PermissionId(
|
||||||
plugin.name.toLowerCase().replace(' ', '.'),
|
plugin.description.id.toLowerCase(),
|
||||||
permissionName.toLowerCase()
|
permissionName.toLowerCase()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ public interface JvmPluginDescription : PluginDescription {
|
|||||||
*/
|
*/
|
||||||
@JvmName("create")
|
@JvmName("create")
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
public operator fun invoke(
|
public inline operator fun invoke(
|
||||||
/**
|
/**
|
||||||
* @see [PluginDescription.id]
|
* @see [PluginDescription.id]
|
||||||
*/
|
*/
|
||||||
@ -57,7 +57,7 @@ public interface JvmPluginDescription : PluginDescription {
|
|||||||
*/
|
*/
|
||||||
@JvmName("create")
|
@JvmName("create")
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
public operator fun invoke(
|
public inline operator fun invoke(
|
||||||
/**
|
/**
|
||||||
* @see [PluginDescription.id]
|
* @see [PluginDescription.id]
|
||||||
*/
|
*/
|
||||||
@ -65,7 +65,7 @@ public interface JvmPluginDescription : PluginDescription {
|
|||||||
/**
|
/**
|
||||||
* @see [PluginDescription.version]
|
* @see [PluginDescription.version]
|
||||||
*/
|
*/
|
||||||
@ResolveContext(PLUGIN_VERSION) version: SemVersion,
|
version: SemVersion,
|
||||||
/**
|
/**
|
||||||
* @see [PluginDescription.name]
|
* @see [PluginDescription.name]
|
||||||
*/
|
*/
|
||||||
@ -87,17 +87,17 @@ public interface JvmPluginDescription : PluginDescription {
|
|||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* #### Java Example
|
* #### Java Example
|
||||||
* ```
|
* ```java
|
||||||
* JvmPluginDescription desc = new JvmPluginDescriptionBuilder("org.example.example-plugin", "1.0.0")
|
* JvmPluginDescription desc = new JvmPluginDescriptionBuilder("org.example.example-plugin", "1.0.0")
|
||||||
* .info("This is an example plugin")
|
* .info("This is an example plugin")
|
||||||
* .dependsOn("org.example.another-plugin")
|
* .dependsOn("org.example.another-plugin")
|
||||||
* .build()
|
* .build();
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see [JvmPluginDescription.invoke]
|
* @see [JvmPluginDescription.invoke]
|
||||||
*/
|
*/
|
||||||
public class JvmPluginDescriptionBuilder(
|
public class JvmPluginDescriptionBuilder(
|
||||||
private var id: String,
|
@ResolveContext(PLUGIN_ID) private var id: String,
|
||||||
private var version: SemVersion,
|
private var version: SemVersion,
|
||||||
) {
|
) {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -119,7 +119,7 @@ public class JvmPluginDescriptionBuilder(
|
|||||||
apply { this.version = SemVersion(value) }
|
apply { this.version = SemVersion(value) }
|
||||||
|
|
||||||
@ILoveKuriyamaMiraiForever
|
@ILoveKuriyamaMiraiForever
|
||||||
public fun version(@ResolveContext(PLUGIN_VERSION) value: SemVersion): JvmPluginDescriptionBuilder =
|
public fun version(value: SemVersion): JvmPluginDescriptionBuilder =
|
||||||
apply { this.version = value }
|
apply { this.version = value }
|
||||||
|
|
||||||
@ILoveKuriyamaMiraiForever
|
@ILoveKuriyamaMiraiForever
|
||||||
@ -148,18 +148,6 @@ public class JvmPluginDescriptionBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see PluginDependency
|
|
||||||
*/
|
|
||||||
@ILoveKuriyamaMiraiForever
|
|
||||||
public fun dependsOn(
|
|
||||||
@ResolveContext(PLUGIN_ID) pluginId: String,
|
|
||||||
isOptional: Boolean = false,
|
|
||||||
versionRequirement: SemVersion.Requirement,
|
|
||||||
): JvmPluginDescriptionBuilder = apply {
|
|
||||||
this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isOptional = false
|
* isOptional = false
|
||||||
*
|
*
|
||||||
@ -169,13 +157,28 @@ public class JvmPluginDescriptionBuilder(
|
|||||||
public fun dependsOn(
|
public fun dependsOn(
|
||||||
@ResolveContext(PLUGIN_ID) pluginId: String,
|
@ResolveContext(PLUGIN_ID) pluginId: String,
|
||||||
versionRequirement: SemVersion.Requirement,
|
versionRequirement: SemVersion.Requirement,
|
||||||
|
isOptional: Boolean = false,
|
||||||
): JvmPluginDescriptionBuilder = apply {
|
): JvmPluginDescriptionBuilder = apply {
|
||||||
this.dependencies.add(PluginDependency(pluginId, versionRequirement, false))
|
this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PluginDependency
|
||||||
|
*/
|
||||||
|
@ILoveKuriyamaMiraiForever
|
||||||
|
public fun dependsOn(
|
||||||
|
@ResolveContext(PLUGIN_ID) pluginId: String,
|
||||||
|
@ResolveContext(VERSION_REQUIREMENT) versionRequirement: String,
|
||||||
|
isOptional: Boolean = false,
|
||||||
|
): JvmPluginDescriptionBuilder = apply {
|
||||||
|
this.dependencies.add(PluginDependency(pluginId, SemVersion.parseRangeRequirement(versionRequirement), isOptional))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无版本要求
|
* 无版本要求
|
||||||
*
|
*
|
||||||
|
* @param isOptional [PluginDependency.isOptional]
|
||||||
|
*
|
||||||
* @see PluginDependency
|
* @see PluginDependency
|
||||||
*/
|
*/
|
||||||
@ILoveKuriyamaMiraiForever
|
@ILoveKuriyamaMiraiForever
|
||||||
@ -187,8 +190,8 @@ public class JvmPluginDescriptionBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION_ERROR")
|
|
||||||
public fun build(): JvmPluginDescription =
|
public fun build(): JvmPluginDescription =
|
||||||
|
@Suppress("DEPRECATION_ERROR")
|
||||||
SimpleJvmPluginDescription(name, version, id, author, info, dependencies)
|
SimpleJvmPluginDescription(name, version, id, author, info, dependencies)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,6 +230,6 @@ internal data class SimpleJvmPluginDescription
|
|||||||
) : this(name, SemVersion(version), id, author, info, dependencies)
|
) : this(name, SemVersion(version), id, author, info, dependencies)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(!name.contains(':')) { "':' is forbidden in plugin name" }
|
PluginDescription.checkPluginDescription(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,10 +22,12 @@ import kotlinx.serialization.Transient
|
|||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
import net.mamoe.mirai.console.compiler.common.ResolveContext
|
import net.mamoe.mirai.console.compiler.common.ResolveContext
|
||||||
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION
|
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION
|
||||||
|
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.VERSION_REQUIREMENT
|
||||||
import net.mamoe.mirai.console.internal.data.map
|
import net.mamoe.mirai.console.internal.data.map
|
||||||
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
|
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
|
||||||
import net.mamoe.mirai.console.util.SemVersion.Companion.equals
|
import net.mamoe.mirai.console.util.SemVersion.Companion.equals
|
||||||
import net.mamoe.mirai.console.util.SemVersion.Requirement
|
import net.mamoe.mirai.console.util.SemVersion.Requirement
|
||||||
|
import kotlin.LazyThreadSafetyMode.PUBLICATION
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [语义化版本](https://semver.org/lang/zh-CN/) 支持
|
* [语义化版本](https://semver.org/lang/zh-CN/) 支持
|
||||||
@ -138,7 +140,7 @@ internal constructor(
|
|||||||
*/
|
*/
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun parseRangeRequirement(requirement: String): Requirement =
|
public fun parseRangeRequirement(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Requirement =
|
||||||
SemVersionInternal.parseRangeRequirement(requirement)
|
SemVersionInternal.parseRangeRequirement(requirement)
|
||||||
|
|
||||||
/** @see [Requirement.test] */
|
/** @see [Requirement.test] */
|
||||||
@ -151,6 +153,12 @@ internal constructor(
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun SemVersion.satisfies(requirement: Requirement): Boolean = requirement.test(this)
|
public fun SemVersion.satisfies(requirement: Requirement): Boolean = requirement.test(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当满足 [requirement] 时返回 true, 否则返回 false
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
public fun SemVersion.satisfies(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Boolean = parseRangeRequirement(requirement).test(this)
|
||||||
|
|
||||||
/** for Kotlin only */
|
/** for Kotlin only */
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
@ -163,7 +171,7 @@ internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private val toString: String by lazy(LazyThreadSafetyMode.NONE) {
|
private val toString: String by lazy(PUBLICATION) {
|
||||||
buildString {
|
buildString {
|
||||||
append(major)
|
append(major)
|
||||||
append('.').append(minor)
|
append('.').append(minor)
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
package net.mamoe.mirai.console.permission
|
package net.mamoe.mirai.console.permission
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
internal class PermissionsBasicsTest {
|
internal class PermissionsBasicsTest {
|
||||||
|
@Test
|
||||||
|
fun testInvalidPermissionId() {
|
||||||
|
assertFails { PermissionId("space namespace", "name") }
|
||||||
|
assertFails { PermissionId("namespace", "space name") }
|
||||||
|
// assertFails { PermissionId("", "name") }
|
||||||
|
// assertFails { PermissionId("namespace", "") }
|
||||||
|
assertFails { PermissionId("namespace:name", "name") }
|
||||||
|
assertFails { PermissionId("namespace", "namespace:name") }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parentsWithSelfSequence() {
|
fun parentsWithSelfSequence() {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val core = "1.3.0"
|
const val core = "1.3.0"
|
||||||
const val console = "1.0-RC-dev-30"
|
const val console = "1.0-RC-dev-31"
|
||||||
const val consoleGraphical = "0.0.7"
|
const val consoleGraphical = "0.0.7"
|
||||||
const val consoleTerminal = console
|
const val consoleTerminal = console
|
||||||
|
|
||||||
|
@ -57,6 +57,8 @@ enum class ResolveContextKind {
|
|||||||
PLUGIN_NAME,
|
PLUGIN_NAME,
|
||||||
PLUGIN_VERSION,
|
PLUGIN_VERSION,
|
||||||
|
|
||||||
|
VERSION_REQUIREMENT,
|
||||||
|
|
||||||
COMMAND_NAME,
|
COMMAND_NAME,
|
||||||
|
|
||||||
PERMISSION_NAMESPACE,
|
PERMISSION_NAMESPACE,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.4.0"
|
kotlin("jvm") version "1.4.10"
|
||||||
kotlin("plugin.serialization") version "1.4.0"
|
kotlin("plugin.serialization") version "1.4.10"
|
||||||
kotlin("kapt") version "1.4.0"
|
kotlin("kapt") version "1.4.10"
|
||||||
id("com.github.johnrengelman.shadow") version "5.2.0"
|
id("com.github.johnrengelman.shadow") version "5.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation("net.mamoe:mirai-console:$console")
|
testImplementation("net.mamoe:mirai-console:$console")
|
||||||
testImplementation("net.mamoe:mirai-core:$core")
|
testImplementation("net.mamoe:mirai-core:$core")
|
||||||
testImplementation("net.mamoe:mirai-console-pure:$console")
|
testImplementation("net.mamoe:mirai-console-terminal:$console")
|
||||||
testImplementation(kotlin("stdlib-jdk8"))
|
testImplementation(kotlin("stdlib-jdk8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ object MyPluginMain : KotlinPlugin(
|
|||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
super.onEnable()
|
super.onEnable()
|
||||||
PermissionService.INSTANCE.register(permissionId("dvs"), "ok")
|
PermissionService.INSTANCE.register(permissionId("dvs"), "ok")
|
||||||
|
PermissionService.INSTANCE.register(permissionId("perm with space"), "error")
|
||||||
|
PermissionId("Namespace with space", "Name with space")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun test() {
|
fun test() {
|
||||||
|
@ -85,7 +85,7 @@ class ContextualParametersChecker : DeclarationChecker {
|
|||||||
fun checkPermissionNamespace(inspectionTarget: PsiElement, value: String): Diagnostic? {
|
fun checkPermissionNamespace(inspectionTarget: PsiElement, value: String): Diagnostic? {
|
||||||
return when {
|
return when {
|
||||||
value.isBlank() -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不能为空")
|
value.isBlank() -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不能为空")
|
||||||
value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "暂时不允许权限命名空间中存在空格")
|
value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "不允许权限命名空间中存在空格")
|
||||||
value.contains(':') -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不允许包含 ':'")
|
value.contains(':') -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不允许包含 ':'")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ class ContextualParametersChecker : DeclarationChecker {
|
|||||||
fun checkPermissionName(inspectionTarget: PsiElement, value: String): Diagnostic? {
|
fun checkPermissionName(inspectionTarget: PsiElement, value: String): Diagnostic? {
|
||||||
return when {
|
return when {
|
||||||
value.isBlank() -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不能为空")
|
value.isBlank() -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不能为空")
|
||||||
value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "暂时不允许权限名称中存在空格")
|
value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "不允许权限名称中存在空格")
|
||||||
value.contains(':') -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不允许包含 ':'")
|
value.contains(':') -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不允许包含 ':'")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -108,6 +108,12 @@ class ContextualParametersChecker : DeclarationChecker {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun checkVersionRequirement(inspectionTarget: PsiElement, value: String): Diagnostic? {
|
||||||
|
// TODO: 2020/10/23 checkVersionRequirement
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val checkersMap: EnumMap<ResolveContextKind, (declaration: PsiElement, value: String) -> Diagnostic?> =
|
private val checkersMap: EnumMap<ResolveContextKind, (declaration: PsiElement, value: String) -> Diagnostic?> =
|
||||||
@ -119,6 +125,7 @@ class ContextualParametersChecker : DeclarationChecker {
|
|||||||
put(ResolveContextKind.PERMISSION_NAME, ::checkPermissionName)
|
put(ResolveContextKind.PERMISSION_NAME, ::checkPermissionName)
|
||||||
put(ResolveContextKind.PERMISSION_NAMESPACE, ::checkPermissionNamespace)
|
put(ResolveContextKind.PERMISSION_NAMESPACE, ::checkPermissionNamespace)
|
||||||
put(ResolveContextKind.PERMISSION_ID, ::checkPermissionId)
|
put(ResolveContextKind.PERMISSION_ID, ::checkPermissionId)
|
||||||
|
put(ResolveContextKind.VERSION_REQUIREMENT, ::checkVersionRequirement)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun check(
|
override fun check(
|
||||||
|
@ -25,7 +25,7 @@ class AddSerializerFix(
|
|||||||
element: KtClassOrObject,
|
element: KtClassOrObject,
|
||||||
) : KotlinCrossLanguageQuickFixAction<KtModifierListOwner>(element), KotlinUniversalQuickFix {
|
) : KotlinCrossLanguageQuickFixAction<KtModifierListOwner>(element), KotlinUniversalQuickFix {
|
||||||
|
|
||||||
override fun getFamilyName(): String = "添加注解"
|
override fun getFamilyName(): String = "Mirai Console"
|
||||||
override fun getText(): String = "添加 @Serializable"
|
override fun getText(): String = "添加 @Serializable"
|
||||||
|
|
||||||
override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) {
|
override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) {
|
||||||
|
Loading…
Reference in New Issue
Block a user