mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-03 03:29:22 +08:00
Message serialization
This commit is contained in:
parent
021aac1b56
commit
f59fcf7d5d
@ -14,12 +14,12 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.plus
|
import kotlinx.serialization.modules.plus
|
||||||
import kotlinx.serialization.modules.serializersModuleOf
|
import kotlinx.serialization.modules.polymorphic
|
||||||
import net.mamoe.mirai.Mirai
|
import net.mamoe.mirai.Mirai
|
||||||
import net.mamoe.mirai.message.data.*
|
import net.mamoe.mirai.message.data.*
|
||||||
import net.mamoe.mirai.message.data.CombinedMessage
|
|
||||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -29,6 +29,8 @@ public interface MessageSerializer {
|
|||||||
|
|
||||||
public fun <M : Message> registerSerializer(clazz: KClass<M>, serializer: KSerializer<M>)
|
public fun <M : Message> registerSerializer(clazz: KClass<M>, serializer: KSerializer<M>)
|
||||||
|
|
||||||
|
public fun registerSerializers(serializersModule: SerializersModule)
|
||||||
|
|
||||||
public fun clearRegisteredSerializers()
|
public fun clearRegisteredSerializers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,54 +82,135 @@ internal object MessageSourceSerializer : KSerializer<MessageSource> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val builtInSerializersModule = SerializersModule {
|
private val builtInSerializersModule by lazy {
|
||||||
// In case Proguard or something else obfuscated the Kotlin metadata, providing the serializesrs explicity will help.
|
SerializersModule {
|
||||||
contextual(At::class, At.serializer())
|
// non-Message classes
|
||||||
contextual(AtAll::class, AtAll.serializer())
|
contextual(RawForwardMessage::class, RawForwardMessage.serializer())
|
||||||
contextual(CombinedMessage::class, CombinedMessage.serializer())
|
contextual(ForwardMessage.Node::class, ForwardMessage.Node.serializer())
|
||||||
contextual(CustomMessage::class, CustomMessage.serializer())
|
contextual(VipFace.Kind::class, VipFace.Kind.serializer())
|
||||||
contextual(CustomMessageMetadata::class, CustomMessageMetadata.serializer())
|
|
||||||
contextual(Face::class, Face.serializer())
|
|
||||||
contextual(MessageSource::class, MessageSource.serializer())
|
|
||||||
contextual(Image::class, Image.Serializer)
|
|
||||||
contextual(PlainText::class, PlainText.serializer())
|
|
||||||
contextual(QuoteReply::class, QuoteReply.serializer())
|
|
||||||
|
|
||||||
contextual(ForwardMessage::class, ForwardMessage.serializer())
|
|
||||||
contextual(RawForwardMessage::class, RawForwardMessage.serializer())
|
|
||||||
contextual(ForwardMessage.Node::class, ForwardMessage.Node.serializer())
|
|
||||||
|
|
||||||
|
|
||||||
contextual(LightApp::class, LightApp.serializer())
|
// In case Proguard or something else obfuscated the Kotlin metadata, providing the serializers explicitly will help.
|
||||||
contextual(SimpleServiceMessage::class, SimpleServiceMessage.serializer())
|
contextual(At::class, At.serializer())
|
||||||
contextual(AbstractServiceMessage::class, AbstractServiceMessage.serializer())
|
contextual(AtAll::class, AtAll.serializer())
|
||||||
contextual(LongMessage::class, LongMessage.serializer())
|
contextual(CustomMessage::class, CustomMessage.serializer())
|
||||||
contextual(ForwardMessageInternal::class, ForwardMessageInternal.serializer())
|
contextual(CustomMessageMetadata::class, CustomMessageMetadata.serializer())
|
||||||
|
contextual(Face::class, Face.serializer())
|
||||||
|
contextual(MessageSource::class, MessageSource.serializer())
|
||||||
|
contextual(Image::class, Image.Serializer)
|
||||||
|
contextual(PlainText::class, PlainText.serializer())
|
||||||
|
contextual(QuoteReply::class, QuoteReply.serializer())
|
||||||
|
|
||||||
contextual(PttMessage::class, PttMessage.serializer())
|
contextual(ForwardMessage::class, ForwardMessage.serializer())
|
||||||
contextual(Voice::class, Voice.serializer())
|
|
||||||
|
|
||||||
contextual(HummerMessage::class, HummerMessage.serializer())
|
|
||||||
contextual(PokeMessage::class, PokeMessage.serializer())
|
|
||||||
contextual(VipFace::class, VipFace.serializer())
|
|
||||||
contextual(FlashImage::class, FlashImage.serializer())
|
|
||||||
contextual(VipFace.Kind::class, VipFace.Kind.serializer())
|
|
||||||
|
|
||||||
|
|
||||||
contextual(Message::class, Message.Serializer)
|
contextual(LightApp::class, LightApp.serializer())
|
||||||
contextual(MessageChain::class, MessageChain.Serializer)
|
contextual(SimpleServiceMessage::class, SimpleServiceMessage.serializer())
|
||||||
|
contextual(AbstractServiceMessage::class, AbstractServiceMessage.serializer())
|
||||||
|
contextual(LongMessage::class, LongMessage.serializer())
|
||||||
|
contextual(ForwardMessageInternal::class, ForwardMessageInternal.serializer())
|
||||||
|
|
||||||
|
contextual(PttMessage::class, PttMessage.serializer())
|
||||||
|
contextual(Voice::class, Voice.serializer())
|
||||||
|
|
||||||
|
contextual(HummerMessage::class, HummerMessage.serializer())
|
||||||
|
contextual(PokeMessage::class, PokeMessage.serializer())
|
||||||
|
contextual(VipFace::class, VipFace.serializer())
|
||||||
|
contextual(FlashImage::class, FlashImage.serializer())
|
||||||
|
|
||||||
|
fun PolymorphicModuleBuilder<SingleMessage>.singleMessageSubclasses() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PolymorphicModuleBuilder<MessageMetadata>.messageMetadataSubclasses() {
|
||||||
|
subclass(MessageSource::class, MessageSource.serializer())
|
||||||
|
subclass(QuoteReply::class, QuoteReply.serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PolymorphicModuleBuilder<MessageContent>.messageContentSubclasses() {
|
||||||
|
subclass(At::class, At.serializer())
|
||||||
|
subclass(AtAll::class, AtAll.serializer())
|
||||||
|
subclass(Face::class, Face.serializer())
|
||||||
|
subclass(Image::class, Image.Serializer)
|
||||||
|
subclass(PlainText::class, PlainText.serializer())
|
||||||
|
|
||||||
|
subclass(ForwardMessage::class, ForwardMessage.serializer())
|
||||||
|
|
||||||
|
|
||||||
|
subclass(LightApp::class, LightApp.serializer())
|
||||||
|
subclass(SimpleServiceMessage::class, SimpleServiceMessage.serializer())
|
||||||
|
subclass(LongMessage::class, LongMessage.serializer())
|
||||||
|
subclass(ForwardMessageInternal::class, ForwardMessageInternal.serializer())
|
||||||
|
|
||||||
|
// subclass(PttMessage::class, PttMessage.serializer())
|
||||||
|
subclass(Voice::class, Voice.serializer())
|
||||||
|
|
||||||
|
// subclass(HummerMessage::class, HummerMessage.serializer())
|
||||||
|
subclass(PokeMessage::class, PokeMessage.serializer())
|
||||||
|
subclass(VipFace::class, VipFace.serializer())
|
||||||
|
subclass(FlashImage::class, FlashImage.serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
contextual(MessageChain::class, MessageChain.Serializer)
|
||||||
|
polymorphicDefault(MessageChain::class) { MessageChainImpl.serializer() }
|
||||||
|
|
||||||
|
polymorphic(AbstractServiceMessage::class) {
|
||||||
|
subclass(LongMessage::class, LongMessage.serializer())
|
||||||
|
subclass(ForwardMessageInternal::class, ForwardMessageInternal.serializer())
|
||||||
|
}
|
||||||
|
|
||||||
|
polymorphicDefault(Image::class) { Image.Serializer }
|
||||||
|
|
||||||
|
contextual(Message::class, Message.Serializer)
|
||||||
|
// polymorphic(Message::class) {
|
||||||
|
// subclass(PlainText::class, PlainText.serializer())
|
||||||
|
// }
|
||||||
|
polymorphic(Message::class) {
|
||||||
|
messageContentSubclasses()
|
||||||
|
messageMetadataSubclasses()
|
||||||
|
singleMessageSubclasses()
|
||||||
|
}
|
||||||
|
|
||||||
|
//contextual(SingleMessage::class, SingleMessage.Serializer)
|
||||||
|
// polymorphic(SingleMessage::class, SingleMessage.Serializer) {
|
||||||
|
// messageContentSubclasses()
|
||||||
|
// messageMetadataSubclasses()
|
||||||
|
// singleMessageSubclasses()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// contextual(MessageContent::class, MessageContent.Serializer)
|
||||||
|
// polymorphic(MessageContent::class, MessageContent.Serializer) {
|
||||||
|
// messageContentSubclasses()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// contextual(MessageMetadata::class, MessageMetadata.Serializer)
|
||||||
|
// polymorphic(MessageMetadata::class, MessageMetadata.Serializer) {
|
||||||
|
// messageMetadataSubclasses()
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object MessageSerializerImpl : MessageSerializer {
|
internal object MessageSerializerImpl : MessageSerializer {
|
||||||
override var serializersModule: SerializersModule = builtInSerializersModule
|
@Volatile
|
||||||
|
private var serializersModuleField: SerializersModule? = null
|
||||||
|
override val serializersModule: SerializersModule get() = serializersModuleField ?: builtInSerializersModule
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun <M : Message> registerSerializer(clazz: KClass<M>, serializer: KSerializer<M>) {
|
override fun <M : Message> registerSerializer(clazz: KClass<M>, serializer: KSerializer<M>) {
|
||||||
serializersModule = serializersModule.plus(serializersModuleOf(clazz, serializer))
|
serializersModuleField = serializersModule.plus(SerializersModule {
|
||||||
|
contextual(clazz, serializer)
|
||||||
|
polymorphic(Message::class) {
|
||||||
|
subclass(clazz, serializer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun registerSerializers(serializersModule: SerializersModule) {
|
||||||
|
serializersModuleField = serializersModule
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun clearRegisteredSerializers() {
|
override fun clearRegisteredSerializers() {
|
||||||
serializersModule = builtInSerializersModule
|
serializersModuleField = builtInSerializersModule
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,8 +28,11 @@ import kotlinx.serialization.KSerializer
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.encoding.decodeStructure
|
||||||
|
import kotlinx.serialization.encoding.encodeStructure
|
||||||
import net.mamoe.kjbb.JvmBlockingBridge
|
import net.mamoe.kjbb.JvmBlockingBridge
|
||||||
import net.mamoe.mirai.Bot
|
import net.mamoe.mirai.Bot
|
||||||
import net.mamoe.mirai.IMirai
|
import net.mamoe.mirai.IMirai
|
||||||
@ -97,14 +100,21 @@ public interface Image : Message, MessageContent, CodableMessage {
|
|||||||
public val imageId: String
|
public val imageId: String
|
||||||
|
|
||||||
public object Serializer : KSerializer<Image> {
|
public object Serializer : KSerializer<Image> {
|
||||||
override val descriptor: SerialDescriptor = String.serializer().descriptor
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("net.mamoe.mirai.message.data.Image") {
|
||||||
override fun deserialize(decoder: Decoder): Image {
|
element("imageId", String.serializer().descriptor)
|
||||||
val id = String.serializer().deserialize(decoder)
|
|
||||||
return Image(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: Image) {
|
override fun deserialize(decoder: Decoder): Image = decoder.decodeStructure(descriptor) {
|
||||||
String.serializer().serialize(encoder, value.imageId)
|
val imageId = if (decodeSequentially()) {
|
||||||
|
decodeStringElement(descriptor, 0)
|
||||||
|
} else {
|
||||||
|
decodeStringElement(descriptor, decodeElementIndex(descriptor))
|
||||||
|
}
|
||||||
|
Image(imageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Image): Unit = encoder.encodeStructure(descriptor) {
|
||||||
|
encodeStringElement(descriptor, 0, value.imageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +274,10 @@ public inline operator fun Message.times(count: Int): MessageChain = this.repeat
|
|||||||
/**
|
/**
|
||||||
* 单个消息元素. 与之相对的是 [MessageChain], 是多个 [SingleMessage] 的集合.
|
* 单个消息元素. 与之相对的是 [MessageChain], 是多个 [SingleMessage] 的集合.
|
||||||
*/
|
*/
|
||||||
public interface SingleMessage : Message
|
@Serializable(SingleMessage.Serializer::class)
|
||||||
|
public interface SingleMessage : Message {
|
||||||
|
public object Serializer : KSerializer<SingleMessage> by PolymorphicSerializer(SingleMessage::class)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息元数据, 即不含内容的元素.
|
* 消息元数据, 即不含内容的元素.
|
||||||
@ -289,11 +292,14 @@ public interface SingleMessage : Message
|
|||||||
*
|
*
|
||||||
* @see ConstrainSingle 约束一个 [MessageChain] 中只存在这一种类型的元素
|
* @see ConstrainSingle 约束一个 [MessageChain] 中只存在这一种类型的元素
|
||||||
*/
|
*/
|
||||||
|
@Serializable(MessageMetadata.Serializer::class)
|
||||||
public interface MessageMetadata : SingleMessage {
|
public interface MessageMetadata : SingleMessage {
|
||||||
/**
|
/**
|
||||||
* 返回空字符串
|
* 返回空字符串
|
||||||
*/
|
*/
|
||||||
override fun contentToString(): String = ""
|
override fun contentToString(): String = ""
|
||||||
|
|
||||||
|
public object Serializer : KSerializer<MessageMetadata> by PolymorphicSerializer(MessageMetadata::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,9 +384,12 @@ public abstract class AbstractPolymorphicMessageKey<out B : SingleMessage, out M
|
|||||||
* @see ForwardMessage 合并转发
|
* @see ForwardMessage 合并转发
|
||||||
* @see Voice 语音
|
* @see Voice 语音
|
||||||
*/
|
*/
|
||||||
|
@Serializable(MessageContent.Serializer::class)
|
||||||
public interface MessageContent : SingleMessage {
|
public interface MessageContent : SingleMessage {
|
||||||
@ExperimentalMessageKey
|
@ExperimentalMessageKey
|
||||||
public companion object Key : AbstractMessageKey<MessageContent>({ it.safeCast() })
|
public companion object Key : AbstractMessageKey<MessageContent>({ it.safeCast() })
|
||||||
|
|
||||||
|
public object Serializer : KSerializer<MessageContent> by PolymorphicSerializer(MessageContent::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +56,10 @@ import kotlin.random.Random
|
|||||||
internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||||
companion object INSTANCE : MiraiImpl() {
|
companion object INSTANCE : MiraiImpl() {
|
||||||
@Suppress("ObjectPropertyName", "unused")
|
@Suppress("ObjectPropertyName", "unused")
|
||||||
private val _init = Mirai.let { }
|
private val _init = Mirai.let {
|
||||||
|
Message.Serializer.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())
|
||||||
|
Message.Serializer.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val BotFactory: BotFactory
|
override val BotFactory: BotFactory
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.internal
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
internal class AtomicResizeCacheListTest {
|
internal class AtomicResizeCacheListTest {
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.internal.message.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import net.mamoe.mirai.Mirai
|
||||||
|
import net.mamoe.mirai.message.data.*
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
internal class MessageSerializationTest {
|
||||||
|
private val module = Message.Serializer.serializersModule
|
||||||
|
private val format = Json {
|
||||||
|
serializersModule = module
|
||||||
|
useArrayPolymorphism = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Any> T.serialize(serializer: KSerializer<T> = module.serializer()): String {
|
||||||
|
return format.encodeToString(serializer, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Any> String.deserialize(serializer: KSerializer<T> = module.serializer()): T {
|
||||||
|
return format.decodeFromString(serializer, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Any> testSerialization(t: T, serializer: KSerializer<T> = module.serializer()) {
|
||||||
|
assertEquals(
|
||||||
|
t,
|
||||||
|
t.serialize(serializer).deserialize(serializer),
|
||||||
|
message = "serialized string: ${t.serialize(serializer)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val testMessageContentInstances: Array<out MessageContent> = arrayOf(
|
||||||
|
PlainText("test"),
|
||||||
|
At._lowLevelConstructAtInstance(123456, ""),
|
||||||
|
AtAll,
|
||||||
|
Image("{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
private val testConstrainSingleMessageInstances: Array<out ConstrainSingle> = arrayOf(
|
||||||
|
LongMessage("content", "resId")
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeAll
|
||||||
|
@JvmStatic
|
||||||
|
fun init() {
|
||||||
|
Mirai
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test serialize each message contents`() {
|
||||||
|
for (message in testMessageContentInstances) {
|
||||||
|
testSerialization(message, module.serializer(message.javaClass))
|
||||||
|
}
|
||||||
|
for (message in testConstrainSingleMessageInstances) {
|
||||||
|
testSerialization(message, module.serializer(message.javaClass))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test serialize message chain`() {
|
||||||
|
val chain = testMessageContentInstances.asMessageChain()
|
||||||
|
println(chain.serialize()) // [["net.mamoe.mirai.message.data.PlainText",{"content":"test"}],["net.mamoe.mirai.message.data.At",{"target":123456,"display":""}],["net.mamoe.mirai.message.data.AtAll",{}],["net.mamoe.mirai.internal.message.OfflineGroupImage",{"imageId":"{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai"}]]
|
||||||
|
|
||||||
|
testSerialization(chain)
|
||||||
|
}
|
||||||
|
}
|
10
mirai-core/src/jvmTest/kotlin/package.kt
Normal file
10
mirai-core/src/jvmTest/kotlin/package.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.internal
|
Loading…
Reference in New Issue
Block a user