ing
This commit is contained in:
parent
6a5cb736ed
commit
35ba58c220
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"server": {
|
"server": {
|
||||||
"addr": ":80",
|
"addr": ":18081",
|
||||||
"tls": false
|
"tls": false
|
||||||
},
|
},
|
||||||
"websocket": {
|
"websocket": {
|
||||||
|
3
main.go
3
main.go
@ -7,7 +7,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/config"
|
"git.loafle.net/overflow/overflow_service_websocket/config"
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/protocol/jsonrpc"
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/server"
|
"git.loafle.net/overflow/overflow_service_websocket/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ func main() {
|
|||||||
// Config TLS
|
// Config TLS
|
||||||
|
|
||||||
ws := server.New(sConfig)
|
ws := server.New(sConfig)
|
||||||
ws.RegistProtocol("jsonrpc", jsonrpc.NewHandler())
|
|
||||||
ws.OnConnection(func(c server.Client) {
|
ws.OnConnection(func(c server.Client) {
|
||||||
log.Println("Client have been connected")
|
log.Println("Client have been connected")
|
||||||
c.OnDisconnect(func(c server.Client) {
|
c.OnDisconnect(func(c server.Client) {
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package protocol
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
type ErrorCode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
E_PARSE ErrorCode = -32700
|
|
||||||
E_INVALID_REQ ErrorCode = -32600
|
|
||||||
E_NOT_FOUND_METHOD ErrorCode = -32601
|
|
||||||
E_INVALID_PARAMS ErrorCode = -32602
|
|
||||||
E_INTERNAL ErrorCode = -32603
|
|
||||||
E_SERVER ErrorCode = -32000
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNullResult = errors.New("result is null")
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Code ErrorCode `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler interface {
|
|
||||||
Handle([]byte) ([]byte, *Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewError(code ErrorCode, err error, data interface{}) *Error {
|
|
||||||
e := &Error{
|
|
||||||
Code: code,
|
|
||||||
Message: err.Error(),
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package jsonrpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
serverGrpc "git.loafle.net/overflow/overflow_api_server/golang"
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/config"
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/pool"
|
|
||||||
grpcPool "git.loafle.net/overflow/overflow_service_websocket/pool/grpc"
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params []string `json:"params,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler returns a new JSON RPC handler.
|
|
||||||
func NewHandler() protocol.Handler {
|
|
||||||
c := config.GetConfig().GRpc
|
|
||||||
|
|
||||||
addr := c.Addr
|
|
||||||
|
|
||||||
servicePool, err := grpcPool.New(1, 5,
|
|
||||||
func(conn *grpc.ClientConn) (interface{}, error) {
|
|
||||||
return serverGrpc.NewOverflowApiServerClient(conn), nil
|
|
||||||
},
|
|
||||||
func() (*grpc.ClientConn, error) {
|
|
||||||
return grpc.Dial(addr, grpc.WithInsecure())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if nil != err {
|
|
||||||
log.Fatal(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
h := &handler{
|
|
||||||
pool: servicePool,
|
|
||||||
}
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Codec creates a CodecRequest to process each request.
|
|
||||||
type handler struct {
|
|
||||||
pool pool.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest returns a CodecRequest.
|
|
||||||
func (h *handler) Handle(data []byte) ([]byte, *protocol.Error) {
|
|
||||||
req := Request{}
|
|
||||||
err := json.Unmarshal(data, &req)
|
|
||||||
if nil != err {
|
|
||||||
return nil, protocol.NewError(protocol.E_PARSE, err, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(req.Method, ".")
|
|
||||||
|
|
||||||
si := &serverGrpc.ServerInput{
|
|
||||||
Target: parts[0],
|
|
||||||
Method: parts[1],
|
|
||||||
Params: req.Params,
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := h.pool.Get()
|
|
||||||
if nil != err {
|
|
||||||
return nil, protocol.NewError(protocol.E_INTERNAL, err, nil)
|
|
||||||
}
|
|
||||||
defer h.pool.Put(c)
|
|
||||||
client := c.(serverGrpc.OverflowApiServerClient)
|
|
||||||
out, err := client.Exec(context.Background(), si)
|
|
||||||
if err != nil {
|
|
||||||
return nil, protocol.NewError(protocol.E_SERVER, err, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &Response{
|
|
||||||
Result: out.Result,
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, protocol.NewError(protocol.E_INTERNAL, err, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
@ -1,15 +1,17 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/protocol"
|
serverGrpc "git.loafle.net/overflow/overflow_api_server/golang"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -168,17 +170,26 @@ func (c *client) onMessageReceived(messageType int, r io.Reader) {
|
|||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := c.server.ProtocolHandler(req.Protocol)
|
parts := strings.Split(req.Method, ".")
|
||||||
if nil == h {
|
|
||||||
|
|
||||||
|
si := &serverGrpc.ServerInput{
|
||||||
|
Target: parts[0],
|
||||||
|
Method: parts[1],
|
||||||
|
Params: req.Params,
|
||||||
}
|
}
|
||||||
|
|
||||||
hr, perr := h.Handle(*req.Body)
|
grpcPool, err := c.server.GetGRPCPool().Get()
|
||||||
if perr != nil {
|
if nil != err {
|
||||||
c.writeError(req, perr)
|
c.writeError(req, NewError(E_INTERNAL, err, nil))
|
||||||
|
}
|
||||||
|
defer c.server.GetGRPCPool().Put(grpcPool)
|
||||||
|
grpcClient := grpcPool.(serverGrpc.OverflowApiServerClient)
|
||||||
|
out, err := grpcClient.Exec(context.Background(), si)
|
||||||
|
if err != nil {
|
||||||
|
c.writeError(req, NewError(E_SERVER, err, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.writeResult(req, hr)
|
c.writeResult(req, out.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) fireError(err error) {
|
func (c *client) fireError(err error) {
|
||||||
@ -187,7 +198,7 @@ func (c *client) fireError(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) writeResult(r *Request, data []byte) {
|
func (c *client) writeResult(r *Request, result string) {
|
||||||
c.writeMTX.Lock()
|
c.writeMTX.Lock()
|
||||||
if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
|
if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
|
||||||
// set the write deadline based on the configuration
|
// set the write deadline based on the configuration
|
||||||
@ -195,28 +206,27 @@ func (c *client) writeResult(r *Request, data []byte) {
|
|||||||
log.Println(fmt.Errorf("%v", err))
|
log.Println(fmt.Errorf("%v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
raw := json.RawMessage(data)
|
|
||||||
|
|
||||||
res := &Response{
|
res := &Response{
|
||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Body: &raw,
|
Result: &result,
|
||||||
Error: nil,
|
Error: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
jRes, err := json.Marshal(res)
|
jRes, err := json.Marshal(res)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
log.Println(fmt.Errorf("%v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.conn.WriteMessage(c.messageType, jRes)
|
err = c.conn.WriteMessage(c.messageType, jRes)
|
||||||
c.writeMTX.Unlock()
|
c.writeMTX.Unlock()
|
||||||
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
c.Disconnect()
|
_ = c.Disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) writeError(r *Request, perr *protocol.Error) {
|
func (c *client) writeError(r *Request, perr *Error) {
|
||||||
c.writeMTX.Lock()
|
c.writeMTX.Lock()
|
||||||
if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
|
if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
|
||||||
// set the write deadline based on the configuration
|
// set the write deadline based on the configuration
|
||||||
@ -227,18 +237,19 @@ func (c *client) writeError(r *Request, perr *protocol.Error) {
|
|||||||
res := &Response{
|
res := &Response{
|
||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Body: nil,
|
Result: nil,
|
||||||
Error: perr,
|
Error: perr,
|
||||||
}
|
}
|
||||||
|
|
||||||
jRes, err := json.Marshal(res)
|
jRes, err := json.Marshal(res)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
log.Println(fmt.Errorf("%v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.conn.WriteMessage(c.messageType, jRes)
|
err = c.conn.WriteMessage(c.messageType, jRes)
|
||||||
c.writeMTX.Unlock()
|
c.writeMTX.Unlock()
|
||||||
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
c.Disconnect()
|
_ = c.Disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,49 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/protocol"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ErrorCode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
E_PARSE ErrorCode = -32700
|
||||||
|
E_INVALID_REQ ErrorCode = -32600
|
||||||
|
E_NOT_FOUND_METHOD ErrorCode = -32601
|
||||||
|
E_INVALID_PARAMS ErrorCode = -32602
|
||||||
|
E_INTERNAL ErrorCode = -32603
|
||||||
|
E_SERVER ErrorCode = -32000
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNullResult = errors.New("result is null")
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code ErrorCode `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Request is protocol which Websocket Request
|
// Request is protocol which Websocket Request
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
ID *json.RawMessage `json:"id"`
|
Method string `json:"method"`
|
||||||
Body *json.RawMessage `json:"body"`
|
Params []string `json:"params,omitempty"`
|
||||||
|
ID *json.RawMessage `json:"id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response is protocol which Websocket Response
|
// Response is protocol which Websocket Response
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
ID *json.RawMessage `json:"id"`
|
Result *string `json:"result,omitempty"`
|
||||||
Error *protocol.Error `json:"error,omitempty"`
|
Error *Error `json:"error,omitempty"`
|
||||||
Body *json.RawMessage `json:"body"`
|
ID *json.RawMessage `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(code ErrorCode, err error, data interface{}) *Error {
|
||||||
|
e := &Error{
|
||||||
|
Code: code,
|
||||||
|
Message: err.Error(),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.loafle.net/overflow/overflow_service_websocket/protocol"
|
serverGrpc "git.loafle.net/overflow/overflow_api_server/golang"
|
||||||
|
"git.loafle.net/overflow/overflow_service_websocket/config"
|
||||||
|
"git.loafle.net/overflow/overflow_service_websocket/pool"
|
||||||
|
grpcPool "git.loafle.net/overflow/overflow_service_websocket/pool/grpc"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -20,8 +23,7 @@ type (
|
|||||||
// listens on the config's port, the critical part is the event OnConnection
|
// listens on the config's port, the critical part is the event OnConnection
|
||||||
type Server interface {
|
type Server interface {
|
||||||
GetOptions() *Options
|
GetOptions() *Options
|
||||||
RegistProtocol(protocol string, h protocol.Handler)
|
GetGRPCPool() pool.Pool
|
||||||
ProtocolHandler(protocol string) protocol.Handler
|
|
||||||
HTTPHandler() http.Handler
|
HTTPHandler() http.Handler
|
||||||
HandleConnection(*http.Request, Connection)
|
HandleConnection(*http.Request, Connection)
|
||||||
OnConnection(cb OnConnectionFunc)
|
OnConnection(cb OnConnectionFunc)
|
||||||
@ -35,13 +37,9 @@ type server struct {
|
|||||||
clients map[string]Client
|
clients map[string]Client
|
||||||
clientMTX sync.Mutex
|
clientMTX sync.Mutex
|
||||||
onConnectionListeners []OnConnectionFunc
|
onConnectionListeners []OnConnectionFunc
|
||||||
protocols map[string]protocol.Handler
|
grpcPool pool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Server = &server{}
|
|
||||||
|
|
||||||
var defaultServer = newServer(nil)
|
|
||||||
|
|
||||||
// server implementation
|
// server implementation
|
||||||
|
|
||||||
// New creates a websocket server and returns it
|
// New creates a websocket server and returns it
|
||||||
@ -55,7 +53,6 @@ func newServer(o *Options) Server {
|
|||||||
s := &server{
|
s := &server{
|
||||||
clients: make(map[string]Client, 100),
|
clients: make(map[string]Client, 100),
|
||||||
onConnectionListeners: make([]OnConnectionFunc, 0),
|
onConnectionListeners: make([]OnConnectionFunc, 0),
|
||||||
protocols: make(map[string]protocol.Handler, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if nil == o {
|
if nil == o {
|
||||||
@ -65,37 +62,31 @@ func newServer(o *Options) Server {
|
|||||||
o.Validate()
|
o.Validate()
|
||||||
|
|
||||||
s.options = o
|
s.options = o
|
||||||
|
|
||||||
|
pool, err := grpcPool.New(1, 5,
|
||||||
|
func(conn *grpc.ClientConn) (interface{}, error) {
|
||||||
|
return serverGrpc.NewOverflowApiServerClient(conn), nil
|
||||||
|
},
|
||||||
|
func() (*grpc.ClientConn, error) {
|
||||||
|
return grpc.Dial(config.GetConfig().GRpc.Addr, grpc.WithInsecure())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if nil != err {
|
||||||
|
log.Fatal(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.grpcPool = pool
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegistProtocol(protocol string, h protocol.Handler) {
|
|
||||||
defaultServer.RegistProtocol(protocol, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) RegistProtocol(protocol string, h protocol.Handler) {
|
|
||||||
s.protocols[strings.ToLower(protocol)] = h
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProtocolHandler(protocol string) protocol.Handler {
|
|
||||||
return defaultServer.ProtocolHandler(protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) ProtocolHandler(protocol string) protocol.Handler {
|
|
||||||
return s.protocols[protocol]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOptions is function that get option values
|
|
||||||
func GetOptions() *Options {
|
|
||||||
return defaultServer.GetOptions()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) GetOptions() *Options {
|
func (s *server) GetOptions() *Options {
|
||||||
return s.options
|
return s.options
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is the function that used on http request
|
func (s *server) GetGRPCPool() pool.Pool {
|
||||||
func HTTPHandler() http.Handler {
|
return s.grpcPool
|
||||||
return defaultServer.HTTPHandler()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) HTTPHandler() http.Handler {
|
func (s *server) HTTPHandler() http.Handler {
|
||||||
@ -118,10 +109,6 @@ func (s *server) HTTPHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleConnection(r *http.Request, conn Connection) {
|
|
||||||
defaultServer.HandleConnection(r, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) HandleConnection(r *http.Request, conn Connection) {
|
func (s *server) HandleConnection(r *http.Request, conn Connection) {
|
||||||
clientID := s.options.IDGenerator(r)
|
clientID := s.options.IDGenerator(r)
|
||||||
c := newClient(s, r, conn, clientID)
|
c := newClient(s, r, conn, clientID)
|
||||||
@ -138,39 +125,19 @@ func (s *server) HandleConnection(r *http.Request, conn Connection) {
|
|||||||
c.initialize()
|
c.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnConnection is function that add the callback when client is connected to default Server
|
|
||||||
func OnConnection(cb OnConnectionFunc) {
|
|
||||||
defaultServer.OnConnection(cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) OnConnection(cb OnConnectionFunc) {
|
func (s *server) OnConnection(cb OnConnectionFunc) {
|
||||||
s.onConnectionListeners = append(s.onConnectionListeners, cb)
|
s.onConnectionListeners = append(s.onConnectionListeners, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConnected is function that check client is connect
|
|
||||||
func IsConnected(clientID string) bool {
|
|
||||||
return defaultServer.IsConnected(clientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) IsConnected(clientID string) bool {
|
func (s *server) IsConnected(clientID string) bool {
|
||||||
soc := s.clients[clientID]
|
soc := s.clients[clientID]
|
||||||
return soc != nil
|
return soc != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClient is function that return client instance
|
|
||||||
func GetClient(clientID string) Client {
|
|
||||||
return defaultServer.GetClient(clientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) GetClient(clientID string) Client {
|
func (s *server) GetClient(clientID string) Client {
|
||||||
return s.clients[clientID]
|
return s.clients[clientID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect is function that disconnect a client
|
|
||||||
func Disconnect(clientID string) error {
|
|
||||||
return defaultServer.Disconnect(clientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) Disconnect(clientID string) error {
|
func (s *server) Disconnect(clientID string) error {
|
||||||
c := s.clients[clientID]
|
c := s.clients[clientID]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user