mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
3 Commits
ccbfc9a1e1
...
35c82fcc51
Author | SHA1 | Date | |
---|---|---|---|
35c82fcc51 | |||
89a4912ed7 | |||
aeabc66067 |
@ -8,6 +8,7 @@ import android.os.Bundle
|
|||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.parameter
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@ -58,7 +59,7 @@ object DashboardInitializer {
|
|||||||
url("http://127.0.0.1:$servicePort/get_account_info")
|
url("http://127.0.0.1:$servicePort/get_account_info")
|
||||||
val token = ShamrockConfig.getToken(context)
|
val token = ShamrockConfig.getToken(context)
|
||||||
if (token.isNotBlank()) {
|
if (token.isNotBlank()) {
|
||||||
header("Authorization", "Bearer $token")
|
parameter("token", token)
|
||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
if (it.status == HttpStatusCode.OK) {
|
if (it.status == HttpStatusCode.OK) {
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QQ收藏相关接口
|
||||||
|
*/
|
||||||
|
internal object QFavSvc: BaseSvc() {
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.api
|
package moe.fuqiuluo.shamrock.remote.api
|
||||||
|
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.server.application.ApplicationCall
|
||||||
import io.ktor.server.application.call
|
import io.ktor.server.application.call
|
||||||
import io.ktor.server.request.httpVersion
|
import io.ktor.server.request.httpVersion
|
||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
@ -8,8 +9,12 @@ import io.ktor.server.routing.Routing
|
|||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
import io.ktor.server.routing.route
|
import io.ktor.server.routing.route
|
||||||
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
import moe.fuqiuluo.shamrock.remote.HTTPServer
|
import moe.fuqiuluo.shamrock.remote.HTTPServer
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
@ -18,12 +23,17 @@ import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
|
|||||||
import moe.fuqiuluo.shamrock.remote.entries.IndexData
|
import moe.fuqiuluo.shamrock.remote.entries.IndexData
|
||||||
import moe.fuqiuluo.shamrock.remote.entries.Status
|
import moe.fuqiuluo.shamrock.remote.entries.Status
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
|
||||||
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asString
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchOrNull
|
import moe.fuqiuluo.shamrock.tools.fetchOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
|
||||||
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElementOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonArray
|
import moe.fuqiuluo.shamrock.tools.isJsonArray
|
||||||
|
import moe.fuqiuluo.shamrock.tools.isJsonData
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonObject
|
import moe.fuqiuluo.shamrock.tools.isJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonString
|
import moe.fuqiuluo.shamrock.tools.isJsonString
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
@ -39,6 +49,31 @@ data class OldApiResult<T>(
|
|||||||
val data: T? = null
|
val data: T? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonObject(data: JsonObject) {
|
||||||
|
val action = data["action"].asString
|
||||||
|
val echo = data["echo"] ?: EmptyJsonString
|
||||||
|
call.attributes.put(ECHO_KEY, echo)
|
||||||
|
|
||||||
|
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
|
||||||
|
|
||||||
|
val handler = ActionManager[action]
|
||||||
|
if (handler == null) {
|
||||||
|
respond(false, Status.UnsupportedAction, EmptyObject, "不支持的Action", echo = echo)
|
||||||
|
} else {
|
||||||
|
call.respondText(handler.handle(ActionSession(params, echo)), ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonArray(data: JsonArray) {
|
||||||
|
data.forEach {
|
||||||
|
when (it) {
|
||||||
|
is JsonArray -> handleAsJsonArray(it)
|
||||||
|
is JsonObject -> handleAsJsonObject(it)
|
||||||
|
else -> handleAsJsonObject(it.jsonObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Routing.echoVersion() {
|
fun Routing.echoVersion() {
|
||||||
route("/") {
|
route("/") {
|
||||||
get {
|
get {
|
||||||
@ -49,6 +84,15 @@ fun Routing.echoVersion() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
|
fetchPostJsonElementOrNull()?.let {
|
||||||
|
if (it is JsonArray) {
|
||||||
|
handleAsJsonArray(it)
|
||||||
|
return@post
|
||||||
|
} else if (it is JsonObject) {
|
||||||
|
handleAsJsonObject(it)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
}
|
||||||
val action = fetchOrThrow("action")
|
val action = fetchOrThrow("action")
|
||||||
val echo = if (isJsonObject("echo") || isJsonArray("echo")) {
|
val echo = if (isJsonObject("echo") || isJsonArray("echo")) {
|
||||||
fetchPostJsonElement("echo")
|
fetchPostJsonElement("echo")
|
||||||
|
@ -7,6 +7,9 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.server.application.call
|
||||||
|
import io.ktor.server.response.respondText
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.*
|
import moe.fuqiuluo.qqinterface.servlet.msg.*
|
||||||
import moe.fuqiuluo.shamrock.remote.service.api.HttpTransmitServlet
|
import moe.fuqiuluo.shamrock.remote.service.api.HttpTransmitServlet
|
||||||
@ -21,6 +25,11 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.*
|
|||||||
import moe.fuqiuluo.shamrock.tools.*
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
|
import moe.fuqiuluo.shamrock.remote.config.ECHO_KEY
|
||||||
|
import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
|
||||||
|
import moe.fuqiuluo.shamrock.remote.entries.Status
|
||||||
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
||||||
|
|
||||||
internal object HttpService: HttpTransmitServlet() {
|
internal object HttpService: HttpTransmitServlet() {
|
||||||
@ -66,65 +75,86 @@ internal object HttpService: HttpTransmitServlet() {
|
|||||||
|
|
||||||
private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) {
|
private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) {
|
||||||
try {
|
try {
|
||||||
val data = Json.parseToJsonElement(jsonText).asJsonObject
|
val data = Json.parseToJsonElement(jsonText)
|
||||||
if (data.containsKey("reply")) {
|
|
||||||
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
|
if (data is JsonObject) {
|
||||||
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
|
if (data.containsKey("reply")) {
|
||||||
val atSender = data["at_sender"].asBooleanOrNull ?: false
|
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
|
||||||
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
|
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
|
||||||
val message = data["reply"]
|
val atSender = data["at_sender"].asBooleanOrNull ?: false
|
||||||
if (message is JsonPrimitive) {
|
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
|
||||||
if (autoEscape) {
|
val message = data["reply"]
|
||||||
val msgList = mutableSetOf<JsonElement>()
|
if (message is JsonPrimitive) {
|
||||||
msgList.add(mapOf(
|
if (autoEscape) {
|
||||||
"type" to "text",
|
val msgList = mutableSetOf<JsonElement>()
|
||||||
"data" to mapOf(
|
msgList.add(mapOf(
|
||||||
"text" to message.asString
|
"type" to "text",
|
||||||
|
"data" to mapOf(
|
||||||
|
"text" to message.asString
|
||||||
|
)
|
||||||
|
).json)
|
||||||
|
quicklyReply(
|
||||||
|
record,
|
||||||
|
msgList.jsonArray,
|
||||||
|
msgHash,
|
||||||
|
atSender,
|
||||||
|
autoReply
|
||||||
)
|
)
|
||||||
).json)
|
} else {
|
||||||
|
val messageArray = MessageHelper.decodeCQCode(message.asString)
|
||||||
|
quicklyReply(
|
||||||
|
record,
|
||||||
|
messageArray,
|
||||||
|
msgHash,
|
||||||
|
atSender,
|
||||||
|
autoReply
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (message is JsonArray) {
|
||||||
quicklyReply(
|
quicklyReply(
|
||||||
record,
|
record,
|
||||||
msgList.jsonArray,
|
message,
|
||||||
msgHash,
|
|
||||||
atSender,
|
|
||||||
autoReply
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val messageArray = MessageHelper.decodeCQCode(message.asString)
|
|
||||||
quicklyReply(
|
|
||||||
record,
|
|
||||||
messageArray,
|
|
||||||
msgHash,
|
msgHash,
|
||||||
atSender,
|
atSender,
|
||||||
autoReply
|
autoReply
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (message is JsonArray) {
|
|
||||||
quicklyReply(
|
|
||||||
record,
|
|
||||||
message,
|
|
||||||
msgHash,
|
|
||||||
atSender,
|
|
||||||
autoReply
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
|
||||||
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
|
MsgSvc.recallMsg(msgHash)
|
||||||
MsgSvc.recallMsg(msgHash)
|
}
|
||||||
}
|
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
|
||||||
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
|
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
|
||||||
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
|
}
|
||||||
}
|
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
|
||||||
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
|
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
|
||||||
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
|
if (banTime <= 0) return
|
||||||
if (banTime <= 0) return
|
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
|
||||||
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
|
}
|
||||||
|
} else if (data is JsonArray) {
|
||||||
|
data.forEach {
|
||||||
|
handleQuicklyActions(it.asJsonObject)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
LogCenter.log("处理快速操作错误: $e", Level.WARN)
|
LogCenter.log("处理快速操作错误: $e", Level.WARN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleQuicklyActions(data: JsonObject) {
|
||||||
|
val action = data["action"].asString
|
||||||
|
val echo = data["echo"] ?: EmptyJsonString
|
||||||
|
|
||||||
|
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
|
||||||
|
|
||||||
|
val handler = ActionManager[action]
|
||||||
|
if (handler == null) {
|
||||||
|
LogCenter.log("HTTP快速操作:不支持的Action: $action", Level.WARN)
|
||||||
|
} else {
|
||||||
|
handler.handle(ActionSession(params, echo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun quicklyReply(
|
private suspend fun quicklyReply(
|
||||||
record: MsgRecord,
|
record: MsgRecord,
|
||||||
message: JsonArray,
|
message: JsonArray,
|
||||||
|
@ -30,7 +30,7 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
|
|||||||
if (!allowTransmit()) return null
|
if (!allowTransmit()) return null
|
||||||
try {
|
try {
|
||||||
if (address.startsWith("http://") || address.startsWith("https://")) {
|
if (address.startsWith("http://") || address.startsWith("https://")) {
|
||||||
return GlobalClient.post(address) {
|
val response = GlobalClient.post(address) {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(body)
|
setBody(body)
|
||||||
|
|
||||||
@ -44,6 +44,11 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
|
|||||||
header("X-Client-Role", "Universal")
|
header("X-Client-Role", "Universal")
|
||||||
header("Sec-WebSocket-Protocol", "11.Shamrock")
|
header("Sec-WebSocket-Protocol", "11.Shamrock")
|
||||||
}
|
}
|
||||||
|
return if (response.status.value == 204) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LogCenter.log("HTTP推送地址错误: ${address}。", Level.ERROR)
|
LogCenter.log("HTTP推送地址错误: ${address}。", Level.ERROR)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@ annotation class ShamrockDsl
|
|||||||
|
|
||||||
private val keyIsJson = AttributeKey<Boolean>("isJson")
|
private val keyIsJson = AttributeKey<Boolean>("isJson")
|
||||||
private val keyJsonObject = AttributeKey<JsonObject>("paramsJson")
|
private val keyJsonObject = AttributeKey<JsonObject>("paramsJson")
|
||||||
|
private val keyJsonArray = AttributeKey<JsonArray>("paramsJsonArray")
|
||||||
|
private val keyJsonElement = AttributeKey<JsonElement>("paramsJsonElement")
|
||||||
private val keyParts = AttributeKey<Parameters>("paramsParts")
|
private val keyParts = AttributeKey<Parameters>("paramsParts")
|
||||||
|
|
||||||
suspend fun ApplicationCall.fetch(key: String): String {
|
suspend fun ApplicationCall.fetch(key: String): String {
|
||||||
@ -167,7 +169,9 @@ suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostOrThrow(key: String)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun PipelineContext<Unit, ApplicationCall>.isJsonData(): Boolean {
|
fun PipelineContext<Unit, ApplicationCall>.isJsonData(): Boolean {
|
||||||
return ContentType.Application.Json == call.request.contentType() || (keyIsJson in call.attributes && call.attributes[keyIsJson])
|
return ContentType.Application.Json == call.request.contentType()
|
||||||
|
|| (keyIsJson in call.attributes && call.attributes[keyIsJson])
|
||||||
|
|| (keyJsonElement in call.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun PipelineContext<Unit, ApplicationCall>.isJsonString(key: String): Boolean {
|
suspend fun PipelineContext<Unit, ApplicationCall>.isJsonString(key: String): Boolean {
|
||||||
@ -245,12 +249,33 @@ suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonObjectOrNull(key
|
|||||||
call.attributes[keyJsonObject]
|
call.attributes[keyJsonObject]
|
||||||
} else {
|
} else {
|
||||||
Json.parseToJsonElement(call.receiveText()).jsonObject.also {
|
Json.parseToJsonElement(call.receiveText()).jsonObject.also {
|
||||||
|
call.attributes.put(keyIsJson, true)
|
||||||
call.attributes.put(keyJsonObject, it)
|
call.attributes.put(keyJsonObject, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data[key].asJsonObjectOrNull
|
return data[key].asJsonObjectOrNull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonElementOrNull(): JsonElement? {
|
||||||
|
return runCatching {
|
||||||
|
if (call.attributes.contains(keyJsonObject)) {
|
||||||
|
call.attributes[keyJsonObject]
|
||||||
|
} else if (call.attributes.contains(keyJsonArray)) {
|
||||||
|
call.attributes[keyJsonArray]
|
||||||
|
} else if (call.attributes.contains(keyJsonElement)) {
|
||||||
|
call.attributes[keyJsonElement]
|
||||||
|
} else {
|
||||||
|
Json.parseToJsonElement(call.receiveText()).also {
|
||||||
|
call.attributes.put(keyJsonElement, it)
|
||||||
|
if (it is JsonObject) {
|
||||||
|
call.attributes.put(keyJsonObject, it)
|
||||||
|
} else if (it is JsonArray) {
|
||||||
|
call.attributes.put(keyJsonArray, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonArray(key: String): JsonArray {
|
suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonArray(key: String): JsonArray {
|
||||||
val data = if (call.attributes.contains(keyJsonObject)) {
|
val data = if (call.attributes.contains(keyJsonObject)) {
|
||||||
|
Reference in New Issue
Block a user