[*] 日常优化
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.acgist.taoyao.boot.utils;
|
package com.acgist.taoyao.boot.utils;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.nio.channels.AsynchronousChannelGroup;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -45,4 +46,19 @@ public final class CloseableUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭通道线程池
|
||||||
|
*
|
||||||
|
* @param group 通道线程池
|
||||||
|
*/
|
||||||
|
public static final void shutdown(AsynchronousChannelGroup group) {
|
||||||
|
try {
|
||||||
|
if(group != null) {
|
||||||
|
group.shutdown();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("关闭通道线程池异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public abstract class ClientAdapter<T extends AutoCloseable> implements Client {
|
|||||||
* @param instance 终端实例
|
* @param instance 终端实例
|
||||||
*/
|
*/
|
||||||
protected ClientAdapter(long timeout, T instance) {
|
protected ClientAdapter(long timeout, T instance) {
|
||||||
|
this.ip = this.getClientIP(instance);
|
||||||
this.time = System.currentTimeMillis();
|
this.time = System.currentTimeMillis();
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
@@ -151,9 +152,19 @@ public abstract class ClientAdapter<T extends AutoCloseable> implements Client {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
|
log.info("关闭终端实例:{} - {}", this.ip, this.clientId);
|
||||||
this.instance.close();
|
this.instance.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析终端IP
|
||||||
|
*
|
||||||
|
* @param instance 终端实例
|
||||||
|
*
|
||||||
|
* @return 终端IP
|
||||||
|
*/
|
||||||
|
protected abstract String getClientIP(T instance);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.getClass().getSimpleName() + " - " + this.ip + " - " + this.clientId;
|
return this.getClass().getSimpleName() + " - " + this.ip + " - " + this.clientId;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class ClientManager {
|
|||||||
*/
|
*/
|
||||||
public void unicast(Client to, Message message) {
|
public void unicast(Client to, Message message) {
|
||||||
this.clients().stream()
|
this.clients().stream()
|
||||||
.filter(v -> v.getInstance() == to)
|
.filter(v -> v == to)
|
||||||
.forEach(v -> v.push(message));
|
.forEach(v -> v.push(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,14 +105,14 @@ public class ClientManager {
|
|||||||
*/
|
*/
|
||||||
public void broadcast(Client from, Message message, ClientType ... clientTypes) {
|
public void broadcast(Client from, Message message, ClientType ... clientTypes) {
|
||||||
this.clients(clientTypes).stream()
|
this.clients(clientTypes).stream()
|
||||||
.filter(v -> v.getInstance() != from)
|
.filter(v -> v != from)
|
||||||
.forEach(v -> v.push(message));
|
.forEach(v -> v.push(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param instance 终端实例
|
* @param instance 终端实例
|
||||||
*
|
*
|
||||||
* @return 终端
|
* @return 终端(包含授权和未授权)
|
||||||
*/
|
*/
|
||||||
public Client clients(AutoCloseable instance) {
|
public Client clients(AutoCloseable instance) {
|
||||||
return this.clients.stream()
|
return this.clients.stream()
|
||||||
@@ -122,7 +122,7 @@ public class ClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param clientId 终端标识
|
* @param clientId 终端ID
|
||||||
*
|
*
|
||||||
* @return 授权终端
|
* @return 授权终端
|
||||||
*/
|
*/
|
||||||
@@ -136,7 +136,7 @@ public class ClientManager {
|
|||||||
/**
|
/**
|
||||||
* @param clientTypes 终端类型
|
* @param clientTypes 终端类型
|
||||||
*
|
*
|
||||||
* @return 所有授权终端列表
|
* @return 授权终端列表
|
||||||
*/
|
*/
|
||||||
public List<Client> clients(ClientType ... clientTypes) {
|
public List<Client> clients(ClientType ... clientTypes) {
|
||||||
return this.clients.stream()
|
return this.clients.stream()
|
||||||
@@ -156,7 +156,7 @@ public class ClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param clientId 终端标识
|
* @param clientId 终端ID
|
||||||
*
|
*
|
||||||
* @return 授权终端状态
|
* @return 授权终端状态
|
||||||
*/
|
*/
|
||||||
@@ -168,7 +168,7 @@ public class ClientManager {
|
|||||||
/**
|
/**
|
||||||
* @param clientTypes 终端类型
|
* @param clientTypes 终端类型
|
||||||
*
|
*
|
||||||
* @return 所有授权终端状态列表
|
* @return 授权终端状态列表
|
||||||
*/
|
*/
|
||||||
public List<ClientStatus> status(ClientType ... clientTypes) {
|
public List<ClientStatus> status(ClientType ... clientTypes) {
|
||||||
return this.clients(clientTypes).stream()
|
return this.clients(clientTypes).stream()
|
||||||
@@ -208,9 +208,7 @@ public class ClientManager {
|
|||||||
log.error("关闭终端异常:{}", instance, e);
|
log.error("关闭终端异常:{}", instance, e);
|
||||||
} finally {
|
} finally {
|
||||||
if(client != null) {
|
if(client != null) {
|
||||||
// 移除管理
|
|
||||||
this.clients.remove(client);
|
this.clients.remove(client);
|
||||||
// 关闭事件
|
|
||||||
this.applicationContext.publishEvent(new ClientCloseEvent(client));
|
this.applicationContext.publishEvent(new ClientCloseEvent(client));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ public class ClientStatus {
|
|||||||
private Boolean clientRecording;
|
private Boolean clientRecording;
|
||||||
@Schema(title = "服务端是否正在录像", description = "服务端是否正在录像")
|
@Schema(title = "服务端是否正在录像", description = "服务端是否正在录像")
|
||||||
private Boolean serverRecording;
|
private Boolean serverRecording;
|
||||||
@Schema(title = "最后心跳时间", description = "最后心跳时间")
|
|
||||||
private LocalDateTime lastHeartbeat;
|
|
||||||
@Schema(title = "终端状态", description = "其他扩展终端状态")
|
@Schema(title = "终端状态", description = "其他扩展终端状态")
|
||||||
private Map<String, Object> status = new HashMap<>();
|
private Map<String, Object> status = new HashMap<>();
|
||||||
@Schema(title = "终端配置", description = "其他扩展终端配置")
|
@Schema(title = "终端配置", description = "其他扩展终端配置")
|
||||||
private Map<String, Object> config = new HashMap<>();
|
private Map<String, Object> config = new HashMap<>();
|
||||||
|
@Schema(title = "最后心跳时间", description = "最后心跳时间")
|
||||||
|
private LocalDateTime lastHeartbeat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拷贝属性
|
* 拷贝属性
|
||||||
|
|||||||
@@ -14,10 +14,25 @@ import lombok.Getter;
|
|||||||
@Getter
|
@Getter
|
||||||
public enum ClientType {
|
public enum ClientType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过浏览器接入的终端
|
||||||
|
*/
|
||||||
WEB("Web"),
|
WEB("Web"),
|
||||||
|
/**
|
||||||
|
* 媒体服务终端
|
||||||
|
*/
|
||||||
MEDIA("媒体服务"),
|
MEDIA("媒体服务"),
|
||||||
|
/**
|
||||||
|
* 没有界面的摄像头
|
||||||
|
*/
|
||||||
CAMERA("摄像头"),
|
CAMERA("摄像头"),
|
||||||
|
/**
|
||||||
|
* 手机APP、平板APP
|
||||||
|
*/
|
||||||
MOBILE("移动端"),
|
MOBILE("移动端"),
|
||||||
|
/**
|
||||||
|
* 其他智能终端
|
||||||
|
*/
|
||||||
OTHER("其他终端");
|
OTHER("其他终端");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +48,10 @@ public enum ClientType {
|
|||||||
* @return 是否是媒体终端
|
* @return 是否是媒体终端
|
||||||
*/
|
*/
|
||||||
public boolean mediaClient() {
|
public boolean mediaClient() {
|
||||||
return this == WEB || this == CAMERA || this == MOBILE;
|
return
|
||||||
|
this == WEB ||
|
||||||
|
this == CAMERA ||
|
||||||
|
this == MOBILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +67,8 @@ public enum ClientType {
|
|||||||
* @return 类型
|
* @return 类型
|
||||||
*/
|
*/
|
||||||
public static final ClientType of(String value) {
|
public static final ClientType of(String value) {
|
||||||
for (ClientType type : ClientType.values()) {
|
final ClientType[] types = ClientType.values();
|
||||||
|
for (ClientType type : types) {
|
||||||
if(type.name().equalsIgnoreCase(value)) {
|
if(type.name().equalsIgnoreCase(value)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -58,12 +77,17 @@ public enum ClientType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 媒体终端
|
* 媒体终端类型列表
|
||||||
*/
|
*/
|
||||||
public static final ClientType[] MEDIA_CLIENT = Stream.of(ClientType.values()).filter(ClientType::mediaClient).toArray(ClientType[]::new);
|
public static final ClientType[] MEDIA_CLIENT_TYPE
|
||||||
|
=
|
||||||
|
Stream.of(ClientType.values()).filter(ClientType::mediaClient).toArray(ClientType[]::new);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 媒体服务
|
* 媒体服务类型列表
|
||||||
*/
|
*/
|
||||||
public static final ClientType[] MEDIA_SERVER = Stream.of(ClientType.values()).filter(ClientType::mediaServer).toArray(ClientType[]::new);
|
public static final ClientType[] MEDIA_SERVER_TYPE
|
||||||
|
=
|
||||||
|
Stream.of(ClientType.values()).filter(ClientType::mediaServer).toArray(ClientType[]::new);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ public class SocketClient extends ClientAdapter<AsynchronousSocketChannel> {
|
|||||||
|
|
||||||
public SocketClient(SocketProperties socketProperties, AsynchronousSocketChannel instance) {
|
public SocketClient(SocketProperties socketProperties, AsynchronousSocketChannel instance) {
|
||||||
super(socketProperties.getTimeout(), instance);
|
super(socketProperties.getTimeout(), instance);
|
||||||
this.ip = this.clientIp(instance);
|
|
||||||
this.cipher = CipherUtils.buildCipher(Cipher.ENCRYPT_MODE, socketProperties.getEncrypt(), socketProperties.getEncryptSecret());
|
this.cipher = CipherUtils.buildCipher(Cipher.ENCRYPT_MODE, socketProperties.getEncrypt(), socketProperties.getEncryptSecret());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,16 +64,12 @@ public class SocketClient extends ClientAdapter<AsynchronousSocketChannel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* @param instance 终端实例
|
protected String getClientIP(AsynchronousSocketChannel instance) {
|
||||||
*
|
|
||||||
* @return 终端IP
|
|
||||||
*/
|
|
||||||
private String clientIp(AsynchronousSocketChannel instance) {
|
|
||||||
try {
|
try {
|
||||||
return ((InetSocketAddress) instance.getRemoteAddress()).getHostString();
|
return ((InetSocketAddress) instance.getRemoteAddress()).getHostString();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw MessageCodeException.of(e, "无效终端(IP):" + instance);
|
throw MessageCodeException.of(e, "无效终端IP:" + instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,15 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
/**
|
/**
|
||||||
* Socket信令
|
* Socket信令
|
||||||
*
|
*
|
||||||
* TODO:加密
|
|
||||||
*
|
|
||||||
* @author acgist
|
* @author acgist
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SocketSignal {
|
public class SocketSignal {
|
||||||
|
|
||||||
private ClientManager clientManager;
|
private final ClientManager clientManager;
|
||||||
private ProtocolManager protocolManager;
|
private final ProtocolManager protocolManager;
|
||||||
private SocketProperties socketProperties;
|
private final SocketProperties socketProperties;
|
||||||
private PlatformErrorProtocol platformErrorProtocol;
|
private final PlatformErrorProtocol platformErrorProtocol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 线程序号
|
* 线程序号
|
||||||
@@ -45,7 +43,7 @@ public class SocketSignal {
|
|||||||
/**
|
/**
|
||||||
* 服务端通道
|
* 服务端通道
|
||||||
*/
|
*/
|
||||||
private AsynchronousServerSocketChannel channel;
|
private AsynchronousServerSocketChannel server;
|
||||||
|
|
||||||
public SocketSignal(
|
public SocketSignal(
|
||||||
ClientManager clientManager,
|
ClientManager clientManager,
|
||||||
@@ -76,9 +74,9 @@ public class SocketSignal {
|
|||||||
this.newThreadFactory()
|
this.newThreadFactory()
|
||||||
);
|
);
|
||||||
this.group = AsynchronousChannelGroup.withThreadPool(executor);
|
this.group = AsynchronousChannelGroup.withThreadPool(executor);
|
||||||
this.channel = AsynchronousServerSocketChannel.open(this.group);
|
this.server = AsynchronousServerSocketChannel.open(this.group);
|
||||||
this.channel.bind(new InetSocketAddress(host, port));
|
this.server.bind(new InetSocketAddress(host, port));
|
||||||
this.channel.accept(this.channel, new SocketSignalAcceptHandler(
|
this.server.accept(this.server, new SocketSignalAcceptHandler(
|
||||||
this.clientManager,
|
this.clientManager,
|
||||||
this.protocolManager,
|
this.protocolManager,
|
||||||
this.socketProperties,
|
this.socketProperties,
|
||||||
@@ -117,11 +115,9 @@ public class SocketSignal {
|
|||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
log.debug("关闭Socket信令服务:{}", this.channel);
|
log.debug("关闭Socket信令服务:{}", this.server);
|
||||||
CloseableUtils.close(this.channel);
|
CloseableUtils.close(this.server);
|
||||||
if(this.group != null) {
|
CloseableUtils.shutdown(this.group);
|
||||||
this.group.shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public final class SocketSignalAcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {
|
public final class SocketSignalAcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {
|
||||||
|
|
||||||
private ClientManager clientManager;
|
private final ClientManager clientManager;
|
||||||
private ProtocolManager protocolManager;
|
private final ProtocolManager protocolManager;
|
||||||
private SocketProperties socketProperties;
|
private final SocketProperties socketProperties;
|
||||||
private PlatformErrorProtocol platformErrorProtocol;
|
private final PlatformErrorProtocol platformErrorProtocol;
|
||||||
|
|
||||||
public SocketSignalAcceptHandler(
|
public SocketSignalAcceptHandler(
|
||||||
ClientManager clientManager,
|
ClientManager clientManager,
|
||||||
@@ -47,8 +47,8 @@ public final class SocketSignalAcceptHandler implements CompletionHandler<Asynch
|
|||||||
this.clientManager,
|
this.clientManager,
|
||||||
this.protocolManager,
|
this.protocolManager,
|
||||||
this.socketProperties,
|
this.socketProperties,
|
||||||
channel,
|
this.platformErrorProtocol,
|
||||||
this.platformErrorProtocol
|
channel
|
||||||
);
|
);
|
||||||
messageHandler.loopMessage();
|
messageHandler.loopMessage();
|
||||||
log.debug("Socket信令终端连接成功:{}", channel);
|
log.debug("Socket信令终端连接成功:{}", channel);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import javax.crypto.IllegalBlockSizeException;
|
|||||||
|
|
||||||
import com.acgist.taoyao.boot.config.SocketProperties;
|
import com.acgist.taoyao.boot.config.SocketProperties;
|
||||||
import com.acgist.taoyao.boot.model.MessageCodeException;
|
import com.acgist.taoyao.boot.model.MessageCodeException;
|
||||||
import com.acgist.taoyao.boot.utils.CloseableUtils;
|
|
||||||
import com.acgist.taoyao.signal.client.ClientManager;
|
import com.acgist.taoyao.signal.client.ClientManager;
|
||||||
import com.acgist.taoyao.signal.protocol.ProtocolManager;
|
import com.acgist.taoyao.signal.protocol.ProtocolManager;
|
||||||
import com.acgist.taoyao.signal.protocol.platform.PlatformErrorProtocol;
|
import com.acgist.taoyao.signal.protocol.platform.PlatformErrorProtocol;
|
||||||
@@ -27,9 +26,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public final class SocketSignalMessageHandler implements CompletionHandler<Integer, ByteBuffer> {
|
public final class SocketSignalMessageHandler implements CompletionHandler<Integer, ByteBuffer> {
|
||||||
|
|
||||||
private ClientManager clientManager;
|
private final ClientManager clientManager;
|
||||||
private ProtocolManager protocolManager;
|
private final ProtocolManager protocolManager;
|
||||||
private PlatformErrorProtocol platformErrorProtocol;
|
private final PlatformErrorProtocol platformErrorProtocol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息长度
|
* 消息长度
|
||||||
@@ -60,18 +59,18 @@ public final class SocketSignalMessageHandler implements CompletionHandler<Integ
|
|||||||
ClientManager clientManager,
|
ClientManager clientManager,
|
||||||
ProtocolManager protocolManager,
|
ProtocolManager protocolManager,
|
||||||
SocketProperties socketProperties,
|
SocketProperties socketProperties,
|
||||||
AsynchronousSocketChannel channel,
|
PlatformErrorProtocol platformErrorProtocol,
|
||||||
PlatformErrorProtocol platformErrorProtocol
|
AsynchronousSocketChannel channel
|
||||||
) {
|
) {
|
||||||
|
this.clientManager = clientManager;
|
||||||
|
this.protocolManager = protocolManager;
|
||||||
|
this.platformErrorProtocol = platformErrorProtocol;
|
||||||
|
this.channel = channel;
|
||||||
this.messageLength = 0;
|
this.messageLength = 0;
|
||||||
this.bufferSize = socketProperties.getBufferSize();
|
this.bufferSize = socketProperties.getBufferSize();
|
||||||
this.maxBufferSize = socketProperties.getMaxBufferSize();
|
this.maxBufferSize = socketProperties.getMaxBufferSize();
|
||||||
this.cipher = CipherUtils.buildCipher(Cipher.DECRYPT_MODE, socketProperties.getEncrypt(), socketProperties.getEncryptSecret());
|
this.cipher = CipherUtils.buildCipher(Cipher.DECRYPT_MODE, socketProperties.getEncrypt(), socketProperties.getEncryptSecret());
|
||||||
this.buffer = ByteBuffer.allocateDirect(maxBufferSize);
|
this.buffer = ByteBuffer.allocateDirect(maxBufferSize);
|
||||||
this.channel = channel;
|
|
||||||
this.clientManager = clientManager;
|
|
||||||
this.protocolManager = protocolManager;
|
|
||||||
this.platformErrorProtocol = platformErrorProtocol;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +91,6 @@ public final class SocketSignalMessageHandler implements CompletionHandler<Integ
|
|||||||
*/
|
*/
|
||||||
private void close() {
|
private void close() {
|
||||||
log.debug("Socket信令终端关闭:{}", this.channel);
|
log.debug("Socket信令终端关闭:{}", this.channel);
|
||||||
CloseableUtils.close(this.channel);
|
|
||||||
this.clientManager.close(this.channel);
|
this.clientManager.close(this.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public class WebSocketClient extends ClientAdapter<Session> {
|
|||||||
|
|
||||||
public WebSocketClient(long timeout, Session instance) {
|
public WebSocketClient(long timeout, Session instance) {
|
||||||
super(timeout, instance);
|
super(timeout, instance);
|
||||||
this.ip = (String) instance.getUserProperties().get(Constant.IP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,4 +38,9 @@ public class WebSocketClient extends ClientAdapter<Session> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getClientIP(Session instance) {
|
||||||
|
return (String) instance.getUserProperties().get(Constant.IP);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class RoomCreateProtocol extends ProtocolClientAdapter implements Applica
|
|||||||
);
|
);
|
||||||
message.setBody(room.getRoomStatus());
|
message.setBody(room.getRoomStatus());
|
||||||
// 通知媒体终端
|
// 通知媒体终端
|
||||||
this.clientManager.broadcast(message, ClientType.MEDIA_CLIENT);
|
this.clientManager.broadcast(message, ClientType.MEDIA_CLIENT_TYPE);
|
||||||
} else {
|
} else {
|
||||||
this.logNoAdapter(clientType);
|
this.logNoAdapter(clientType);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user