Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
30609135a4 | |||
84167d359e | |||
145a0f4aa6 | |||
|
b2b4d6a5a3 | ||
|
f17afa7950 | ||
bb763c71b5 | |||
622a79bd06 | |||
|
0eb3c0b452 |
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@@ -2,6 +2,7 @@
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"postUpdateOptions": ["gomodTidy"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
|
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/unistack-org/micro/v3 v3.3.13
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d
|
||||
)
|
||||
|
||||
//replace github.com/unistack-org/micro/v3 => ../../micro
|
||||
|
47
go.sum
47
go.sum
@@ -6,57 +6,16 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||
github.com/unistack-org/micro/v3 v3.3.0 h1:pEj/8QVFzMlNMEL//q/Te8qgG+XI6LTYIQrb6hMymgk=
|
||||
github.com/unistack-org/micro/v3 v3.3.0/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8=
|
||||
github.com/unistack-org/micro/v3 v3.3.1 h1:2KUgb5THy5GUjYgMcOf3TXYIJ0h2RbJHb2j6eeWdRx0=
|
||||
github.com/unistack-org/micro/v3 v3.3.1/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8=
|
||||
github.com/unistack-org/micro/v3 v3.3.2 h1:1r7fmaobJVZBKDhIewSBy8R4H/6YazQFetsDgULV6Mw=
|
||||
github.com/unistack-org/micro/v3 v3.3.2/go.mod h1:tX95c0Qx4w6oqU7qKThs9lya9P507BdZ29MsTVDmU6w=
|
||||
github.com/unistack-org/micro/v3 v3.3.4 h1:wSYjL1sfrqtMmgFfYKylrKpAepMHrPcOpe10hvQAKuo=
|
||||
github.com/unistack-org/micro/v3 v3.3.4/go.mod h1:tX95c0Qx4w6oqU7qKThs9lya9P507BdZ29MsTVDmU6w=
|
||||
github.com/unistack-org/micro/v3 v3.3.7 h1:AKQEevrlT2aqHYRFGd6BEFNnurHLvJyqvWvLCFP7iX4=
|
||||
github.com/unistack-org/micro/v3 v3.3.7/go.mod h1:M3RGgKZX1OlTn0QB+3R1tlSoGwmEg6rb/Isr0lLYmjQ=
|
||||
github.com/unistack-org/micro/v3 v3.3.8 h1:4e4gWuZI/cRUGZ6ZZpdrIDOF2MLO6cjHxeFfZzy610Y=
|
||||
github.com/unistack-org/micro/v3 v3.3.8/go.mod h1:M3RGgKZX1OlTn0QB+3R1tlSoGwmEg6rb/Isr0lLYmjQ=
|
||||
github.com/unistack-org/micro/v3 v3.3.9 h1:NFF4wymDDxgYeZU++04BxvETrrAnd0iNzVyfm/ssQI4=
|
||||
github.com/unistack-org/micro/v3 v3.3.9/go.mod h1:M3RGgKZX1OlTn0QB+3R1tlSoGwmEg6rb/Isr0lLYmjQ=
|
||||
github.com/unistack-org/micro/v3 v3.3.10 h1:yMSiyplupFQ7xy/kNXL/zqSnqGSjrlNa2GTCwea5BVg=
|
||||
github.com/unistack-org/micro/v3 v3.3.10/go.mod h1:5ragE2E8ER5d4FZQJG9pB6qdfOoLXLfKW89l77Dy3jQ=
|
||||
github.com/unistack-org/micro/v3 v3.3.11 h1:Jr0gAw5lLqgddiHKQeWUOUeP6ZqgRhz52EA9zJ5MJ3U=
|
||||
github.com/unistack-org/micro/v3 v3.3.11/go.mod h1:PPCt675o3HPcODFbJ4iRWPmQFAk1WQ+asQSOb/syq6U=
|
||||
github.com/unistack-org/micro/v3 v3.3.13 h1:y4bDDkbwnjgOckrhFkC6D/o42tr75X33UbrB+Ko0M68=
|
||||
github.com/unistack-org/micro/v3 v3.3.13/go.mod h1:98hNcMXp/WyWJwLwCuwrhN1Jm7aCWaRNsMfRjK8Fq+Y=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210323141857-08027d57d8cf h1:sewfyKLWuY3ko6EI4hbFziQ8bHkfammpzCDfLT92I1c=
|
||||
golang.org/x/net v0.0.0-20210323141857-08027d57d8cf/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826 h1:lNRDRnwZWawoPHDS50ebYHTOHjctRMLSrUSQFcAHiW4=
|
||||
golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8=
|
||||
golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210326220855-61e056675ecf h1:WUcCxqQqDT0aXO4VnQbfMvp4zh7m1Gb2clVuHUAGGRE=
|
||||
golang.org/x/net v0.0.0-20210326220855-61e056675ecf/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210329181859-df645c7b52b1 h1:GDp1VG8WvY8lq4ic4L7GzKC3fNRlOa3UqUEpib8aBQE=
|
||||
golang.org/x/net v0.0.0-20210329181859-df645c7b52b1/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210330075724-22f4162a9025 h1:WkEv4x2mvGbnkuK9cWl/Ru2pzfVxAgpSDLT/holPSug=
|
||||
golang.org/x/net v0.0.0-20210330075724-22f4162a9025/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210330142815-c8897c278d10 h1:j6SlnKgJhONcqBZAH/cLNz6hg79L7Q18RY4BiN0r0LI=
|
||||
golang.org/x/net v0.0.0-20210330142815-c8897c278d10/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210330230544-e57232859fb2 h1:nGCZOty+lVDsc4H2qPFksI5Se296+V+GhMiL/TzmYNk=
|
||||
golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5 h1:zuP3axpB9rV3xH0EA7n3/gCrNPZm2SRl0l4mVH2BRj4=
|
||||
golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs=
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
88
handler.go
88
handler.go
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/unistack-org/micro/v3/codec"
|
||||
"github.com/unistack-org/micro/v3/errors"
|
||||
@@ -40,9 +41,17 @@ type httpHandler struct {
|
||||
eps []*register.Endpoint
|
||||
hd interface{}
|
||||
handlers map[string][]patHandler
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (h *httpHandler) newCodec(ct string) (codec.Codec, error) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||
ct = ct[:idx]
|
||||
}
|
||||
|
||||
if cf, ok := h.sopts.Codecs[ct]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
@@ -66,6 +75,26 @@ func (h *httpHandler) Options() server.HandlerOptions {
|
||||
}
|
||||
|
||||
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
for exp, ph := range h.pathHandlers {
|
||||
if exp.MatchString(r.URL.String()) {
|
||||
ph(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ct := DefaultContentType
|
||||
if htype := r.Header.Get("Content-Type"); htype != "" {
|
||||
ct = htype
|
||||
}
|
||||
|
||||
if idx := strings.Index(ct, ":"); idx > 0 {
|
||||
if ph, ok := h.contentTypeHandlers[ct[:idx]]; ok {
|
||||
ph(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := metadata.NewContext(r.Context(), nil)
|
||||
|
||||
defer r.Body.Close()
|
||||
@@ -76,20 +105,7 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ct := strings.Split(DefaultContentType, ";")[0]
|
||||
if htype := r.Header.Get("Content-Type"); htype != "" {
|
||||
ct = strings.Split(htype, ";")[0]
|
||||
}
|
||||
|
||||
var cf codec.Codec
|
||||
var err error
|
||||
switch ct {
|
||||
case "application/x-www-form-urlencoded":
|
||||
cf, err = h.newCodec(strings.Split(DefaultContentType, ";")[0])
|
||||
default:
|
||||
cf, err = h.newCodec(ct)
|
||||
}
|
||||
|
||||
cf, err := h.newCodec(ct)
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, nil, w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
@@ -155,22 +171,6 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if ct == "application/x-www-form-urlencoded" {
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, nil, w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
umd, err := rflutil.URLMap(string(buf))
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for k, v := range umd {
|
||||
matches[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var argv, replyv reflect.Value
|
||||
|
||||
// Decode the argument value.
|
||||
@@ -193,11 +193,9 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
//function := hldr.rcvr
|
||||
var returnValues []reflect.Value
|
||||
|
||||
if ct != "application/x-www-form-urlencoded" {
|
||||
if err = cf.ReadBody(r.Body, argv.Interface()); err != nil && err != io.EOF {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err = cf.ReadBody(r.Body, argv.Interface()); err != nil && err != io.EOF {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
matches = rflutil.FlattenMap(matches)
|
||||
@@ -206,13 +204,10 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if ct != "application/x-www-form-urlencoded" {
|
||||
b, err = cf.Marshal(argv.Interface())
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
b, err := cf.Marshal(argv.Interface())
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hr := &rpcRequest{
|
||||
@@ -249,6 +244,15 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fn = handler.sopts.HdlrWrappers[i-1](fn)
|
||||
}
|
||||
|
||||
if ct == "application/x-www-form-urlencoded" {
|
||||
cf, err = h.newCodec(DefaultContentType)
|
||||
if err != nil {
|
||||
h.errorHandler(ctx, handler, w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ct = DefaultContentType
|
||||
}
|
||||
|
||||
if appErr := fn(ctx, hr, replyv.Interface()); appErr != nil {
|
||||
switch verr := appErr.(type) {
|
||||
case *errors.Error:
|
||||
|
40
http.go
40
http.go
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -32,12 +33,21 @@ type httpServer struct {
|
||||
// used for first registration
|
||||
registered bool
|
||||
// register service instance
|
||||
rsvc *register.Service
|
||||
init bool
|
||||
errorHandler func(context.Context, server.Handler, http.ResponseWriter, *http.Request, error, int)
|
||||
rsvc *register.Service
|
||||
init bool
|
||||
errorHandler func(context.Context, server.Handler, http.ResponseWriter, *http.Request, error, int)
|
||||
pathHandlers map[*regexp.Regexp]http.HandlerFunc
|
||||
contentTypeHandlers map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
func (h *httpServer) newCodec(ct string) (codec.Codec, error) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||
ct = ct[:idx]
|
||||
}
|
||||
|
||||
if cf, ok := h.opts.Codecs[ct]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
@@ -57,6 +67,8 @@ func (h *httpServer) Init(opts ...server.Option) error {
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
for _, o := range opts {
|
||||
o(&h.opts)
|
||||
}
|
||||
@@ -66,7 +78,26 @@ func (h *httpServer) Init(opts ...server.Option) error {
|
||||
if h.handlers == nil {
|
||||
h.handlers = make(map[string]server.Handler)
|
||||
}
|
||||
h.Unlock()
|
||||
if h.pathHandlers == nil {
|
||||
h.pathHandlers = make(map[*regexp.Regexp]http.HandlerFunc)
|
||||
}
|
||||
if h.contentTypeHandlers == nil {
|
||||
h.contentTypeHandlers = make(map[string]http.HandlerFunc)
|
||||
}
|
||||
if phs, ok := h.opts.Context.Value(pathHandlerKey{}).(*pathHandlerVal); ok && phs.h != nil {
|
||||
for pp, ph := range phs.h {
|
||||
exp, err := regexp.Compile(pp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.pathHandlers[exp] = ph
|
||||
}
|
||||
}
|
||||
if phs, ok := h.opts.Context.Value(contentTypeHandlerKey{}).(*contentTypeHandlerVal); ok && phs.h != nil {
|
||||
for pp, ph := range phs.h {
|
||||
h.contentTypeHandlers[pp] = ph
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.opts.Register.Init(); err != nil {
|
||||
return err
|
||||
@@ -554,5 +585,6 @@ func NewServer(opts ...server.Option) server.Server {
|
||||
exit: make(chan chan error),
|
||||
subscribers: make(map[*httpSubscriber][]broker.Subscriber),
|
||||
errorHandler: DefaultErrorHandler,
|
||||
pathHandlers: make(map[*regexp.Regexp]http.HandlerFunc),
|
||||
}
|
||||
}
|
||||
|
41
options.go
41
options.go
@@ -65,5 +65,42 @@ func ErrorHandler(fn func(ctx context.Context, s server.Handler, w http.Response
|
||||
return server.SetOption(errorHandlerKey{}, fn)
|
||||
}
|
||||
|
||||
// type pathHandlerKey struct{}
|
||||
// PathHandler specifies http handler for path
|
||||
type pathHandlerKey struct{}
|
||||
type pathHandlerVal struct {
|
||||
h map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
// PathHandler specifies http handler for path regexp
|
||||
func PathHandler(path string, h http.HandlerFunc) server.Option {
|
||||
return func(o *server.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
v, ok := o.Context.Value(pathHandlerKey{}).(*pathHandlerVal)
|
||||
if !ok {
|
||||
v = &pathHandlerVal{h: make(map[string]http.HandlerFunc)}
|
||||
}
|
||||
v.h[path] = h
|
||||
o.Context = context.WithValue(o.Context, pathHandlerKey{}, v)
|
||||
}
|
||||
}
|
||||
|
||||
type contentTypeHandlerKey struct{}
|
||||
type contentTypeHandlerVal struct {
|
||||
h map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
// ContentTypeHandler specifies http handler for Content-Type
|
||||
func ContentTypeHandler(ct string, h http.HandlerFunc) server.Option {
|
||||
return func(o *server.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
v, ok := o.Context.Value(contentTypeHandlerKey{}).(*contentTypeHandlerVal)
|
||||
if !ok {
|
||||
v = &contentTypeHandlerVal{h: make(map[string]http.HandlerFunc)}
|
||||
}
|
||||
v.h[ct] = h
|
||||
o.Context = context.WithValue(o.Context, contentTypeHandlerKey{}, v)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user