add bitbucket, update build tags
This commit is contained in:
		
							parent
							
								
									cad179977c
								
							
						
					
					
						commit
						c07056d9fd
					
				
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @ -69,6 +69,17 @@ Just the `push` event. | |||||||
| Active: ✅ | Active: ✅ | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### Bitbucket | ||||||
|  | 
 | ||||||
|  | Sometimes Bitbucket does not give you the option to specify the (`X-Hub-Signature`) `secret`, | ||||||
|  | so you'll have to append an `access_token` instead. Example: | ||||||
|  | 
 | ||||||
|  | ```txt | ||||||
|  | Title: git-deploy | ||||||
|  | URL: https://YOUR_DOMAIN/api/webhooks/bitbucket?access_token=YOUR_SECRET | ||||||
|  | Triggers: Repository push | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ## TODO | ## TODO | ||||||
| 
 | 
 | ||||||
| **git-deploy** is intended for use with static websites that are generated after | **git-deploy** is intended for use with static websites that are generated after | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								bitbucket.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								bitbucket.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | // +build !nobitbucket | ||||||
|  | 
 | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	_ "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks/bitbucket" | ||||||
|  | ) | ||||||
| @ -1,5 +1,4 @@ | |||||||
| // // +build github | // +build !nogithub | ||||||
| // TODO omit github unless specified by build tag |  | ||||||
| 
 | 
 | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										144
									
								
								internal/webhooks/bitbucket/bitbucket.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								internal/webhooks/bitbucket/bitbucket.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | package bitbucket | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/subtle" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"git.ryanburnette.com/ryanburnette/git-deploy/internal/options" | ||||||
|  | 	"git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-chi/chi" | ||||||
|  | 	"github.com/google/go-github/v32/github" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	var secret string | ||||||
|  | 	name := "bitbucket" | ||||||
|  | 	options.ServerFlags.StringVar( | ||||||
|  | 		&secret, fmt.Sprintf("%s-secret", name), "", | ||||||
|  | 		fmt.Sprintf( | ||||||
|  | 			"secret for %s webhooks (same as %s_SECRET=)", | ||||||
|  | 			name, strings.ToUpper(name)), | ||||||
|  | 	) | ||||||
|  | 	webhooks.AddProvider("bitbucket", InitWebhook("bitbucket", &secret, "BITBUCKET_SECRET")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // InitWebhook prepares the webhook router. | ||||||
|  | // It should be called after arguments are parsed and ENVs are set.InitWebhook | ||||||
|  | func InitWebhook(providername string, secret *string, envname string) func() { | ||||||
|  | 	return func() { | ||||||
|  | 		if "" == *secret { | ||||||
|  | 			*secret = os.Getenv(envname) | ||||||
|  | 		} | ||||||
|  | 		if "" == *secret { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "skipped route for missing %s\n", envname) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		secretB := []byte(*secret) | ||||||
|  | 		webhooks.AddRouteHandler(providername, func(router chi.Router) { | ||||||
|  | 			router.Post("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 				body := http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) | ||||||
|  | 				defer func() { | ||||||
|  | 					_ = body.Close() | ||||||
|  | 				}() | ||||||
|  | 
 | ||||||
|  | 				accessToken := r.URL.Query().Get("access_token") | ||||||
|  | 				if "" != accessToken { | ||||||
|  | 					if 0 == subtle.ConstantTimeCompare( | ||||||
|  | 						[]byte(r.URL.Query().Get("access_token")), | ||||||
|  | 						secretB, | ||||||
|  | 					) { | ||||||
|  | 						log.Printf("invalid bitbucket access_token\n") | ||||||
|  | 						http.Error(w, "invalid access_token", http.StatusBadRequest) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				payload, err := ioutil.ReadAll(r.Body) | ||||||
|  | 				if err != nil { | ||||||
|  | 					// if there's a read error, it should have been handled | ||||||
|  | 					// already by the MaxBytesReader | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if "" == accessToken { | ||||||
|  | 					sig := r.Header.Get("X-Hub-Signature") | ||||||
|  | 					// TODO replace with generic X-Hub-Signature validation | ||||||
|  | 					if err := github.ValidateSignature(sig, payload, secretB); nil != err { | ||||||
|  | 						log.Printf("invalid bitbucket signature: error: %s\n", err) | ||||||
|  | 						http.Error(w, "invalid bitbucket signature", http.StatusBadRequest) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				info := Webhook{} | ||||||
|  | 				if err := json.Unmarshal(payload, &info); nil != err { | ||||||
|  | 					log.Printf("invalid bitbucket payload: error: %s\n%s\n", err, string(payload)) | ||||||
|  | 					http.Error(w, "invalid bitbucket payload", http.StatusBadRequest) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				var branch string | ||||||
|  | 				var tag string | ||||||
|  | 				var ref string | ||||||
|  | 
 | ||||||
|  | 				n := len(info.Push.Changes) | ||||||
|  | 				if n < 1 { | ||||||
|  | 					log.Printf("invalid bitbucket changeset (n): %d\n%s\n", n, string(payload)) | ||||||
|  | 					http.Error(w, "invalid bitbucket payload", http.StatusBadRequest) | ||||||
|  | 					return | ||||||
|  | 				} else if n > 1 { | ||||||
|  | 					log.Printf("more than one bitbucket changeset (n): %d\n%s\n", n, string(payload)) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				refName := info.Push.Changes[0].New.Name | ||||||
|  | 				refType := info.Push.Changes[0].New.Type | ||||||
|  | 				switch refType { | ||||||
|  | 				case "tag": | ||||||
|  | 					tag = refName | ||||||
|  | 					ref = fmt.Sprintf("refs/tags/%s", refName) | ||||||
|  | 				case "branch": | ||||||
|  | 					branch = refName | ||||||
|  | 					ref = fmt.Sprintf("refs/heads/%s", refName) | ||||||
|  | 				default: | ||||||
|  | 					log.Println("unexpected bitbucket RefType", refType) | ||||||
|  | 					ref = fmt.Sprintf("refs/UNKNOWN/%s", refName) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				switch refType { | ||||||
|  | 				case "tags": | ||||||
|  | 					refType = "tag" | ||||||
|  | 					tag = refName | ||||||
|  | 				case "heads": | ||||||
|  | 					refType = "branch" | ||||||
|  | 					branch = refName | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				var rev string | ||||||
|  | 				if len(info.Push.Changes[0].Commits) > 0 { | ||||||
|  | 					// TODO first or last? | ||||||
|  | 					// TODO shouldn't tags have a Commit as well? | ||||||
|  | 					rev = info.Push.Changes[0].Commits[0].Hash | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				webhooks.Hook(webhooks.Ref{ | ||||||
|  | 					HTTPSURL: info.Repository.Links.HTML.Href, | ||||||
|  | 					Rev:      rev, | ||||||
|  | 					Ref:      ref, | ||||||
|  | 					RefType:  refType, | ||||||
|  | 					RefName:  refName, | ||||||
|  | 					Branch:   branch, | ||||||
|  | 					Tag:      tag, | ||||||
|  | 					Repo:     info.Repository.Name, | ||||||
|  | 					Owner:    info.Repository.Workspace.Slug, | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										245
									
								
								internal/webhooks/bitbucket/payload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								internal/webhooks/bitbucket/payload.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,245 @@ | |||||||
|  | package bitbucket | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | // Thank you Matt! | ||||||
|  | // See https://mholt.github.io/json-to-go/ | ||||||
|  | 
 | ||||||
|  | type Webhook struct { | ||||||
|  | 	Push       Push       `json:"push"` | ||||||
|  | 	Actor      Actor      `json:"actor"` | ||||||
|  | 	Repository Repository `json:"repository"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Push struct { | ||||||
|  | 	Changes []struct { | ||||||
|  | 		Forced bool `json:"forced"` | ||||||
|  | 		Old    struct { | ||||||
|  | 			Name   string `json:"name"` | ||||||
|  | 			Type   string `json:"type"` | ||||||
|  | 			Target struct { | ||||||
|  | 				Hash   string `json:"hash"` | ||||||
|  | 				Author struct { | ||||||
|  | 					User struct { | ||||||
|  | 						DisplayName string `json:"display_name"` | ||||||
|  | 						UUID        string `json:"uuid"` | ||||||
|  | 						Nickname    string `json:"nickname"` | ||||||
|  | 						AccountID   string `json:"account_id"` | ||||||
|  | 					} `json:"user"` | ||||||
|  | 				} `json:"author"` | ||||||
|  | 				Date    time.Time `json:"date"` | ||||||
|  | 				Message string    `json:"message"` | ||||||
|  | 				Type    string    `json:"type"` | ||||||
|  | 			} `json:"target"` | ||||||
|  | 		} `json:"old"` | ||||||
|  | 		Links struct { | ||||||
|  | 			HTML struct { | ||||||
|  | 				Href string `json:"href"` | ||||||
|  | 			} `json:"html"` | ||||||
|  | 		} `json:"links"` | ||||||
|  | 		Created bool `json:"created"` | ||||||
|  | 		Commits []struct { | ||||||
|  | 			Rendered struct { | ||||||
|  | 			} `json:"rendered"` | ||||||
|  | 			Hash  string `json:"hash"` | ||||||
|  | 			Links struct { | ||||||
|  | 				Self struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"self"` | ||||||
|  | 				Comments struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"comments"` | ||||||
|  | 				Patch struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"patch"` | ||||||
|  | 				HTML struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"html"` | ||||||
|  | 				Diff struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"diff"` | ||||||
|  | 				Approve struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"approve"` | ||||||
|  | 				Statuses struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"statuses"` | ||||||
|  | 			} `json:"links"` | ||||||
|  | 			Author struct { | ||||||
|  | 				Raw  string `json:"raw"` | ||||||
|  | 				Type string `json:"type"` | ||||||
|  | 				User struct { | ||||||
|  | 					DisplayName string `json:"display_name"` | ||||||
|  | 					UUID        string `json:"uuid"` | ||||||
|  | 					Links       struct { | ||||||
|  | 						Self struct { | ||||||
|  | 							Href string `json:"href"` | ||||||
|  | 						} `json:"self"` | ||||||
|  | 						HTML struct { | ||||||
|  | 							Href string `json:"href"` | ||||||
|  | 						} `json:"html"` | ||||||
|  | 						Avatar struct { | ||||||
|  | 							Href string `json:"href"` | ||||||
|  | 						} `json:"avatar"` | ||||||
|  | 					} `json:"links"` | ||||||
|  | 					Nickname  string `json:"nickname"` | ||||||
|  | 					Type      string `json:"type"` | ||||||
|  | 					AccountID string `json:"account_id"` | ||||||
|  | 				} `json:"user"` | ||||||
|  | 			} `json:"author"` | ||||||
|  | 			Summary struct { | ||||||
|  | 				Raw    string `json:"raw"` | ||||||
|  | 				Markup string `json:"markup"` | ||||||
|  | 				HTML   string `json:"html"` | ||||||
|  | 				Type   string `json:"type"` | ||||||
|  | 			} `json:"summary"` | ||||||
|  | 			Parents []struct { | ||||||
|  | 				Hash  string `json:"hash"` | ||||||
|  | 				Type  string `json:"type"` | ||||||
|  | 				Links struct { | ||||||
|  | 					Self struct { | ||||||
|  | 						Href string `json:"href"` | ||||||
|  | 					} `json:"self"` | ||||||
|  | 					HTML struct { | ||||||
|  | 						Href string `json:"href"` | ||||||
|  | 					} `json:"html"` | ||||||
|  | 				} `json:"links"` | ||||||
|  | 			} `json:"parents"` | ||||||
|  | 			Date       time.Time `json:"date"` | ||||||
|  | 			Message    string    `json:"message"` | ||||||
|  | 			Type       string    `json:"type"` | ||||||
|  | 			Properties struct { | ||||||
|  | 			} `json:"properties"` | ||||||
|  | 		} `json:"commits"` | ||||||
|  | 		Truncated bool `json:"truncated"` | ||||||
|  | 		Closed    bool `json:"closed"` | ||||||
|  | 		New       struct { | ||||||
|  | 			Name  string `json:"name"` | ||||||
|  | 			Links struct { | ||||||
|  | 				Commits struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"commits"` | ||||||
|  | 				Self struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"self"` | ||||||
|  | 				HTML struct { | ||||||
|  | 					Href string `json:"href"` | ||||||
|  | 				} `json:"html"` | ||||||
|  | 			} `json:"links"` | ||||||
|  | 			DefaultMergeStrategy string   `json:"default_merge_strategy"` | ||||||
|  | 			MergeStrategies      []string `json:"merge_strategies"` | ||||||
|  | 			Type                 string   `json:"type"` | ||||||
|  | 			Target               struct { | ||||||
|  | 				Rendered struct { | ||||||
|  | 				} `json:"rendered"` | ||||||
|  | 				Hash  string `json:"hash"` | ||||||
|  | 				Links struct { | ||||||
|  | 					Self struct { | ||||||
|  | 						Href string `json:"href"` | ||||||
|  | 					} `json:"self"` | ||||||
|  | 					HTML struct { | ||||||
|  | 						Href string `json:"href"` | ||||||
|  | 					} `json:"html"` | ||||||
|  | 				} `json:"links"` | ||||||
|  | 				Author struct { | ||||||
|  | 					Raw  string `json:"raw"` | ||||||
|  | 					Type string `json:"type"` | ||||||
|  | 					User struct { | ||||||
|  | 						DisplayName string `json:"display_name"` | ||||||
|  | 						UUID        string `json:"uuid"` | ||||||
|  | 						Links       struct { | ||||||
|  | 							Self struct { | ||||||
|  | 								Href string `json:"href"` | ||||||
|  | 							} `json:"self"` | ||||||
|  | 							HTML struct { | ||||||
|  | 								Href string `json:"href"` | ||||||
|  | 							} `json:"html"` | ||||||
|  | 							Avatar struct { | ||||||
|  | 								Href string `json:"href"` | ||||||
|  | 							} `json:"avatar"` | ||||||
|  | 						} `json:"links"` | ||||||
|  | 						Nickname  string `json:"nickname"` | ||||||
|  | 						Type      string `json:"type"` | ||||||
|  | 						AccountID string `json:"account_id"` | ||||||
|  | 					} `json:"user"` | ||||||
|  | 				} `json:"author"` | ||||||
|  | 				Summary struct { | ||||||
|  | 					Raw    string `json:"raw"` | ||||||
|  | 					Markup string `json:"markup"` | ||||||
|  | 					HTML   string `json:"html"` | ||||||
|  | 					Type   string `json:"type"` | ||||||
|  | 				} `json:"summary"` | ||||||
|  | 				Parents []struct { | ||||||
|  | 					Hash  string `json:"hash"` | ||||||
|  | 					Type  string `json:"type"` | ||||||
|  | 					Links struct { | ||||||
|  | 						Self struct { | ||||||
|  | 							Href string `json:"href"` | ||||||
|  | 						} `json:"self"` | ||||||
|  | 						HTML struct { | ||||||
|  | 							Href string `json:"href"` | ||||||
|  | 						} `json:"html"` | ||||||
|  | 					} `json:"links"` | ||||||
|  | 				} `json:"parents"` | ||||||
|  | 				Date       time.Time `json:"date"` | ||||||
|  | 				Message    string    `json:"message"` | ||||||
|  | 				Type       string    `json:"type"` | ||||||
|  | 				Properties struct { | ||||||
|  | 				} `json:"properties"` | ||||||
|  | 			} `json:"target"` | ||||||
|  | 		} `json:"new"` | ||||||
|  | 	} `json:"changes"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Actor struct { | ||||||
|  | 	DisplayName string `json:"display_name"` | ||||||
|  | 	UUID        string `json:"uuid"` | ||||||
|  | 	Nickname    string `json:"nickname"` | ||||||
|  | 	Type        string `json:"type"` | ||||||
|  | 	AccountID   string `json:"account_id"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Repository struct { | ||||||
|  | 	Name    string      `json:"name"` | ||||||
|  | 	Scm     string      `json:"scm"` | ||||||
|  | 	Website interface{} `json:"website"` | ||||||
|  | 	UUID    string      `json:"uuid"` | ||||||
|  | 	Links   struct { | ||||||
|  | 		Self struct { | ||||||
|  | 			Href string `json:"href"` | ||||||
|  | 		} `json:"self"` | ||||||
|  | 		HTML struct { | ||||||
|  | 			Href string `json:"href"` | ||||||
|  | 		} `json:"html"` | ||||||
|  | 		Avatar struct { | ||||||
|  | 			Href string `json:"href"` | ||||||
|  | 		} `json:"avatar"` | ||||||
|  | 	} `json:"links"` | ||||||
|  | 	FullName string `json:"full_name"` | ||||||
|  | 	Owner    struct { | ||||||
|  | 		DisplayName string `json:"display_name"` | ||||||
|  | 		UUID        string `json:"uuid"` | ||||||
|  | 		Links       struct { | ||||||
|  | 			Self struct { | ||||||
|  | 				Href string `json:"href"` | ||||||
|  | 			} `json:"self"` | ||||||
|  | 			HTML struct { | ||||||
|  | 				Href string `json:"href"` | ||||||
|  | 			} `json:"html"` | ||||||
|  | 			Avatar struct { | ||||||
|  | 				Href string `json:"href"` | ||||||
|  | 			} `json:"avatar"` | ||||||
|  | 		} `json:"links"` | ||||||
|  | 		Nickname  string `json:"nickname"` | ||||||
|  | 		Type      string `json:"type"` | ||||||
|  | 		AccountID string `json:"account_id"` | ||||||
|  | 	} `json:"owner"` | ||||||
|  | 	Workspace struct { | ||||||
|  | 		Slug string `json:"slug"` | ||||||
|  | 		Type string `json:"type"` | ||||||
|  | 		Name string `json:"name"` | ||||||
|  | 		UUID string `json:"uuid"` | ||||||
|  | 	} `json:"workspace"` | ||||||
|  | 	Type      string `json:"type"` | ||||||
|  | 	IsPrivate bool   `json:"is_private"` | ||||||
|  | } | ||||||
| @ -12,6 +12,8 @@ import ( | |||||||
| 	"git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" | 	"git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-chi/chi" | 	"github.com/go-chi/chi" | ||||||
|  | 	// TODO nix this dependency in favor of a lightweight X-Hub-Signature | ||||||
|  | 	// and JSON-to-Go-struct approach | ||||||
| 	"github.com/google/go-github/v32/github" | 	"github.com/google/go-github/v32/github" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								main.go
									
									
									
									
									
								
							| @ -93,9 +93,9 @@ func main() { | |||||||
| 		os.Exit(0) | 		os.Exit(0) | ||||||
| 		return | 		return | ||||||
| 	case "init": | 	case "init": | ||||||
| 		initFlags.Parse(args[2:]) | 		_ = initFlags.Parse(args[2:]) | ||||||
| 	case "run": | 	case "run": | ||||||
| 		runFlags.Parse(args[2:]) | 		_ = runFlags.Parse(args[2:]) | ||||||
| 		if "" == runOpts.Exec { | 		if "" == runOpts.Exec { | ||||||
| 			fmt.Printf("--exec <path/to/script.sh> is a required flag") | 			fmt.Printf("--exec <path/to/script.sh> is a required flag") | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| @ -210,7 +210,7 @@ func serve() { | |||||||
| 
 | 
 | ||||||
| 			go func() { | 			go func() { | ||||||
| 				log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName) | 				log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName) | ||||||
| 				cmd.Wait() | 				_ = cmd.Wait() | ||||||
| 				delete(jobs, jobID) | 				delete(jobs, jobID) | ||||||
| 				log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) | 				log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) | ||||||
| 			}() | 			}() | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user