simple refactoring, upgrade CLI
This commit is contained in:
+143
-72
@@ -5,16 +5,14 @@ import io.netty.channel.nio.NioEventLoopGroup
|
|||||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||||
import io.netty.handler.codec.http.*
|
import io.netty.handler.codec.http.*
|
||||||
import objects.Car
|
import objects.Car
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by user on 7/6/16.
|
* Created by user on 7/6/16.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//хардкод это плохо, но пока так...:)
|
val timeDeltaToDrop = 600000//time in ms. if car is inactive more than this time, server drop session with car
|
||||||
val carServerPort: Int = 7925
|
val carServerPort: Int = 7925
|
||||||
val webServerPort: Int = 7926
|
val webServerPort: Int = 7926
|
||||||
val handlerThreadsCount: Int = 100
|
val handlerThreadsCount: Int = 100
|
||||||
@@ -25,8 +23,103 @@ val connectUrl = "/connect"
|
|||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
||||||
println("car server started")
|
val carServer = getCarServerThread()
|
||||||
val serverCarThread = thread {
|
// val webServer = getWebServerThread()
|
||||||
|
val carsDestroy = getCarsDestroyThread()
|
||||||
|
carServer.start()
|
||||||
|
carsDestroy.start()
|
||||||
|
|
||||||
|
//CL user interface
|
||||||
|
val helpString = "available commands:\n" +
|
||||||
|
"cars - get list of connected cars\n" +
|
||||||
|
"route [car_id] - setting a route for car with car id.\n" +
|
||||||
|
"refloc - refresh all car locations\n" +
|
||||||
|
"stop - exit from this interface and stop all servers\n"
|
||||||
|
println(helpString)
|
||||||
|
val routeRegex = Regex("route [0-9]{1,10}")
|
||||||
|
val environment = objects.Environment.instance
|
||||||
|
while (true) {
|
||||||
|
val readedString = readLine()
|
||||||
|
if (readedString == null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (readedString.equals("cars", true)) {
|
||||||
|
synchronized(environment, {
|
||||||
|
println(environment.map.values)
|
||||||
|
})
|
||||||
|
} else if (routeRegex.matches(readedString)) {
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
try {
|
||||||
|
id = readedString.split(" ")[1].toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
println("error in converting id to int type")
|
||||||
|
println(helpString)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val car: Car? =
|
||||||
|
synchronized(environment, {
|
||||||
|
environment.map.get(id)
|
||||||
|
})
|
||||||
|
if (car != null) {
|
||||||
|
println("print way points in polar coordinate als [distance] [rotation angle] in metres and degrees. after enter all points print \"done\". for reset route print \"reset\"")
|
||||||
|
println("e.g. for move from (x,y) to (x+1,y) and back to (x,y) u need enter: 1 0[press enter] 1 180[press enter] done")
|
||||||
|
val routeBuilder = RouteRequest.BuilderRouteRequest()
|
||||||
|
while (true) {
|
||||||
|
val wayPointInputString = readLine()!!
|
||||||
|
if (wayPointInputString.equals("reset", true)) {
|
||||||
|
break
|
||||||
|
} else if (wayPointInputString.equals("done", true)) {
|
||||||
|
val requestBytes = ByteArrayOutputStream()
|
||||||
|
routeBuilder.build().writeTo(CodedOutputStream(requestBytes))
|
||||||
|
val request = DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, setRouteUrl, Unpooled.copiedBuffer(requestBytes.toByteArray()))
|
||||||
|
request.headers().set(HttpHeaderNames.HOST, car.host)
|
||||||
|
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||||
|
request.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
|
||||||
|
Client.sendRequest(request, car.host, car.port, id)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
val wayPointData = wayPointInputString.split(" ")
|
||||||
|
val distance: Double
|
||||||
|
val angle: Double
|
||||||
|
try {
|
||||||
|
distance = wayPointData[0].toDouble()
|
||||||
|
angle = wayPointData[1].toDouble()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
println("error in convertion angle or distance to double. try again")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val wayPoint = RouteRequest.WayPoint.BuilderWayPoint().setDistance(distance).setAngle_delta(angle).build()
|
||||||
|
routeBuilder.addWayPoint(wayPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("car with id=$id not found")
|
||||||
|
}
|
||||||
|
} else if (readedString.equals("refloc", true)) {
|
||||||
|
val cars = synchronized(environment, { environment.map.values })
|
||||||
|
for (car in cars) {
|
||||||
|
val request = DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, getLocationUrl, Unpooled.EMPTY_BUFFER)
|
||||||
|
request.headers().set(HttpHeaderNames.HOST, car.host)
|
||||||
|
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||||
|
request.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
|
||||||
|
Client.sendRequest(request, car.host, car.port, car.uid)
|
||||||
|
}
|
||||||
|
} else if (readedString.equals("quit")) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
println("not supported command: $readedString")
|
||||||
|
println(helpString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
carsDestroy.interrupt()
|
||||||
|
carServer.interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCarServerThread(): Thread {
|
||||||
|
return thread(false, false, null, "carServer", -1, {
|
||||||
|
println("car server started")
|
||||||
val bossGroup = NioEventLoopGroup(1)
|
val bossGroup = NioEventLoopGroup(1)
|
||||||
val workerGroup = NioEventLoopGroup()
|
val workerGroup = NioEventLoopGroup()
|
||||||
val b = ServerBootstrap()
|
val b = ServerBootstrap()
|
||||||
@@ -38,82 +131,60 @@ fun main(args: Array<String>) {
|
|||||||
val channel = b.bind(carServerPort).sync().channel()
|
val channel = b.bind(carServerPort).sync().channel()
|
||||||
channel.closeFuture().sync()
|
channel.closeFuture().sync()
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
println("car server stoped!")
|
println("car server stoped")
|
||||||
e.printStackTrace()
|
|
||||||
} finally {
|
} finally {
|
||||||
bossGroup.shutdownGracefully()
|
bossGroup.shutdownGracefully()
|
||||||
workerGroup.shutdownGracefully()
|
workerGroup.shutdownGracefully()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// val serverWebThread = thread {
|
fun getWebServerThread(): Thread {
|
||||||
// val bossGroup = NioEventLoopGroup(1)
|
return thread(false, false, null, "webServer", -1, {
|
||||||
// val workerGroup = NioEventLoopGroup()
|
println("web server started")
|
||||||
// val b = ServerBootstrap()
|
val bossGroup = NioEventLoopGroup(1)
|
||||||
// b.group(bossGroup, workerGroup)
|
val workerGroup = NioEventLoopGroup()
|
||||||
// .channel(NioServerSocketChannel().javaClass)
|
val b = ServerBootstrap()
|
||||||
// .childHandler(web.server.Initializer(handlerThreadsCount))
|
b.group(bossGroup, workerGroup)
|
||||||
//
|
.channel(NioServerSocketChannel().javaClass)
|
||||||
// try {
|
.childHandler(web.server.Initializer(handlerThreadsCount))
|
||||||
// val channel = b.bind(webServerPort).sync().channel()
|
|
||||||
// channel.closeFuture().sync()
|
|
||||||
// } catch (e: InterruptedException) {
|
|
||||||
// println("web server stoped!")
|
|
||||||
// } finally {
|
|
||||||
// bossGroup.shutdownGracefully()
|
|
||||||
// workerGroup.shutdownGracefully()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
val serverWebThread = thread {
|
|
||||||
val environment = objects.Environment.instance
|
|
||||||
val scanner = Scanner(System.`in`)
|
|
||||||
while (scanner.hasNext()) {
|
|
||||||
val s = scanner.nextLine()
|
|
||||||
if (s.equals("cars", true)) {
|
|
||||||
synchronized(environment, {
|
|
||||||
println(environment.map.values)
|
|
||||||
})
|
|
||||||
} else if (s.equals("pathto", true)) {
|
|
||||||
println("print datas. format: [string id] [double distance] [double angle]")
|
|
||||||
val data = scanner.nextLine()
|
|
||||||
val datas = data.split(" ")
|
|
||||||
try {
|
|
||||||
val id = datas[0].toInt()
|
|
||||||
val distance = datas[1].toDouble()
|
|
||||||
val angle = datas[2].toDouble()
|
|
||||||
|
|
||||||
val car: Car? =
|
try {
|
||||||
synchronized(environment, {
|
val channel = b.bind(webServerPort).sync().channel()
|
||||||
environment.map.get(id)
|
channel.closeFuture().sync()
|
||||||
})
|
} catch (e: InterruptedException) {
|
||||||
if (car != null) {
|
println("web server stoped")
|
||||||
val wayPoint = RouteRequest.WayPoint.BuilderWayPoint().setDistance(distance).setAngle_delta(angle).build()
|
} finally {
|
||||||
val route = RouteRequest.BuilderRouteRequest().addWayPoint(wayPoint).build()
|
bossGroup.shutdownGracefully()
|
||||||
val requestBytes = ByteArrayOutputStream()
|
workerGroup.shutdownGracefully()
|
||||||
route.writeTo(CodedOutputStream(requestBytes))
|
}
|
||||||
val request = DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, setRouteUrl, Unpooled.copiedBuffer(requestBytes.toByteArray()))
|
})
|
||||||
request.headers().set(HttpHeaderNames.HOST, car.host)
|
}
|
||||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
|
||||||
request.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
|
//thread, that dropped inactive car. every minute check all connected cars and if car is inactive more, als timeDeltaToDrop then remove this car.
|
||||||
Client.sendRequest(request, car.host, car.port, id)
|
fun getCarsDestroyThread(): Thread {
|
||||||
|
return thread(false, false, null, "dropCar", -1, {
|
||||||
|
var stoped = false
|
||||||
|
while (!stoped) {
|
||||||
|
val environment = objects.Environment.instance
|
||||||
|
synchronized(environment, {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
val keysToRemove = mutableListOf<Int>();
|
||||||
|
for (keyValue in environment.map) {
|
||||||
|
if ((keyValue.value.lastAction + timeDeltaToDrop) < currentTime) {
|
||||||
|
keysToRemove.add(keyValue.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
println("incorrect format")
|
|
||||||
}
|
}
|
||||||
} else if (s.equals("refloc", true)) {
|
for (key in keysToRemove) {
|
||||||
val cars = synchronized(environment, { environment.map.values })
|
environment.map.remove(key)
|
||||||
for (car in cars) {
|
|
||||||
val request = DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, getLocationUrl, Unpooled.EMPTY_BUFFER)
|
|
||||||
request.headers().set(HttpHeaderNames.HOST, car.host)
|
|
||||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
|
||||||
request.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
|
|
||||||
Client.sendRequest(request, car.host, car.port, car.uid)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
Thread.sleep(60000)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
println("thread for destroy cars stoped")
|
||||||
|
stoped = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,6 @@ object Client {
|
|||||||
|
|
||||||
fun sendRequest(request: HttpRequest, host: String, port: Int, carUid: Int) {
|
fun sendRequest(request: HttpRequest, host: String, port: Int, carUid: Int) {
|
||||||
try {
|
try {
|
||||||
println("sending to $host:$port")
|
|
||||||
bootstrap.attr(AttributeKey.valueOf<String>("url"), request.uri())
|
bootstrap.attr(AttributeKey.valueOf<String>("url"), request.uri())
|
||||||
bootstrap.attr(AttributeKey.valueOf<Int>("uid"), carUid)
|
bootstrap.attr(AttributeKey.valueOf<Int>("uid"), carUid)
|
||||||
val ch = bootstrap.connect(host, port).sync().channel()
|
val ch = bootstrap.connect(host, port).sync().channel()
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package car.server
|
package car.server
|
||||||
|
|
||||||
|
import CodedInputStream
|
||||||
|
import CodedOutputStream
|
||||||
|
import ConnectionRequest
|
||||||
|
import ConnectionResponse
|
||||||
|
import InvalidProtocolBufferException
|
||||||
|
import RouteDoneRequest
|
||||||
|
import RouteDoneResponse
|
||||||
import connectUrl
|
import connectUrl
|
||||||
import io.netty.buffer.Unpooled
|
import io.netty.buffer.Unpooled
|
||||||
import io.netty.channel.ChannelFutureListener
|
import io.netty.channel.ChannelFutureListener
|
||||||
@@ -8,14 +15,8 @@ import io.netty.channel.SimpleChannelInboundHandler
|
|||||||
import io.netty.handler.codec.http.*
|
import io.netty.handler.codec.http.*
|
||||||
import objects.Environment
|
import objects.Environment
|
||||||
import routeDoneUrl
|
import routeDoneUrl
|
||||||
import ConnectionRequest
|
|
||||||
import ConnectionResponse
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import CodedInputStream
|
|
||||||
import CodedOutputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import RouteDoneRequest
|
|
||||||
import InvalidProtocolBufferException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by user on 7/6/16.
|
* Created by user on 7/6/16.
|
||||||
@@ -37,13 +38,12 @@ class Handler : SimpleChannelInboundHandler<Any>() {
|
|||||||
data.mergeFrom(CodedInputStream(ByteArrayInputStream(contentBytes)))
|
data.mergeFrom(CodedInputStream(ByteArrayInputStream(contentBytes)))
|
||||||
} catch (e: InvalidProtocolBufferException) {
|
} catch (e: InvalidProtocolBufferException) {
|
||||||
success = false;
|
success = false;
|
||||||
|
ConnectionResponse.BuilderConnectionResponse().setCode(1).setErrorMsg("invalid protobuf request").build().writeTo(CodedOutputStream(answer))
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
val uid = environment.connectCar(data.ip, data.port)
|
val uid = environment.connectCar(data.ip, data.port)
|
||||||
|
|
||||||
ConnectionResponse.BuilderConnectionResponse().setUid(uid).build().writeTo(CodedOutputStream(answer))
|
ConnectionResponse.BuilderConnectionResponse().setUid(uid).build().writeTo(CodedOutputStream(answer))
|
||||||
}
|
}
|
||||||
//todo return connection error
|
|
||||||
}
|
}
|
||||||
routeDoneUrl -> {
|
routeDoneUrl -> {
|
||||||
val data = RouteDoneRequest.BuilderRouteDoneRequest().build()
|
val data = RouteDoneRequest.BuilderRouteDoneRequest().build()
|
||||||
@@ -58,11 +58,13 @@ class Handler : SimpleChannelInboundHandler<Any>() {
|
|||||||
val car = environment.map.get(id)
|
val car = environment.map.get(id)
|
||||||
if (car != null) {
|
if (car != null) {
|
||||||
car.free = true
|
car.free = true
|
||||||
|
car.lastAction = System.currentTimeMillis()
|
||||||
|
RouteDoneResponse.BuilderRouteDoneResponse().setCode(0).setErrorMsg("").build().writeTo(CodedOutputStream(answer))
|
||||||
} else {
|
} else {
|
||||||
success = false
|
success = false
|
||||||
|
RouteDoneResponse.BuilderRouteDoneResponse().setCode(2).setErrorMsg("car not found by id").build().writeTo(CodedOutputStream(answer))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//todo return connection error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package car.server
|
|||||||
import io.netty.channel.ChannelInitializer
|
import io.netty.channel.ChannelInitializer
|
||||||
import io.netty.channel.ChannelPipeline
|
import io.netty.channel.ChannelPipeline
|
||||||
import io.netty.channel.socket.SocketChannel
|
import io.netty.channel.socket.SocketChannel
|
||||||
import io.netty.handler.codec.http.HttpRequestDecoder
|
|
||||||
import io.netty.handler.codec.http.HttpResponseEncoder
|
|
||||||
import io.netty.handler.codec.http.HttpServerCodec
|
import io.netty.handler.codec.http.HttpServerCodec
|
||||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||||
import io.netty.util.concurrent.EventExecutorGroup
|
import io.netty.util.concurrent.EventExecutorGroup
|
||||||
@@ -24,8 +22,6 @@ class Initializer : ChannelInitializer<SocketChannel> {
|
|||||||
val p: ChannelPipeline = channel.pipeline()
|
val p: ChannelPipeline = channel.pipeline()
|
||||||
|
|
||||||
p.addLast(HttpServerCodec())
|
p.addLast(HttpServerCodec())
|
||||||
// p.addLast(HttpRequestDecoder())
|
|
||||||
// p.addLast(HttpResponseEncoder())
|
|
||||||
p.addLast(group, Handler())
|
p.addLast(group, Handler())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import car.client.Client
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by user on 7/7/16.
|
* Created by user on 7/7/16.
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +10,7 @@ class Car constructor(uid: Int, host: String, port: Int) {
|
|||||||
val port: Int
|
val port: Int
|
||||||
|
|
||||||
var free: Boolean
|
var free: Boolean
|
||||||
|
var lastAction: Long
|
||||||
|
|
||||||
var x: Double
|
var x: Double
|
||||||
var y: Double
|
var y: Double
|
||||||
@@ -23,6 +22,7 @@ class Car constructor(uid: Int, host: String, port: Int) {
|
|||||||
this.free = true
|
this.free = true
|
||||||
x = 0.toDouble()
|
x = 0.toDouble()
|
||||||
y = 0.toDouble()
|
y = 0.toDouble()
|
||||||
|
this.lastAction = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import java.util.*
|
import java.util.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by user on 7/7/16.
|
* Created by user on 7/7/16.
|
||||||
@@ -19,21 +19,23 @@ class Environment private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun connectCar(host:String, port:Int):Int {
|
fun connectCar(host: String, port: Int): Int {
|
||||||
//todo учетка памяти, тут машинки добавляются,но никогда не удаляются. Если включать и выключать одну и ту же машинку, то либо сервер зациклится, либо out of memory
|
val uid = getNewUid()
|
||||||
//todo в зависимости от того, что кончится раньше, память или айдишники:) Может сделать поток, который мониторит раз в N минут все машинки и дропает те, которые не активны более какого-то времени?
|
map.put(uid, Car(uid, host, port))
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNewUid(): Int {
|
||||||
var unique = false
|
var unique = false
|
||||||
val random = Random()
|
val random = Random()
|
||||||
var uid:Int = 0
|
var uid: Int = 0
|
||||||
while (!unique) {
|
while (!unique) {
|
||||||
uid = random.nextInt(1000000)
|
uid = random.nextInt(1000000)
|
||||||
if (map.get(uid) == null) {
|
if (map.get(uid) == null) {
|
||||||
unique = true;
|
unique = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map.put(uid, Car(uid, host, port))
|
return uid;
|
||||||
return uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package web.server
|
|||||||
import io.netty.channel.ChannelInitializer
|
import io.netty.channel.ChannelInitializer
|
||||||
import io.netty.channel.ChannelPipeline
|
import io.netty.channel.ChannelPipeline
|
||||||
import io.netty.channel.socket.SocketChannel
|
import io.netty.channel.socket.SocketChannel
|
||||||
import io.netty.handler.codec.http.HttpRequestDecoder
|
|
||||||
import io.netty.handler.codec.http.HttpResponseEncoder
|
|
||||||
import io.netty.handler.codec.http.HttpServerCodec
|
import io.netty.handler.codec.http.HttpServerCodec
|
||||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||||
import io.netty.util.concurrent.EventExecutorGroup
|
import io.netty.util.concurrent.EventExecutorGroup
|
||||||
|
|||||||
Reference in New Issue
Block a user