mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-10-31 12:39:59 +00:00 
			
		
		
		
	✨ added random value selection feature to request objects (#19)
This commit is contained in:
		| @@ -6,6 +6,7 @@ import ( | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| ) | ||||
|  | ||||
| @@ -30,11 +31,11 @@ type RequestConfig struct { | ||||
| 	Timeout      time.Duration | ||||
| 	DodosCount   uint | ||||
| 	RequestCount uint | ||||
| 	Params       map[string]string | ||||
| 	Headers      map[string]string | ||||
| 	Cookies      map[string]string | ||||
| 	Params       map[string][]string | ||||
| 	Headers      map[string][]string | ||||
| 	Cookies      map[string][]string | ||||
| 	Proxies      []Proxy | ||||
| 	Body         string | ||||
| 	Body         []string | ||||
| 	Yes          bool | ||||
| } | ||||
|  | ||||
| @@ -55,17 +56,17 @@ func (config *RequestConfig) Print() { | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Dodos", config.DodosCount}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Request Count", config.RequestCount}) | ||||
| 	t.AppendRow(table.Row{"Request", config.RequestCount}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Params Count", len(config.Params)}) | ||||
| 	t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Headers Count", len(config.Headers)}) | ||||
| 	t.AppendRow(table.Row{"Headers", utils.MarshalJSON(config.Headers, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Cookies Count", len(config.Cookies)}) | ||||
| 	t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)}) | ||||
| 	t.AppendRow(table.Row{"Proxies", utils.MarshalJSON(config.Proxies, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Body", config.Body}) | ||||
| 	t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)}) | ||||
|  | ||||
| 	t.Render() | ||||
| } | ||||
| @@ -134,11 +135,11 @@ type Proxy struct { | ||||
|  | ||||
| type JSONConfig struct { | ||||
| 	Config | ||||
| 	Params  map[string]string `json:"params"` | ||||
| 	Headers map[string]string `json:"headers"` | ||||
| 	Cookies map[string]string `json:"cookies"` | ||||
| 	Proxies []Proxy           `json:"proxies" validate:"dive"` | ||||
| 	Body    string            `json:"body"` | ||||
| 	Params  map[string][]string `json:"params"` | ||||
| 	Headers map[string][]string `json:"headers"` | ||||
| 	Cookies map[string][]string `json:"cookies"` | ||||
| 	Proxies []Proxy             `json:"proxies" validate:"dive"` | ||||
| 	Body    []string            `json:"body"` | ||||
| } | ||||
|  | ||||
| func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | ||||
| @@ -152,7 +153,7 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | ||||
| 	if len(newConfig.Cookies) != 0 { | ||||
| 		config.Cookies = newConfig.Cookies | ||||
| 	} | ||||
| 	if newConfig.Body != "" { | ||||
| 	if len(newConfig.Body) != 0 { | ||||
| 		config.Body = newConfig.Body | ||||
| 	} | ||||
| 	if len(newConfig.Proxies) != 0 { | ||||
|   | ||||
| @@ -16,6 +16,8 @@ import ( | ||||
| 	"github.com/valyala/fasthttp/fasthttpproxy" | ||||
| ) | ||||
|  | ||||
| type ClientDoFunc func(ctx context.Context, request *fasthttp.Request) (*fasthttp.Response, error) | ||||
|  | ||||
| // getClientDoFunc returns a ClientDoFunc function that can be used to make HTTP requests. | ||||
| // | ||||
| // The function first checks if there are any proxies available. If there are, it retrieves the active proxy clients | ||||
|   | ||||
| @@ -1,12 +1,83 @@ | ||||
| package requests | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"math/rand" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
|  | ||||
| // getRequests generates a list of HTTP requests based on the provided parameters. | ||||
| // | ||||
| // Parameters: | ||||
| //   - ctx: The context to control cancellation and deadlines. | ||||
| //   - URL: The base URL for the requests. | ||||
| //   - Headers: A map of headers to include in each request. | ||||
| //   - Cookies: A map of cookies to include in each request. | ||||
| //   - Params: A map of query parameters to include in each request. | ||||
| //   - Method: The HTTP method to use for the requests (e.g., GET, POST). | ||||
| //   - Bodies: A list of request bodies to cycle through for each request. | ||||
| //   - RequestCount: The number of requests to generate. | ||||
| // | ||||
| // Returns: | ||||
| //   - A list of fasthttp.Request objects based on the provided parameters. | ||||
| //   - An error if the context is canceled. | ||||
| func getRequests( | ||||
| 	ctx context.Context, | ||||
| 	URL *url.URL, | ||||
| 	Headers map[string][]string, | ||||
| 	Cookies map[string][]string, | ||||
| 	Params map[string][]string, | ||||
| 	Method string, | ||||
| 	Bodies []string, | ||||
| 	RequestCount uint, | ||||
| ) ([]*fasthttp.Request, error) { | ||||
| 	requests := make([]*fasthttp.Request, 0, RequestCount) | ||||
|  | ||||
| 	bodiesLen := len(Bodies) | ||||
| 	getBody := func() string { return "" } | ||||
| 	if bodiesLen == 1 { | ||||
| 		getBody = func() string { return Bodies[0] } | ||||
| 	} else if bodiesLen > 1 { | ||||
| 		currentIndex := 0 | ||||
| 		stopIndex := bodiesLen - 1 | ||||
|  | ||||
| 		getBody = func() string { | ||||
| 			body := Bodies[currentIndex%bodiesLen] | ||||
| 			if currentIndex == stopIndex { | ||||
| 				currentIndex = rand.Intn(bodiesLen) | ||||
| 				stopIndex = currentIndex - 1 | ||||
| 			} else { | ||||
| 				currentIndex = (currentIndex + 1) % bodiesLen | ||||
| 			} | ||||
| 			return body | ||||
| 		} | ||||
| 	} | ||||
| 	getHeaders := getKeyValueSetFunc(Headers) | ||||
| 	getCookies := getKeyValueSetFunc(Cookies) | ||||
| 	getParams := getKeyValueSetFunc(Params) | ||||
|  | ||||
| 	for range RequestCount { | ||||
| 		if ctx.Err() != nil { | ||||
| 			return nil, customerrors.ErrInterrupt | ||||
| 		} | ||||
| 		request := newRequest( | ||||
| 			URL, | ||||
| 			getHeaders(), | ||||
| 			getCookies(), | ||||
| 			getParams(), | ||||
| 			Method, | ||||
| 			getBody(), | ||||
| 		) | ||||
| 		requests = append(requests, request) | ||||
| 	} | ||||
|  | ||||
| 	return requests, nil | ||||
| } | ||||
|  | ||||
| // newRequest creates a new fasthttp.Request object with the provided parameters. | ||||
| // It sets the request URI, host header, headers, cookies, params, method, and body. | ||||
| func newRequest( | ||||
| @@ -70,3 +141,73 @@ func setRequestMethod(req *fasthttp.Request, method string) { | ||||
| func setRequestBody(req *fasthttp.Request, body string) { | ||||
| 	req.SetBody([]byte(body)) | ||||
| } | ||||
|  | ||||
| // getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set. | ||||
| // The generated function will either return fixed values or random values depending on the input. | ||||
| // | ||||
| // Returns: | ||||
| //   - A function that returns a map of key-value pairs. If the input map contains multiple values for a key, | ||||
| //     the returned function will generate random values for that key. If the input map contains a single value | ||||
| //     for a key, the returned function will always return that value. If the input map is empty for a key, | ||||
| //     the returned function will generate an empty string for that key. | ||||
| func getKeyValueSetFunc[ | ||||
| 	KeyValueSet map[string][]string, | ||||
| 	KeyValue map[string]string, | ||||
| ](keyValueSet KeyValueSet) func() KeyValue { | ||||
| 	getKeyValueSlice := []map[string]func() string{} | ||||
| 	isRandom := false | ||||
| 	for key, values := range keyValueSet { | ||||
| 		valuesLen := len(values) | ||||
|  | ||||
| 		// if values is empty, return a function that generates empty string | ||||
| 		// if values has only one element, return a function that generates that element | ||||
| 		// if values has more than one element, return a function that generates a random element | ||||
| 		getKeyValue := func() string { return "" } | ||||
| 		if valuesLen == 1 { | ||||
| 			getKeyValue = func() string { return values[0] } | ||||
| 		} else if valuesLen > 1 { | ||||
| 			currentIndex := 0 | ||||
| 			stopIndex := valuesLen - 1 | ||||
|  | ||||
| 			getKeyValue = func() string { | ||||
| 				value := values[currentIndex%valuesLen] | ||||
| 				if currentIndex == stopIndex { | ||||
| 					currentIndex = rand.Intn(valuesLen) | ||||
| 					stopIndex = currentIndex - 1 | ||||
| 				} else { | ||||
| 					currentIndex = (currentIndex + 1) % valuesLen | ||||
| 				} | ||||
| 				return value | ||||
| 			} | ||||
|  | ||||
| 			isRandom = true | ||||
| 		} | ||||
|  | ||||
| 		getKeyValueSlice = append( | ||||
| 			getKeyValueSlice, | ||||
| 			map[string]func() string{key: getKeyValue}, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	// if isRandom is true, return a function that generates random values, | ||||
| 	// otherwise return a function that generates fixed values to avoid unnecessary random number generation | ||||
| 	if isRandom { | ||||
| 		return func() KeyValue { | ||||
| 			keyValues := make(KeyValue, len(getKeyValueSlice)) | ||||
| 			for _, keyValue := range getKeyValueSlice { | ||||
| 				for key, value := range keyValue { | ||||
| 					keyValues[key] = value() | ||||
| 				} | ||||
| 			} | ||||
| 			return keyValues | ||||
| 		} | ||||
| 	} else { | ||||
| 		keyValues := make(KeyValue, len(getKeyValueSlice)) | ||||
| 		for _, keyValue := range getKeyValueSlice { | ||||
| 			for key, value := range keyValue { | ||||
| 				keyValues[key] = value() | ||||
| 			} | ||||
| 		} | ||||
| 		return func() KeyValue { return keyValues } | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| package requests | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
|  | ||||
| type Response struct { | ||||
| @@ -17,9 +15,7 @@ type Response struct { | ||||
| 	Time       time.Duration | ||||
| } | ||||
|  | ||||
| type Responses []Response | ||||
|  | ||||
| type ClientDoFunc func(ctx context.Context, request *fasthttp.Request) (*fasthttp.Response, error) | ||||
| type Responses []*Response | ||||
|  | ||||
| // Print prints the responses in a tabular format, including information such as | ||||
| // response count, minimum time, maximum time, and average time. | ||||
|   | ||||
| @@ -36,21 +36,25 @@ func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, e | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 	} | ||||
|  | ||||
| 	request := newRequest( | ||||
| 	requests, err := getRequests( | ||||
| 		ctx, | ||||
| 		requestConfig.URL, | ||||
| 		requestConfig.Headers, | ||||
| 		requestConfig.Cookies, | ||||
| 		requestConfig.Params, | ||||
| 		requestConfig.Method, | ||||
| 		requestConfig.Body, | ||||
| 		requestConfig.RequestCount, | ||||
| 	) | ||||
| 	defer fasthttp.ReleaseRequest(request) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	responses := releaseDodos( | ||||
| 		ctx, | ||||
| 		request, | ||||
| 		requests, | ||||
| 		clientDoFunc, | ||||
| 		requestConfig.GetValidDodosCountForRequests(), | ||||
| 		requestConfig.RequestCount, | ||||
| 	) | ||||
| 	if ctx.Err() != nil && len(responses) == 0 { | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| @@ -59,51 +63,59 @@ func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, e | ||||
| 	return responses, nil | ||||
| } | ||||
|  | ||||
| // releaseDodos sends multiple HTTP requests concurrently using multiple "dodos" (goroutines). | ||||
| // It takes a mainRequest as the base request, timeout duration for each request, clientDoFunc for customizing the client behavior, | ||||
| // dodosCount as the number of goroutines to be used, and requestCount as the total number of requests to be sent. | ||||
| // It returns the responses received from all the requests. | ||||
| // releaseDodos sends HTTP requests concurrently using multiple "dodos" (goroutines). | ||||
| // | ||||
| // Parameters: | ||||
| //   - ctx: The context to control the lifecycle of the requests. | ||||
| //   - requests: A slice of HTTP requests to be sent. | ||||
| //   - clientDoFunc: A function to execute the HTTP requests. | ||||
| //   - dodosCount: The number of dodos (goroutines) to use for sending the requests. | ||||
| // | ||||
| // Returns: | ||||
| //   - A slice of Response objects containing the results of the requests. | ||||
| // | ||||
| // The function divides the requests into equal parts based on the number of dodos. | ||||
| // It then sends each part concurrently using a separate goroutine. | ||||
| func releaseDodos( | ||||
| 	ctx context.Context, | ||||
| 	mainRequest *fasthttp.Request, | ||||
| 	requests []*fasthttp.Request, | ||||
| 	clientDoFunc ClientDoFunc, | ||||
| 	dodosCount uint, | ||||
| 	requestCount uint, | ||||
| ) Responses { | ||||
| 	var ( | ||||
| 		wg                  sync.WaitGroup | ||||
| 		streamWG            sync.WaitGroup | ||||
| 		requestCountPerDodo uint | ||||
| 		dodosCountInt       = int(dodosCount) | ||||
| 		dodosCountInt       int  = int(dodosCount) | ||||
| 		totalRequestCount   uint = uint(len(requests)) | ||||
| 		requestCount        uint = 0 | ||||
| 		responses                = make([][]*Response, dodosCount) | ||||
| 		increase                 = make(chan int64, totalRequestCount) | ||||
| 	) | ||||
|  | ||||
| 	wg.Add(dodosCountInt) | ||||
| 	streamWG.Add(1) | ||||
| 	responses := make([][]Response, dodosCount) | ||||
| 	increase := make(chan int64, requestCount) | ||||
|  | ||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | ||||
| 	go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase) | ||||
|  | ||||
| 	go streamProgress(streamCtx, &streamWG, int64(totalRequestCount), "Dodos Working🔥", increase) | ||||
|  | ||||
| 	for i := range dodosCount { | ||||
| 		if i+1 == dodosCount { | ||||
| 			requestCountPerDodo = requestCount - (i * requestCount / dodosCount) | ||||
| 			requestCountPerDodo = totalRequestCount - (i * totalRequestCount / dodosCount) | ||||
| 		} else { | ||||
| 			requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - | ||||
| 				(i * requestCount / dodosCount) | ||||
| 			requestCountPerDodo = ((i + 1) * totalRequestCount / dodosCount) - | ||||
| 				(i * totalRequestCount / dodosCount) | ||||
| 		} | ||||
| 		dodoSpecificRequest := &fasthttp.Request{} | ||||
| 		mainRequest.CopyTo(dodoSpecificRequest) | ||||
|  | ||||
| 		go sendRequest( | ||||
| 			ctx, | ||||
| 			dodoSpecificRequest, | ||||
| 			requests[requestCount:requestCount+requestCountPerDodo], | ||||
| 			&responses[i], | ||||
| 			increase, | ||||
| 			requestCountPerDodo, | ||||
| 			clientDoFunc, | ||||
| 			&wg, | ||||
| 		) | ||||
| 		requestCount += requestCountPerDodo | ||||
| 	} | ||||
| 	wg.Wait() | ||||
| 	streamCtxCancel() | ||||
| @@ -111,37 +123,37 @@ func releaseDodos( | ||||
| 	return utils.Flatten(responses) | ||||
| } | ||||
|  | ||||
| // sendRequest sends an HTTP request using the provided clientDo function and handles the response. | ||||
| // sendRequest sends multiple HTTP requests concurrently and collects their responses. | ||||
| // | ||||
| // Parameters: | ||||
| //   - ctx: The context to control cancellation and timeout. | ||||
| //   - request: The HTTP request to be sent. | ||||
| //   - responseData: A slice to store the response data. | ||||
| //   - increase: A channel to signal the completion of a request. | ||||
| //   - requestCount: The number of requests to be sent. | ||||
| //   - requests: A slice of pointers to fasthttp.Request objects to be sent. | ||||
| //   - responseData: A pointer to a slice of *Response objects to store the results. | ||||
| //   - increase: A channel to signal the completion of each request. | ||||
| //   - clientDo: A function to execute the HTTP request. | ||||
| //   - wg: A wait group to signal the completion of the function. | ||||
| //   - wg: A wait group to synchronize the completion of the requests. | ||||
| // | ||||
| // The function sends the specified number of requests, handles errors, and appends the response data | ||||
| // to the responseData slice. | ||||
| // The function iterates over the provided requests, sending each one using the clientDo function. | ||||
| // It measures the time taken for each request and appends the response data to responseData. | ||||
| // If an error occurs, it appends an error response. The function signals completion through the increase channel | ||||
| // and ensures proper resource cleanup by releasing requests and responses. | ||||
| func sendRequest( | ||||
| 	ctx context.Context, | ||||
| 	request *fasthttp.Request, | ||||
| 	responseData *[]Response, | ||||
| 	requests []*fasthttp.Request, | ||||
| 	responseData *[]*Response, | ||||
| 	increase chan<- int64, | ||||
| 	requestCount uint, | ||||
| 	clientDo ClientDoFunc, | ||||
| 	wg *sync.WaitGroup, | ||||
| ) { | ||||
| 	defer fasthttp.ReleaseRequest(request) | ||||
| 	defer wg.Done() | ||||
|  | ||||
| 	for range requestCount { | ||||
| 	for _, request := range requests { | ||||
| 		if ctx.Err() != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		func() { | ||||
| 			defer fasthttp.ReleaseRequest(request) | ||||
| 			startTime := time.Now() | ||||
| 			response, err := clientDo(ctx, request) | ||||
| 			completedTime := time.Since(startTime) | ||||
| @@ -150,7 +162,7 @@ func sendRequest( | ||||
| 				if err == customerrors.ErrInterrupt { | ||||
| 					return | ||||
| 				} | ||||
| 				*responseData = append(*responseData, Response{ | ||||
| 				*responseData = append(*responseData, &Response{ | ||||
| 					StatusCode: 0, | ||||
| 					Error:      err, | ||||
| 					Time:       completedTime, | ||||
| @@ -160,7 +172,7 @@ func sendRequest( | ||||
| 			} | ||||
| 			defer fasthttp.ReleaseResponse(response) | ||||
|  | ||||
| 			*responseData = append(*responseData, Response{ | ||||
| 			*responseData = append(*responseData, &Response{ | ||||
| 				StatusCode: response.StatusCode(), | ||||
| 				Error:      nil, | ||||
| 				Time:       completedTime, | ||||
|   | ||||
							
								
								
									
										54
									
								
								utils/convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								utils/convert.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func MarshalJSON(v any, maxSliceSize uint) string { | ||||
| 	rv := reflect.ValueOf(v) | ||||
| 	if rv.Kind() == reflect.Slice && rv.Len() == 0 { | ||||
| 		return "[]" | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.MarshalIndent(truncateLists(v, int(maxSliceSize)), "", "  ") | ||||
| 	if err != nil { | ||||
| 		return "{}" | ||||
| 	} | ||||
|  | ||||
| 	return strings.Replace(string(data), `"..."`, "...", -1) | ||||
| } | ||||
|  | ||||
| func truncateLists(v interface{}, maxItems int) interface{} { | ||||
| 	rv := reflect.ValueOf(v) | ||||
|  | ||||
| 	switch rv.Kind() { | ||||
| 	case reflect.Slice, reflect.Array: | ||||
| 		if rv.Len() > maxItems { | ||||
| 			newSlice := reflect.MakeSlice(rv.Type(), maxItems, maxItems) | ||||
| 			reflect.Copy(newSlice, rv.Slice(0, maxItems)) | ||||
| 			newSlice = reflect.Append(newSlice, reflect.ValueOf("...")) | ||||
| 			return newSlice.Interface() | ||||
| 		} | ||||
| 	case reflect.Map: | ||||
| 		newMap := reflect.MakeMap(rv.Type()) | ||||
| 		for _, key := range rv.MapKeys() { | ||||
| 			newMap.SetMapIndex(key, reflect.ValueOf(truncateLists(rv.MapIndex(key).Interface(), maxItems))) | ||||
| 		} | ||||
| 		return newMap.Interface() | ||||
| 	case reflect.Struct: | ||||
| 		newStruct := reflect.New(rv.Type()).Elem() | ||||
| 		for i := 0; i < rv.NumField(); i++ { | ||||
| 			newStruct.Field(i).Set(reflect.ValueOf(truncateLists(rv.Field(i).Interface(), maxItems))) | ||||
| 		} | ||||
| 		return newStruct.Interface() | ||||
| 	case reflect.Ptr: | ||||
| 		if rv.IsNil() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return truncateLists(rv.Elem().Interface(), maxItems) | ||||
| 	} | ||||
|  | ||||
| 	return v | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| package utils | ||||
|  | ||||
| func Flatten[T any](nested [][]T) []T { | ||||
| 	flattened := make([]T, 0) | ||||
| func Flatten[T any](nested [][]*T) []*T { | ||||
| 	flattened := make([]*T, 0) | ||||
| 	for _, n := range nested { | ||||
| 		flattened = append(flattened, n...) | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user