{"id":318983,"date":"2021-03-03T21:00:13","date_gmt":"2021-03-03T21:00:13","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=318983"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=318983","title":{"rendered":"\u041f\u0438\u0448\u0435\u043c Slack \u0431\u043e\u0442\u0430 \u0434\u043b\u044f Scrum \u043f\u043e\u043a\u0435\u0440\u0430 \u043d\u0430 Go. \u0427\u0430\u0441\u0442\u044c 1"},"content":{"rendered":"\n<div class=\"post__text post__text_v2\" id=\"post-content-body\">\n<p>\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u043c\u044b \u043d\u0430\u043f\u0438\u0448\u0435\u043c Slack \u0431\u043e\u0442\u0430 \u0434\u043b\u044f <a href=\"https:\/\/en.wikipedia.org\/wiki\/Planning_poker\" rel=\"noopener noreferrer nofollow\">Scrum \u043f\u043e\u043a\u0435\u0440\u0430<\/a> \u043d\u0430 \u044f\u0437\u044b\u043a\u0435 Go. \u041f\u0438\u0441\u0430\u0442\u044c \u0431\u0443\u0434\u0435\u043c \u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0431\u0435\u0437 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u0432 \u0438 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0430\u0448\u0430 \u0446\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c\u0441\u044f \u0441 \u044f\u0437\u044b\u043a\u043e\u043c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Go \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u043d\u0430\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e\u0442 \u044f\u0437\u044b\u043a \u0443\u0434\u043e\u0431\u0435\u043d \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432. <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/5e7\/b94\/412\/5e7b944122245a121285f82f8a0a6e24.png\" width=\"900\" height=\"728\"><figcaption><\/figcaption><\/figure>\n<h2>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440<\/h2>\n<p><em>\u042f \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0437\u043d\u0430\u044e Go \u0438 \u043c\u043d\u043e\u0433\u0438\u0445 \u0432\u0435\u0449\u0435\u0439 \u0435\u0449\u0435 \u043d\u0435 \u0437\u043d\u0430\u044e. \u041c\u043e\u0439 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u044f\u0437\u044b\u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 Python. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0447\u0430\u0441\u0442\u043e \u0431\u0443\u0434\u0443 \u043e\u0442\u0441\u044b\u043b\u0430\u0442\u044c \u043a \u043d\u0435\u043c\u0443 \u0432 \u0442\u0435\u0445 \u043c\u0435\u0441\u0442\u0430\u0445, \u0433\u0434\u0435 \u043f\u043e \u043c\u043e\u0435\u043c\u0443 \u043c\u043d\u0435\u043d\u0438\u044e \u0432 Python \u0447\u0442\u043e-\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u043d\u043e \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0438\u043b\u0438 \u043f\u0440\u043e\u0449\u0435. \u0426\u0435\u043b\u044c \u044d\u0442\u0438\u0445 \u043e\u0442\u0441\u044b\u043b\u043e\u043a \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0440\u043e\u0434\u0438\u0442\u044c \u0434\u0438\u0441\u043a\u0443\u0441\u0441\u0438\u044e, \u0432\u0435\u0434\u044c \u0432\u043f\u043e\u043b\u043d\u0435 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u0447\u0442\u043e \u044d\u0442\u0438 &#171;\u0443\u0434\u043e\u0431\u043d\u044b\u0435 \u0432\u0435\u0449\u0438&#187; \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0432 Go, \u043f\u0440\u043e\u0441\u0442\u043e \u044f \u0438\u0445 \u043d\u0435 \u043d\u0430\u0448\u0435\u043b. <\/em><\/p>\n<p><em>\u0422\u0430\u043a\u0436\u0435, \u043e\u0442\u043c\u0435\u0447\u0443, \u0447\u0442\u043e \u0432\u0441\u0435 \u0447\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0438\u0441\u0430\u043d\u043e \u043d\u0438\u0436\u0435, \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0433\u043e\u0440\u0430\u0437\u0434\u043e \u043f\u0440\u043e\u0449\u0435 (\u0431\u0435\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u043b\u043e\u0438 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435), \u043d\u043e \u043c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0441 \u0446\u0435\u043b\u044c\u044e \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u044f \u0438 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0432 &#171;\u0447\u0438\u0441\u0442\u043e\u0439&#187; \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435. \u0414\u0430 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a \u043f\u0440\u043e\u0449\u0435.<\/em><\/p>\n<p><em>\u0425\u0432\u0430\u0442\u0438\u0442 \u043f\u0440\u0435\u043b\u044e\u0434\u0438\u0439, \u0432\u043f\u0435\u0440\u0435\u0434 \u0432 \u0431\u043e\u0439!<\/em><\/p>\n<h2>\u0418\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442<\/h2>\n<details class=\"spoiler\">\n<summary>\u0410\u043d\u0438\u043c\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0431\u0443\u0434\u0443\u0449\u0435\u0433\u043e \u0431\u043e\u0442\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7dd\/f71\/ee8\/7ddf71ee8552c630ec637d44b2a2e923.gif\" width=\"1278\" height=\"622\"><figcaption><\/figcaption><\/figure>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0442\u0435\u0445, \u043a\u043e\u043c\u0443 \u0447\u0438\u0442\u0430\u0442\u044c \u043a\u043e\u0434 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0435\u0439, \u0447\u0435\u043c \u0441\u0442\u0430\u0442\u044c\u044e \u2014 \u043f\u0440\u043e\u0448\u0443 <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\" rel=\"noopener noreferrer nofollow\">\u0441\u044e\u0434\u0430<\/a>.<\/p>\n<h2>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<p>\u0420\u0430\u0437\u043e\u0431\u044c\u0435\u043c \u043d\u0430\u0448\u0443 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0441\u043b\u043e\u0438. \u0423 \u043d\u0430\u0441 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f \u0441\u043b\u043e\u0439 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (<em>web<\/em>), \u0441\u043b\u043e\u0439 \u0434\u043b\u044f \u0440\u0438\u0441\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 <a href=\"https:\/\/api.slack.com\/block-kit\" rel=\"noopener noreferrer nofollow\">Slack UI Block Kit <\/a>(<em>ui<\/em>), \u0441\u043b\u043e\u0439 \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 (<em>storage<\/em>), \u0430 \u0442\u0430\u043a\u0436\u0435 \u043c\u0435\u0441\u0442\u043e \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a (<em>config<\/em>). \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u0430\u043f\u043a\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435:<\/p>\n<pre><code class=\"go\">config\/ storage\/ ui\/ web\/ -- clients\/ -- server\/ main.go<\/code><\/pre>\n<h2>\u0421\u0435\u0440\u0432\u0435\u0440<\/h2>\n<p>\u0414\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0438\u0437 \u043f\u0430\u043a\u0435\u0442\u0430 <code>http<\/code>. \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 <code>Server<\/code> \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u0432\u0438\u0434\u0430 \u0432 <code>web -&gt; server<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package server  import ( \t\"context\" \t\"log\" \t\"net\/http\" \t\"os\" \t\"os\/signal\" \t\"sync\/atomic\" \t\"time\" )  type Server struct {   \/\/ \u0417\u0434\u0435\u0441\u044c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u043d\u0430\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0438\u0445 \u043d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 main.go \thealthy        int32 \tlogger         *log.Logger }  func NewServer(logger *log.Logger) *Server { \treturn &amp;Server{ \t\tlogger: logger, \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432. \u0415\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u0432 \u0434\u043b\u044f \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u0430\u043c\u0438 \u0438 \u0438\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043c\u043e\u0436\u043d\u043e \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0442\u044c \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0432\u0441\u0435 \u0432 <code>main.go<\/code>, \u0442\u0430\u043c \u0436\u0435 \u0433\u0434\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u044b \u043d\u0430\u0448\u0438\u0445 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u0432. \u041d\u043e \u044d\u0442\u043e \u043f\u043b\u043e\u0445\u043e\u0439 \u043f\u0443\u0442\u044c. \u0415\u0449\u0435 \u0435\u0441\u0442\u044c \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438 \u043f\u0440\u043e\u0441\u0442\u043e \u0438\u0445 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c. \u041d\u043e \u0432 \u0442\u0430\u043a\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0441\u043b\u043e\u0436\u043d\u043e \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442 \u0442\u0435\u0441\u0442\u0430\u043c\u0438. \u0414\u0430\u043b\u044c\u0448\u0435 \u043c\u044b \u0443\u0432\u0438\u0434\u0438\u043c \u043f\u043b\u044e\u0441\u044b \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u043d\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434\u0430. \u0418\u0442\u0430\u043a, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440. \u041d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">func (s *Server) setupRouter() http.Handler {  \/\/ TODO \trouter := http.NewServeMux()   return router }  func (s *Server) Serve(address string) { \tserver := &amp;http.Server{ \t\tAddr:         address,     Handler:      s.setupRouter(), \t\tErrorLog:     s.logger, \/\/ \u041d\u0430\u0448 \u043b\u043e\u0433\u0433\u0435\u0440 \t\tReadTimeout:  5 * time.Second, \t\tWriteTimeout: 10 * time.Second, \t\tIdleTimeout:  15 * time.Second, \t}    \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043a\u0430\u043d\u0430\u043b\u044b \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \tdone := make(chan bool) \tquit := make(chan os.Signal, 1)   \/\/ \u041d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c \u0441\u0438\u0433\u043d\u0430\u043b \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \tsignal.Notify(quit, os.Interrupt)  \tgo func() { \t\t&lt;-quit \t\ts.logger.Println(\"Server is shutting down...\")     \/\/ \u042d\u0442\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0434\u043b\u044f healthcheck'\u0430 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \t\tatomic.StoreInt32(&amp;s.healthy, 0)      \/\/ \u0414\u0430\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u0434\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u0431\u0443\u0434\u0435\u0442 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) \t\tdefer cancel()      \/\/ \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u044b \t\tserver.SetKeepAlivesEnabled(false)     \/\/ \u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \t\tif err := server.Shutdown(ctx); err != nil { \t\t\ts.logger.Fatalf(\"Could not gracefully shutdown the server: %v\\n\", err) \t\t} \t\tclose(done) \t}()  \ts.logger.Println(\"Server is ready to handle requests at\", address)   \/\/ \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043b\u0441\u044f \u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e \tatomic.StoreInt32(&amp;s.healthy, 1)   \/\/ \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \tif err := server.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed { \t\ts.logger.Fatalf(\"Could not listen on %s: %v\\n\", address, err) \t}    \/\/ \u041a\u043e\u0433\u0434\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0441\u043d\u043e\u0432\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u043b\u043e\u0433\u0438\u0440\u0443\u0435\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \t&lt;-done \ts.logger.Println(\"Server stopped\") } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u0445\u044d\u043d\u0434\u043b\u0435\u0440. \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0430\u043f\u043a\u0443 \u0432 <code>web -&gt; server -&gt; handlers<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>healthcheck.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers  import ( \t\"net\/http\" )  func Healthcheck() http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { \t\tw.Write(\"OK\") \t}) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043d\u0430\u0448 \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">\/\/ \u041d\u0430\u0448 \u043a\u043e\u0434 \u0432\u044b\u0448\u0435  func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t)   return router }  \/\/ \u041d\u0430\u0448 \u043a\u043e\u0434 \u043d\u0438\u0436\u0435<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418\u0434\u0435\u043c \u0432 <code>main.go<\/code> \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440:<\/p>\n<pre><code class=\"go\">package main  import ( \t\"log\"   \"os\"   \"go-scrum-poker-bot\/web\/server\" )  func main() {   \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043b\u043e\u0433\u0433\u0435\u0440 \u0441\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u043c\u0438 \u0444\u043b\u0430\u0433\u0430\u043c\u0438 \u0438 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \"INFO:\".    \/\/ \u041f\u0438\u0441\u0430\u0442\u044c \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 stdout \tlogger := log.New(os.Stdout, \"INFO: \", log.LstdFlags) \tapp := server.NewServer(logger)  \tapp.Serve(\":8000\") }<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0431\u0443\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442:<\/p>\n<pre><code class=\"bash\">go run main.go<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0442\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0441\u044f \u043d\u0430 <code>:8000<\/code> \u043f\u043e\u0440\u0442\u0443. \u041d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043a \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 \u043d\u0438\u0445 \u043b\u044e\u0431\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u042d\u0442\u043e \u043d\u0430\u043c \u0435\u0449\u0435 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b. \ud83d\ude09 \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0438\u0434\u0442\u0438 \u0434\u0430\u043b\u044c\u0448\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0430\u0448\u0443 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0440\u0435\u0434\u0443, \u0447\u0442\u043e\u0431\u044b Slack \u0441\u043c\u043e\u0433 \u0441 \u043d\u0430\u043c\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c.<\/p>\n<h2>NGROK<\/h2>\n<p>\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u0443 \u043d\u0430\u0448\u0435\u0433\u043e \u0431\u043e\u0442\u0430, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0435\u0431\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c <a href=\"https:\/\/ngrok.com\/\" rel=\"noopener noreferrer nofollow\">ngrok<\/a>. \u0412\u043e\u043e\u0431\u0449\u0435 \u043c\u043e\u0436\u043d\u043e \u043b\u044e\u0431\u043e\u0439 \u0434\u0440\u0443\u0433\u043e\u0439, \u043d\u043e \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u0438 \u043f\u0440\u043e\u0441\u0442 \u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438. \u0414\u0430 \u0438 Slack \u0435\u0433\u043e <a href=\"https:\/\/api.slack.com\/tutorials\/tunneling-with-ngrok\" rel=\"noopener noreferrer nofollow\">\u0441\u043e\u0432\u0435\u0442\u0443\u0435\u0442<\/a>. \u0412 \u043e\u0431\u0449\u0435\u043c, \u043a\u043e\u0433\u0434\u0430 \u0432\u0441\u0435 \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439:<\/p>\n<pre><code>ngrok http 8000<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0442\u043e \u0432\u044b \u0443\u0432\u0438\u0434\u0438\u0442\u0435 \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 \u044d\u0442\u043e\u0433\u043e:<\/p>\n<pre><code>ngrok by @inconshreveable                                                                                                            (Ctrl+C to quit)                                                                                                                                                       Session Status                online                                                                                                                  Account                       Sayakhov Ilya (Plan: Free)                                                                                              Version                       2.3.35                                                                                                                  Region                        United States (us)                                                                                                      Web Interface                 http:\/\/127.0.0.1:4040                                                                                                   Forwarding                    http:\/\/ffd3cfcc460c.ngrok.io -&gt; http:\/\/localhost:8000                                                                   Forwarding                    https:\/\/ffd3cfcc460c.ngrok.io -&gt; http:\/\/localhost:8000                                                                                                                                                                                                                        Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                           0       0       0.00    0.00    0.00    0.00     <\/code><\/pre>\n<p>\u041d\u0430\u0441 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u0435\u0442 \u0441\u0442\u0440\u043e\u0447\u043a\u0430 <code>https:\/\/ffd3cfcc460c.ngrok.io<\/code>. \u041e\u043d\u0430 \u043d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0434\u0430\u043b\u044c\u0448\u0435.<\/p>\n<h2>Slash commands<\/h2>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 Slack. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 <a href=\"https:\/\/api.slack.com\/apps\" rel=\"noopener noreferrer nofollow\">\u0441\u044e\u0434\u0430<\/a><code> -&gt; Create New App<\/code>. \u0414\u0430\u043b\u0435\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u0438\u043c\u044f <code>GoScrumPokerBot<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0435\u0433\u043e \u0432 \u0441\u0432\u043e\u0439 <code>Workspace<\/code>. \u0414\u0430\u043b\u0435\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0434\u0430\u0442\u044c \u043d\u0430\u0448\u0435\u043c\u0443 \u0431\u043e\u0442\u0443 \u043f\u0440\u0430\u0432\u0430. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0434\u0435\u043c \u0432 <code>OAuth &amp; Permissions -&gt; Scopes<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u0440\u0430\u0432\u0430: <code>chat:write<\/code>, <code>commands<\/code>. \u041f\u0435\u0440\u0432\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043f\u0440\u0430\u0432 \u043d\u0443\u0436\u0435\u043d, \u0447\u0442\u043e\u0431\u044b \u0431\u043e\u0442 \u043c\u043e\u0433 \u043f\u0438\u0441\u0430\u0442\u044c \u0432 \u043a\u0430\u043d\u0430\u043b\u044b, \u0430 \u0432\u0442\u043e\u0440\u043e\u0439 \u0434\u043b\u044f slash \u043a\u043e\u043c\u0430\u043d\u0434. \u0418 \u043d\u0430\u043a\u043e\u043d\u0435\u0446 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u043d\u0430 <code>Reinstall to Workspace<\/code>. \u0413\u043e\u0442\u043e\u0432\u043e! \u0422\u0435\u043f\u0435\u0440\u044c \u0438\u0434\u0435\u043c \u0432 \u0440\u0430\u0437\u0434\u0435\u043b <code>Slash commands<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 <code>\/poker<\/code> . <\/p>\n<p>\u0412 Request URL \u043d\u0443\u0436\u043d\u043e \u0432\u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0438\u0437 \u043f\u0443\u043d\u043a\u0442\u0430 \u0432\u044b\u0448\u0435 + \u043f\u0443\u0442\u044c. \u041f\u0443\u0441\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u0442\u0430\u043a: <code>https:\/\/ffd3cfcc460c.ngrok.io\/play-poker<\/code>.<\/p>\n<h2>Slash command handler<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u043a\u043e\u043c\u0430\u043d\u0434\u0443. \u0418\u0434\u0435\u043c \u0432 <code>web -&gt; server -&gt; handlers<\/code> \u0438 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <code>play_poker.go<\/code>:<\/p>\n<pre><code class=\"go\">func PlayPokerCommand() http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {     w.Header().Set(\"Content-Type\", \"application\/json\")     w.Write([]byte(`{\"response_type\": \"ephemeral\", \"text\": \"Hello world!\"}`)) \t}) }<\/code><\/pre>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448 \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t) \trouter.Handle( \t\t\"\/play-poker\", \t\thandlers.PlayPokerCommand(), \t)   return router }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418\u0434\u0435\u043c \u0432 Slack \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443: <code>\/poker<\/code>. \u0412 \u043e\u0442\u0432\u0435\u0442 \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 \u044d\u0442\u043e\u0433\u043e:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7da\/b0d\/d67\/7dab0dd67e55b0502bd1163d00f82834.png\" width=\"924\" height=\"170\"><figcaption><\/figcaption><\/figure>\n<p>\u041d\u043e \u044d\u0442\u043e \u043d\u0435 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u043e Slack. \u041c\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u043c \u0441\u043b\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043a\u0430\u043d\u0430\u043b. \u042d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043c\u043d\u0435 \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u0438 \u043f\u043b\u044e\u0441 \u0443 \u043d\u0435\u0433\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0435\u0439 \u0432 \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0438 \u0441 \u043e\u0442\u0432\u0435\u0442\u043e\u043c \u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0443. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432 \u0444\u043e\u043d\u0435 (\u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u043e\u043b\u0433\u0438\u0445 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439). \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043d\u0430\u0448 <code>http<\/code> \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0418\u0434\u0435\u043c \u0432 <code>web -&gt; clients<\/code>. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <code>client.go<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>client.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432 type Handler func(request *Request) *Response  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f middleware (\u043e \u043d\u0438\u0445 \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435) type Middleware func(handler Handler, request *Request) Handler  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 http \u043a\u043b\u0438\u0435\u043d\u0442\u0430 type Client interface { \tMake(request *Request) *Response }  \/\/ \u041d\u0430\u0448\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 type BasicClient struct { \tclient     *http.Client \tmiddleware []Middleware }  func NewBasicClient(client *http.Client, middleware []Middleware) Client { \treturn &amp;BasicClient{client: client, middleware: middleware} }  \/\/ \u041f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0432\u0441\u0435\u0439 \u0433\u0440\u044f\u0437\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b func (c *BasicClient) makeRequest(request *Request) *Response { \tpayload, err := request.ToBytes() \/\/ TODO \tif err != nil { \t\treturn &amp;Response{Error: err} \t}    \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043d\u043e\u0432\u044b\u0439 request, \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u0432 \u043d\u0435\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u0435 \treq, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(payload)) \tif err != nil { \t\treturn &amp;Response{Error: err} \t}    \/\/ \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \tfor name, value := range request.Headers { \t\treq.Header.Add(name, value) \t}    \/\/ \u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u0437\u0430\u043f\u0440\u043e\u0441 \tresp, err := c.client.Do(req) \tif err != nil { \t\treturn &amp;Response{Error: err} \t} \tdefer resp.Body.Close()    \/\/ \u0427\u0438\u0442\u0430\u0435\u043c \u0442\u0435\u043b\u043e \u043e\u0442\u0432\u0435\u0442\u0430 \tbody, err := ioutil.ReadAll(resp.Body) \tif err != nil { \t\treturn &amp;Response{Error: err} \t}  \terr = nil   \/\/ \u0415\u0441\u043b\u0438 \u0432\u0435\u0440\u043d\u0443\u043b\u043e\u0441\u044c \u0447\u0442\u043e-\u0442\u043e \u043e\u0442\u043b\u0438\u0447\u043d\u043e\u0435 \u0432\u044b\u0448\u0435 \u0438\u043b\u0438 \u043d\u0438\u0436\u0435 20x, \u0442\u043e \u043e\u0448\u0438\u0431\u043a\u0430 \tif resp.StatusCode &gt; http.StatusIMUsed || resp.StatusCode &lt; http.StatusOK { \t\terr = fmt.Errorf(\"Bad response. Status: %d, Body: %s\", resp.StatusCode, string(body)) \t}  \treturn &amp;Response{ \t\tStatus:  resp.StatusCode, \t\tBody:    body, \t\tHeaders: resp.Header, \t\tError:   err, \t} }  \/\/ \u041d\u0430\u0448 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 func (c *BasicClient) Make(request *Request) *Response { \tif request.Headers == nil { \t\trequest.Headers = make(map[string]string) \t}      \/\/ \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c middleware \thandler := c.makeRequest \tfor _, middleware := range c.middleware { \t\thandler = middleware(handler, request) \t}  \treturn handler(request) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0444\u0430\u0439\u043b <code>web -&gt; clients<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>request.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients  import \"encoding\/json\"  type Request struct { \tURL     string \tMethod  string \tHeaders map[string]string \tJson    interface{} }  func (r *Request) ToBytes() ([]byte, error) { \tif r.Json != nil { \t\tresult, err := json.Marshal(r.Json) \t\tif err != nil { \t\t\treturn []byte{}, err \t\t} \t\treturn result, nil \t}  \treturn []byte{}, nil } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0421\u0440\u0430\u0437\u0443 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u043a \u043c\u0435\u0442\u043e\u0434\u0443 <code>ToBytes()<\/code>. \u0414\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u044f \u0432\u0437\u044f\u043b <a href=\"https:\/\/github.com\/stretchr\/testify\" rel=\"noopener noreferrer nofollow\">testify\/assert<\/a>, \u0442\u0430\u043a \u043a\u0430\u043a \u0431\u0435\u0437 \u043d\u0435\u0435 \u0431\u044b\u043b\u0430 \u0431\u044b \u043a\u0443\u0447\u0430 if&#8217;\u043e\u0432, \u0430 \u043c\u0435\u043d\u044f \u043e\u043d\u0438 \u043d\u0430\u043f\u0440\u044f\u0433\u0430\u044e\u0442 \ud83d\ude42 . \u041a \u0442\u043e\u043c\u0443 \u0436\u0435, \u044f \u043f\u0440\u0438\u0432\u044b\u043a \u043a <code>pytest<\/code> \u0438 \u0435\u0433\u043e <code>assert<\/code>, \u0434\u0430 \u0438 \u043a\u0430\u043a-\u0442\u043e \u0433\u043b\u0430\u0437\u0443 \u043f\u0440\u0438\u044f\u0442\u043d\u0435\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>request_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients_test  import ( \t\"encoding\/json\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"reflect\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  func TestRequestToBytes(t *testing.T) {   \/\/ \u0417\u0434\u0435\u0441\u044c \u043c\u044b \u0434\u0435\u043b\u0430\u0435\u043c \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 pytest.parametrize (\u0436\u0430\u043b\u044c, \u0447\u0442\u043e \u0432 Go \u043d\u0435\u0442 \u0441\u0430\u0445\u0430\u0440\u0430 \u0434\u043b\u044f \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u0432, \u044d\u0442\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u0443\u0434\u043e\u0431\u043d\u0435\u0435) \ttestCases := []struct { \t\tjson interface{} \t\tdata []byte \t\terr  error \t}{ \t\t{map[string]string{\"test_key\": \"test_value\"}, []byte(\"{\\\"test_key\\\":\\\"test_value\\\"}\"), nil}, \t\t{nil, []byte{}, nil}, \t\t{make(chan int), []byte{}, &amp;json.UnsupportedTypeError{Type: reflect.TypeOf(make(chan int))}}, \t}    \/\/ \u041f\u0440\u043e\u0445\u043e\u0434\u0438\u043c\u0441\u044f \u043f\u043e \u043d\u0430\u0448\u0438\u043c \u0442\u0435\u0441\u0442 \u043a\u0435\u0439\u0441\u0430\u043c \tfor _, testCase := range testCases { \t\trequest := clients.Request{ \t\t\tURL:     \"https:\/\/example.com\", \t\t\tMethod:  \"GET\", \t\t\tHeaders: nil, \t\t\tJson:    testCase.json, \t\t}  \t\tactual, err := request.ToBytes()      \/\/ \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \t\tassert.Equal(t, testCase.err, err) \t\tassert.Equal(t, testCase.data, actual) \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u043d\u0430\u043c \u043d\u0443\u0436\u0435\u043d <code>web -&gt; clients:<\/code><\/p>\n<details class=\"spoiler\">\n<summary>response.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients  import \"encoding\/json\"  type Response struct { \tStatus  int \tHeaders map[string][]string \tBody    []byte \tError   error }  \/\/ \u042f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u043b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043f\u0440\u0438\u0432\u0435\u0437\u0442\u0438 \u043b\u044e\u0431\u043e\u0439 \u043e\u0442\u0432\u0435\u0442 \u043a \u043d\u0443\u0436\u043d\u043e\u043c\u0443 \u0438 \u043d\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u044d\u0442\u0438 \u0431\u043e\u0433\u043e\u043c\u0435\u0440\u0437\u043a\u0438\u0435 if err != nil func (r *Response) Json(to interface{}) error { \tif r.Error != nil { \t\treturn r.Error \t} \treturn json.Unmarshal(r.Body, to) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u0442\u0430\u043a\u0436\u0435, \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u0430 <code>Json(to interface{})<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>response_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients_test  import ( \t\"errors\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  \/\/ \u041e\u0434\u0438\u043d \u0442\u0435\u0441\u0442 \u043d\u0430 \u043f\u043e\u0437\u0438\u0442\u0438\u0432\u043d\u044b\u0439 \u043a\u0435\u0439\u0441 func TestResponseJson(t *testing.T) { \tto := struct { \t\tTestKey string `json:\"test_key\"` \t}{} \tresponse := clients.Response{ \t\tStatus:  200, \t\tHeaders: nil, \t\tBody:    []byte(`{\"test_key\": \"test_value\"}`), \t\tError:   nil, \t}  \terr := response.Json(&amp;to)  \tassert.Equal(t, nil, err) \tassert.Equal(t, \"test_value\", to.TestKey) }  \/\/ \u041e\u0434\u0438\u043d \u0442\u0435\u0441\u0442 \u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0443 func TestResponseJsonError(t *testing.T) { \texpectedErr := errors.New(\"Error!\") \tresponse := clients.Response{ \t\tStatus:  200, \t\tHeaders: nil, \t\tBody:    nil, \t\tError:   expectedErr, \t}  \terr := response.Json(map[string]string{})  \tassert.Equal(t, expectedErr, err) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c, \u043a\u043e\u0433\u0434\u0430 \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0434\u043b\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0415\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u0434\u043b\u044f <code>http<\/code> \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u042f \u0432\u044b\u0431\u0440\u0430\u043b \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0441 \u043f\u043e\u0434\u043c\u0435\u043d\u043e\u0439 <code>http<\/code> \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0430. \u041e\u0434\u043d\u0430\u043a\u043e \u0435\u0441\u0442\u044c \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b, \u043d\u043e \u044d\u0442\u043e\u0442 \u043c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u0441\u044f \u0443\u0434\u043e\u0431\u043d\u0435\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>client_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients_test  import ( \t\"bytes\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"io\/ioutil\" \t\"net\/http\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  \/\/ \u0414\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u043e\u0431\u044a\u044f\u0432\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f type RoundTripFunc func(request *http.Request) *http.Response  func (f RoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { \treturn f(request), nil }  \/\/ \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 mock \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 func NewTestClient(fn RoundTripFunc) *http.Client { \treturn &amp;http.Client{ \t\tTransport: RoundTripFunc(fn), \t} }  \/\/ \u0412\u0430\u043b\u0438\u0434\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 func TestMakeRequest(t *testing.T) { \turl := \"https:\/\/example.com\/ok\"    \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c mock \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0438\u0448\u0435\u043c \u043d\u0443\u0436\u043d\u044b\u0439 \u043d\u0430\u043c \u043e\u0442\u0432\u0435\u0442 \thttpClient := NewTestClient(func(req *http.Request) *http.Response { \t\tassert.Equal(t, req.URL.String(), url)  \t\treturn &amp;http.Response{ \t\t\tStatusCode: http.StatusOK, \t\t\tBody:       ioutil.NopCloser(bytes.NewBufferString(\"OK\")), \t\t\tHeader:     make(http.Header), \t\t} \t})    \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043d\u0430\u0448\u0435\u0433\u043e http \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0441 \u0437\u0430\u043c\u043e\u043a\u0430\u043d\u043d\u044b\u043c http \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c \twebClient := clients.NewBasicClient(httpClient, nil) \tresponse := webClient.Make(&amp;clients.Request{ \t\tURL:     url, \t\tMethod:  \"GET\", \t\tHeaders: map[string]string{\"Content-Type\": \"application\/json\"}, \t\tJson:    nil, \t})  \tassert.Equal(t, http.StatusOK, response.Status) }  \/\/ \u0422\u0435\u0441\u0442 \u043d\u0430 \u043e\u0448\u0438\u0431\u043e\u0447\u043d\u044b\u0439 response func TestMakeRequestError(t *testing.T) { \turl := \"https:\/\/example.com\/error\"  \thttpClient := NewTestClient(func(req *http.Request) *http.Response { \t\tassert.Equal(t, req.URL.String(), url)  \t\treturn &amp;http.Response{ \t\t\tStatusCode: http.StatusBadGateway, \t\t\tBody:       ioutil.NopCloser(bytes.NewBufferString(\"Bad gateway\")), \t\t\tHeader:     make(http.Header), \t\t} \t})  \twebClient := clients.NewBasicClient(httpClient, nil) \tresponse := webClient.Make(&amp;clients.Request{ \t\tURL:     url, \t\tMethod:  \"GET\", \t\tHeaders: map[string]string{\"Content-Type\": \"application\/json\"}, \t\tJson:    nil, \t})  \tassert.Equal(t, http.StatusBadGateway, response.Status) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0442\u043b\u0438\u0447\u043d\u043e! \u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c <code>middleware<\/code>. \u042f \u043f\u0440\u0438\u0432\u044b\u043a \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439, \u0434\u0430\u0436\u0435 \u0441\u0430\u043c\u043e\u0439 \u043c\u0430\u043b\u0435\u043d\u044c\u043a\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438, \u043f\u0438\u0441\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u043c\u0430\u043b\u0435\u043d\u044c\u043a\u0443\u044e <code>middleware<\/code>. \u0422\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u043b\u0435\u0433\u043a\u043e \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a\u043e\u0439 \u043a\u043e\u0434 \u0432 \u0440\u0430\u0437\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u0445 \/ \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 API \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u043a \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430\u043c \/ \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435. Slack \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 \u043a\u0430\u043d\u0430\u043b \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c <code>Authorization<\/code> \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0441 \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 <code>OAuth &amp; Permissions<\/code>. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0432 <code>web -&gt; clients -&gt; middleware<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>auth.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package middleware  import ( \t\"fmt\" \t\"go-scrum-poker-bot\/web\/clients\" )  \/\/ \u0422\u043e\u043a\u0435\u043d \u0431\u0443\u0434\u0435\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u043f\u0440\u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0438 middleware \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 func Auth(token string) clients.Middleware { \treturn func(handler clients.Handler, request *clients.Request) clients.Handler { \t\treturn func(request *clients.Request) *clients.Response { \t\t\trequest.Headers[\"Authorization\"] = fmt.Sprintf(\"Bearer %s\", token) \t\t\treturn handler(request) \t\t} \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442 \u043a \u043d\u0435\u0439:<\/p>\n<details class=\"spoiler\">\n<summary>auth_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package middleware_test  import ( \t\"fmt\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"go-scrum-poker-bot\/web\/clients\/middleware\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  func TestAuthMiddleware(t *testing.T) { \ttoken := \"test\" \trequest := &amp;clients.Request{ \t\tHeaders: map[string]string{}, \t} \thandler := middleware.Auth(token)( \t\tfunc(request *clients.Request) *clients.Response { \t\t\treturn &amp;clients.Response{} \t\t}, \t\trequest, \t) \thandler(request)  \tassert.Equal(t, map[string]string{\"Authorization\": fmt.Sprintf(\"Bearer %s\", token)}, request.Headers) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0430\u043a\u0436\u0435 \u0432 <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\/tree\/master\/web\/clients\/middleware\" rel=\"noopener noreferrer nofollow\">\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438<\/a> \u0432\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 <code>middleware<\/code> \u0434\u043b\u044f \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 <code>Content-Type: application\/json<\/code>. \u0417\u0434\u0435\u0441\u044c \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u043a\u043e\u0434 \u0432 \u0446\u0435\u043b\u044f\u0445 \u044d\u043a\u043e\u043d\u043e\u043c\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0438 \u043c\u0435\u0441\u0442\u0430 :). <\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u0438\u0448\u0435\u043c \u043d\u0430\u0448 <code>PlayPoker<\/code> \u0445\u044d\u043d\u0434\u043b\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>play_poker.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers  import ( \t\"errors\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"go-scrum-poker-bot\/web\/server\/models\" \t\"net\/http\"  \t\"github.com\/google\/uuid\" )  func PlayPokerCommand(webClient clients.Client, uiBuilder *ui.Builder) http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {     \/\/ \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443, \u0447\u0442\u043e \u043d\u0430\u043c \u043f\u0440\u0438\u0448\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 POST Form \u0441 \u0442\u0435\u043a\u0441\u0442\u043e\u043c \u0438 ID \u043a\u0430\u043d\u0430\u043b\u0430 \t\tif r.PostFormValue(\"channel_id\") == \"\" || r.PostFormValue(\"text\") == \"\" { \t\t\tw.Write(models.ResponseError(errors.New(\"Please write correct subject\"))) \/\/ TODO \t\t\treturn \t\t}  \t\tresp := webClient.Make(&amp;clients.Request{ \t\t\tURL:    \"https:\/\/slack.com\/api\/chat.postMessage\", \t\t\tMethod: \"POST\",       Json: uiBuilder.Build( \/\/ TODO: \u041d\u0430\u043f\u0438\u0448\u0435\u043c builder \u043f\u043e\u0437\u0436\u0435 \t\t\t\tr.PostFormValue(\"channel_id\"), \t\t\t\tuuid.New().String(), \t\t\t\tr.PostFormValue(\"text\"), \t\t\t\tnil, \t\t\t\tfalse, \t\t\t), \t\t}) \t\tif resp.Error != nil { \t\t\tw.Write(models.ResponseError(resp.Error)) \/\/ TODO \t\t\treturn \t\t} \t}) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0432 <code>web -&gt; server -&gt; models<\/code> . \u0424\u0430\u0439\u043b <code>errors.go<\/code> \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043e\u043a:<\/p>\n<details class=\"spoiler\">\n<summary>errors.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package models  import ( \t\"encoding\/json\" \t\"fmt\" )  type SlackError struct { \tResponseType string `json:\"response_type\"` \tText         string `json:\"text\"` }  func ResponseError(err error) []byte { \tresp, err := json.Marshal( \t\tSlackError{ \t\t\tResponseType: \"ephemeral\", \t\t\tText:         fmt.Sprintf(\"Sorry, there is some error happened. Error: %s\", err.Error()), \t\t}, \t) \tif err != nil { \t\treturn []byte(\"Sorry. Some error happened\") \t} \treturn resp } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0434\u043b\u044f \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u0430:<\/p>\n<details class=\"spoiler\">\n<summary>play_poker_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers_test  import ( \t\"errors\" \t\"go-scrum-poker-bot\/config\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/server\/handlers\" \t\"go-scrum-poker-bot\/web\/server\/models\" \t\"net\/http\" \t\"net\/http\/httptest\" \t\"net\/url\" \t\"strings\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  func TestPlayPokerHandler(t *testing.T) { \tconfig := config.NewConfig() \/\/ TODO \tmockClient := &amp;MockClient{} \tuiBuilder := ui.NewBuilder(config) \/\/ TODO  \tresponseRec := httptest.NewRecorder()  \trouter := http.NewServeMux() \trouter.Handle(\"\/play-poker\", handlers.PlayPokerCommand(mockClient, uiBuilder))  \tpayload := url.Values{\"channel_id\": {\"test\"}, \"text\": {\"test\"}}.Encode() \trequest, err := http.NewRequest(\"POST\", \"\/play-poker\", strings.NewReader(payload)) \trequest.Header.Set(\"Content-Type\", \"application\/x-www-form-urlencoded\")  \trouter.ServeHTTP(responseRec, request)  \tassert.Nil(t, err) \tassert.Equal(t, http.StatusOK, responseRec.Code) \tassert.Empty(t, responseRec.Body.String()) \tassert.Equal(t, true, mockClient.Called) }  func TestPlayPokerHandlerEmptyBodyError(t *testing.T) { \tconfig := config.NewConfig() \tmockClient := &amp;MockClient{} \tuiBuilder := ui.NewBuilder(config)  \tresponseRec := httptest.NewRecorder()  \trouter := http.NewServeMux() \trouter.Handle(\"\/play-poker\", handlers.PlayPokerCommand(mockClient, uiBuilder))  \tpayload := url.Values{}.Encode() \trequest, _ := http.NewRequest(\"POST\", \"\/play-poker\", strings.NewReader(payload)) \trequest.Header.Set(\"Content-Type\", \"application\/x-www-form-urlencoded\")  \trouter.ServeHTTP(responseRec, request)  \texpected := string(models.ResponseError(errors.New(\"Please write correct subject\")))  \tassert.Equal(t, http.StatusOK, responseRec.Code) \tassert.Equal(t, expected, responseRec.Body.String()) \tassert.Equal(t, false, mockClient.Called) }  func TestPlayPokerHandlerRequestError(t *testing.T) { \terrMsg := \"Error msg\" \tconfig := config.NewConfig() \/\/ TODO \tmockClient := &amp;MockClient{Error: errMsg} \tuiBuilder := ui.NewBuilder(config) \/\/ TODO  \tresponseRec := httptest.NewRecorder()  \trouter := http.NewServeMux() \trouter.Handle(\"\/play-poker\", handlers.PlayPokerCommand(mockClient, uiBuilder))  \tpayload := url.Values{\"channel_id\": {\"test\"}, \"text\": {\"test\"}}.Encode() \trequest, _ := http.NewRequest(\"POST\", \"\/play-poker\", strings.NewReader(payload)) \trequest.Header.Set(\"Content-Type\", \"application\/x-www-form-urlencoded\")  \trouter.ServeHTTP(responseRec, request)  \texpected := string(models.ResponseError(errors.New(errMsg)))  \tassert.Equal(t, http.StatusOK, responseRec.Code) \tassert.Equal(t, expected, responseRec.Body.String()) \tassert.Equal(t, true, mockClient.Called) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c <code>mock<\/code> \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e <code>http<\/code> \u043a\u043b\u0438\u0435\u043d\u0442\u0430:<\/p>\n<details class=\"spoiler\">\n<summary>common_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers_test  import ( \t\"errors\" \t\"go-scrum-poker-bot\/web\/clients\" )  type MockClient struct { \tCalled bool \tError  string }  func (c *MockClient) Make(request *clients.Request) *clients.Response { \tc.Called = true  \tvar err error = nil \tif c.Error != \"\" { \t\terr = errors.New(c.Error) \t} \treturn &amp;clients.Response{Error: err} }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u0438\u0442\u0435, \u043a\u043e\u0434 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u0430 <code>PlayPoker<\/code> \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u044b\u0439 \u0438 \u0435\u0433\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0438 \u043d\u0435 \u0441\u0442\u0440\u0430\u0448\u043d\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0447\u0435\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u043a \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044e UI \u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u0432 \u0434\u043b\u044f <a href=\"https:\/\/api.slack.com\/block-kit\" rel=\"noopener noreferrer nofollow\">Slack UI Block Kit<\/a>. \u0422\u0430\u043c \u0432\u0441\u0435 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e, \u043d\u043e \u043c\u043d\u043e\u0433\u043e \u043e\u0434\u043d\u043e\u0442\u0438\u043f\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430. \u041e\u0442\u043c\u0435\u0447\u0443 \u043b\u0438\u0448\u044c, \u0447\u0442\u043e Slack API \u043c\u043d\u0435 \u043d\u0435 \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u0438 \u0431\u044b\u043b\u043e \u0442\u044f\u0436\u0435\u043b\u043e \u0441 \u043d\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c. \u0421\u0430\u043c <code>UI Builder<\/code> \u043c\u043e\u0436\u043d\u043e \u0433\u043b\u044f\u043d\u0443\u0442\u044c \u0432 \u043f\u0430\u043f\u043a\u0435 <em>ui<\/em> <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\/tree\/master\/ui\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>. \u0410 \u0437\u0434\u0435\u0441\u044c, \u0432 \u0446\u0435\u043b\u044f\u0445 \u044d\u043a\u043e\u043d\u043e\u043c\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043d\u0430 \u043d\u0435\u043c \u0437\u0430\u043e\u0441\u0442\u0440\u044f\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u044f. \u041e\u0442\u043c\u0435\u0447\u0443 \u043b\u0438\u0448\u044c, \u0447\u0442\u043e \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u044f\u043a\u043e\u0440\u044f \u0434\u043b\u044f \u043f\u043e\u043d\u0438\u043c\u0430\u043d\u0438\u044f \u0442\u043e\u0433\u043e, \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043e\u0442 \u043a\u0430\u043a\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043f\u0440\u0438\u0448\u043b\u043e \u0438 \u043a\u0430\u043a\u043e\u0439 \u0431\u044b\u043b \u0442\u0435\u043a\u0441\u0442 \u0434\u043b\u044f \u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043d\u0438\u044f (\u0435\u0433\u043e \u043c\u044b \u043d\u0435 \u0431\u0443\u0434\u0435\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0443 \u0441\u0435\u0431\u044f, \u0430 \u0431\u0443\u0434\u0435\u043c \u0431\u0440\u0430\u0442\u044c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0438\u0437 \u0441\u043e\u0431\u044b\u0442\u0438\u044f) \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>block_id<\/code><em>. <\/em>\u0410 \u0434\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0431\u0443\u0434\u0435\u043c \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043d\u0430 <code>action_id<\/code>.<\/p>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043a\u043e\u043d\u0444\u0438\u0433 \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0418\u0434\u0435\u043c \u0432 <em>config<\/em> \u0438 \u0441\u043e\u0437\u0434\u0430\u0435\u043c:<\/p>\n<details class=\"spoiler\">\n<summary>config.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package config  type Config struct { \tApp   *App \tSlack *Slack \tRedis *Redis }  func NewConfig() *Config { \treturn &amp;Config{ \t\tApp: &amp;App{ \t\t\tServerAddress: getStrEnv(\"WEB_SERVER_ADDRESS\", \":8000\"), \t\t\tPokerRanks:    getListStrEnv(\"POKER_RANKS\", \"?,0,0.5,1,2,3,5,8,13,20,40,100\"), \t\t}, \t\tSlack: &amp;Slack{ \t\t\tToken: getStrEnv(\"SLACK_TOKEN\", \"FILL_ME\"), \t\t},     \/\/ \u0421\u043a\u043e\u0440\u043e \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \t\tRedis: &amp;Redis{ \t\t\tHost: getStrEnv(\"REDIS_HOST\", \"0.0.0.0\"), \t\t\tPort: getIntEnv(\"REDIS_PORT\", \"6379\"), \t\t\tDB:   getIntEnv(\"REDIS_DB\", \"0\"), \t\t}, \t} }  \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 env \u0438\u043b\u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c default func getStrEnv(key string, defaultValue string) string { \tif value, ok := os.LookupEnv(key); ok { \t\treturn value \t} \treturn defaultValue }  \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c int \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 env \u0438\u043b\u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c default func getIntEnv(key string, defaultValue string) int { \tvalue, err := strconv.Atoi(getStrEnv(key, defaultValue)) \tif err != nil { \t\tpanic(fmt.Sprintf(\"Incorrect env value for %s\", key)) \t}  \treturn value }  \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043e\u043a (e.g. 0,1,2,3,4,5) \u0438\u0437 env \u0438\u043b\u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c default func getListStrEnv(key string, defaultValue string) []string { \tvalue := []string{} \tfor _, item := range strings.Split(getStrEnv(key, defaultValue), \",\") { \t\tvalue = append(value, strings.TrimSpace(item)) \t} \treturn value } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u043a \u043d\u0435\u043c\u0443. \u0411\u0443\u0434\u0435\u043c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>config_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package config_test  import (     \"go-scrum-poker-bot\/config\"     \"os\"     \"testing\"      \"github.com\/stretchr\/testify\/assert\" )  func TestNewConfig(t *testing.T) {     c := config.NewConfig()      assert.Equal(t, \"0.0.0.0\", c.Redis.Host)     assert.Equal(t, 6379, c.Redis.Port)     assert.Equal(t, 0, c.Redis.DB)     assert.Equal(t, []string{\"?\", \"0\", \"0.5\", \"1\", \"2\", \"3\", \"5\", \"8\", \"13\", \"20\", \"40\", \"100\"}, c.App.PokerRanks) }  func TestNewConfigIncorrectIntFromEnv(t *testing.T) {     os.Setenv(\"REDIS_PORT\", \"-\")      assert.Panics(t, func() { config.NewConfig() }) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u043b \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u0445\u043e\u0442\u044f \u044d\u0442\u043e \u043d\u0435 \u0441\u0430\u043c\u044b\u0439 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c. \u0418\u0437\u043c\u0435\u043d\u0438\u043c <code>main.go<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>main.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package main  import ( \t\"fmt\" \t\"go-scrum-poker-bot\/config\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/clients\" \tclients_middleware \"go-scrum-poker-bot\/web\/clients\/middleware\" \t\"go-scrum-poker-bot\/web\/server\"   \"log\" \t\"net\/http\" \t\"os\" \t\"time\" )  func main() { \tlogger := log.New(os.Stdout, \"INFO: \", log.LstdFlags) \tconfig := config.NewConfig() \tbuilder := ui.NewBuilder(config) \twebClient := clients.NewBasicClient( \t\t&amp;http.Client{ \t\t\tTimeout: 5 * time.Second, \t\t}, \t\t[]clients.Middleware{ \/\/ \u041d\u0430\u0448\u0438 middleware \t\t\tclients_middleware.Auth(config.Slack.Token), \t\t\tclients_middleware.JsonContentType, \t\t\tclients_middleware.Log(logger), \t\t}, \t)  \tapp := server.NewServer( \t\tlogger, \t\twebClient, \t\tbuilder, \t) \tapp.Serve(config.App.ServerAddress) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b <code>\/poker<\/code> \u043c\u044b \u0432 \u043e\u0442\u0432\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u043c \u043d\u0430\u0448 \u0441\u0438\u043c\u043f\u0430\u0442\u0438\u0447\u043d\u044b\u0439 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u0438\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441. <\/p>\n<h2>Slack Interactivity<\/h2>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u0443\u0447\u0438\u043c\u0441\u044f \u0440\u0435\u0430\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043f\u0440\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441 \u043d\u0438\u043c. \u0417\u0430\u0439\u0434\u0435\u043c <code>Your apps -&gt; \u041d\u0430\u0448 \u0431\u043e\u0442 -&gt; Interactivity &amp; Shortcuts<\/code>. \u0412 <em>Request URL<\/em> \u0432\u0432\u0435\u0434\u0435\u043c:<\/p>\n<pre><code>https:\/\/ffd3cfcc460c.ngrok.io\/interactivity<\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0435\u0449\u0435 \u043e\u0434\u0438\u043d \u0445\u044d\u043d\u0434\u043b\u0435\u0440 <code>InteractionCallback<\/code> \u0432 <code>web -&gt; server -&gt; handlers<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>interaction_callback.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers  import ( \t\"go-scrum-poker-bot\/storage\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/ui\/blocks\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"go-scrum-poker-bot\/web\/server\/models\" \t\"net\/http\" )  func InteractionCallback( \tuserStorage storage.UserStorage, \tsessionStorage storage.SessionStorage, \tuiBuilder *ui.Builder, \twebClient clients.Client, ) http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { \t\tvar callback models.Callback     \/\/ \u041e\u0431 \u044d\u0442\u043e\u043c \u043d\u0438\u0436\u0435 \t\tdata, err := callback.SerializedData([]byte(r.PostFormValue(\"payload\"))) \t\tif err != nil { \t\t\thttp.Error(w, err.Error(), http.StatusBadRequest) \t\t\treturn \t\t}      \/\/ TODO: \u0421\u043a\u043e\u0440\u043e \u0434\u043e\u0431\u0435\u0440\u0435\u043c\u0441\u044f \u0434\u043e \u043d\u0438\u0445 \t\tusers := userStorage.All(data.SessionID) \t\tvisible := sessionStorage.GetVisibility(data.SessionID)  \t\terr = nil     \/\/ \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043a\u0430\u043a\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a \u043d\u0430\u043c \u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b\u043e \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043b\u043e\u0433\u0438\u043a\u0438 \u0438\u0441\u0445\u043e\u0434\u044f \u0438\u0437 \u043d\u0435\u0433\u043e \t\tswitch data.Action.ActionID { \t\tcase ui.VOTE_ACTION_ID: \t\t\tusers[callback.User.Username] = data.Action.SelectedOption.Value \t\t\terr = userStorage.Save(data.SessionID, callback.User.Username, data.Action.SelectedOption.Value) \t\tcase ui.RESULTS_VISIBILITY_ACTION_ID: \t\t\tvisible = !visible \t\t\terr = sessionStorage.SetVisibility(data.SessionID, visible) \t\t} \t\tif err != nil { \t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError) \t\t\treturn \t\t}      \/\/ \u0428\u043b\u0435\u043c \u043e\u0442\u0432\u0435\u0442 \u043f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0447\u0435\u0440\u0435\u0437 response URL. \u0414\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432\u0441\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u0442 \u043d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u043e \t\tresp := webClient.Make(&amp;clients.Request{ \t\t\tURL:    callback.ResponseURL, \t\t\tMethod: \"POST\", \t\t\tJson: &amp;blocks.Interactive{ \t\t\t\tReplaceOriginal: true, \t\t\t\tBlocks:          uiBuilder.BuildBlocks(data.Subject, users, data.SessionID, visible), \t\t\t\tLinkNames:       true, \t\t\t}, \t\t}) \t\tif resp.Error != nil { \t\t\thttp.Error(w, resp.Error.Error(), http.StatusInternalServerError) \t\t\treturn \t\t} \t}) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041c\u044b \u043f\u043e\u043a\u0430 \u043d\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 \u043d\u0430\u0448\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0438\u0445 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0438 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442 \u043d\u0430 \u044d\u0442\u043e\u0442 \u0445\u044d\u043d\u0434\u043b\u0435\u0440. \u0418\u0434\u0435\u043c \u0432 <code>storage<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>storage.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package storage  type UserStorage interface { \tAll(sessionID string) map[string]string \tSave(sessionID string, username string, value string) error }  type SessionStorage interface { \tGetVisibility(sessionID string) bool \tSetVisibility(sessionID string, state bool) error } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0440\u0430\u0437\u0431\u0438\u043b \u043b\u043e\u0433\u0438\u043a\u0443 \u043d\u0430 \u0434\u0432\u0430 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0442\u0430\u043a \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0435\u0441\u043b\u0438 \u0431\u0443\u0434\u0435\u0442 \u043d\u0443\u0436\u043d\u043e, \u0442\u043e \u043b\u0435\u0433\u043a\u043e \u043c\u043e\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0433\u043e\u043b\u043e\u0441\u043e\u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445, \u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0441\u0441\u0438\u0438 \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432 Redis (\u043a\u0430\u043a \u043f\u0440\u0438\u043c\u0435\u0440).<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c <code>Callback<\/code>. \u0418\u0434\u0435\u043c \u0432 <code>web -&gt; server -&gt; models<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>callback.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package models  import ( \t\"encoding\/json\" \t\"errors\" \t\"go-scrum-poker-bot\/ui\" )  type User struct { \tUsername string `json:\"username\"` }  type Text struct { \tType string `json:\"type\"` \tText string `json:\"text\"` }  type Block struct { \tType    string `json:\"type\"` \tBlockID string `json:\"block_id\"` \tText    *Text  `json:\"text,omitempty\"` }  type Message struct { \tBlocks []*Block `json:\"blocks,omitempty\"` }  type SelectedOption struct { \tValue string `json:\"value\"` }  type Action struct { \tBlockID        string          `json:\"block_id\"` \tActionID       string          `json:\"action_id\"` \tValue          string          `json:\"value,omitempty\"` \tSelectedOption *SelectedOption `json:\"selected_option,omitempty\"` }  type SerializedData struct { \tSessionID string \tSubject   string \tAction    *Action }  type Callback struct { \tResponseURL string    `json:\"response_url\"` \tUser        *User     `json:\"user\"` \tActions     []*Action `json:\"actions\"` \tMessage     *Message  `json:\"message,omitempty\"` }  \/\/ \u0413\u0440\u044f\u0437\u043d\u043e \u0434\u043e\u0441\u0442\u0430\u0435\u043c ID \u0441\u0435\u0441\u0441\u0438\u0438, \u043d\u043e \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u044f \u043d\u0435 \u0441\u043c\u043e\u0433 \u043f\u0440\u0438\u0434\u0443\u043c\u0430\u0442\u044c func (c *Callback) getSessionID() (string, error) { \tfor _, action := range c.Actions { \t\tif action.BlockID != \"\" { \t\t\treturn action.BlockID, nil \t\t} \t}  \treturn \"\", errors.New(\"Invalid session ID\") }  \/\/ \u0422\u0435\u043a\u0441\u0442 \u0434\u043b\u044f \u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043d\u0438\u044f func (c *Callback) getSubject() (string, error) { \tfor _, block := range c.Message.Blocks { \t\tif block.BlockID == ui.SUBJECT_BLOCK_ID &amp;&amp; block.Text != nil { \t\t\treturn block.Text.Text, nil \t\t} \t}  \treturn \"\", errors.New(\"Invalid subject\") }  \/\/ \u041a\u0430\u043a\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043a \u043d\u0430\u043c \u043f\u0440\u0438\u0448\u043b\u043e func (c *Callback) getAction() (*Action, error) { \tfor _, action := range c.Actions { \t\tif action.ActionID == ui.VOTE_ACTION_ID || action.ActionID == ui.RESULTS_VISIBILITY_ACTION_ID { \t\t\treturn action, nil \t\t} \t}  \treturn nil, errors.New(\"Invalid action\") }  func (c *Callback) SerializedData(data []byte) (*SerializedData, error) { \terr := json.Unmarshal(data, c) \tif err != nil { \t\treturn nil, err \t}  \tsessionID, err := c.getSessionID() \tif err != nil { \t\treturn nil, err \t}  \tsubject, err := c.getSubject() \tif err != nil { \t\treturn nil, err \t}  \taction, err := c.getAction() \tif err != nil { \t\treturn nil, err \t}  \treturn &amp;SerializedData{ \t\tSessionID: sessionID, \t\tSubject:   subject, \t\tAction:    action, \t}, nil } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442 \u043d\u0430 \u043d\u0430\u0448 \u0445\u044d\u043d\u0434\u043b\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>interaction_callback_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers_test  import ( \t\"encoding\/json\" \t\"go-scrum-poker-bot\/config\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/server\/handlers\" \t\"go-scrum-poker-bot\/web\/server\/models\" \t\"net\/http\" \t\"net\/http\/httptest\" \t\"net\/url\" \t\"strings\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  func TestInteractionCallbackHandlerActions(t *testing.T) { \tconfig := config.NewConfig() \tmockClient := &amp;MockClient{} \tmockUserStorage := &amp;MockUserStorage{} \tmockSessionStorage := &amp;MockSessionStorage{} \tuiBuilder := ui.NewBuilder(config)  \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/interactivity\", \t\thandlers.InteractionCallback(mockUserStorage, mockSessionStorage, uiBuilder, mockClient), \t)  \tactions := []*models.Action{ \t\t{ \t\t\tBlockID:        \"test\", \t\t\tActionID:       ui.RESULTS_VISIBILITY_ACTION_ID, \t\t\tValue:          \"test\", \t\t\tSelectedOption: nil, \t\t}, \t\t{ \t\t\tBlockID:        \"test\", \t\t\tActionID:       ui.VOTE_ACTION_ID, \t\t\tValue:          \"test\", \t\t\tSelectedOption: &amp;models.SelectedOption{Value: \"1\"}, \t\t}, \t}    \/\/ \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u043d\u0430 \u0434\u0432\u0443\u0445 \u0440\u0430\u0437\u043d\u044b\u0445 \u0442\u0438\u043f\u0430\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \tfor _, action := range actions { \t\tresponseRec := httptest.NewRecorder()  \t\tdata, _ := json.Marshal(models.Callback{ \t\t\tResponseURL: \"test\", \t\t\tUser:        &amp;models.User{Username: \"test\"}, \t\t\tActions:     []*models.Action{action}, \t\t\tMessage: &amp;models.Message{ \t\t\t\tBlocks: []*models.Block{ \t\t\t\t\t{ \t\t\t\t\t\tType:    \"test\", \t\t\t\t\t\tBlockID: ui.SUBJECT_BLOCK_ID, \t\t\t\t\t\tText:    &amp;models.Text{Type: \"test\", Text: \"test\"}, \t\t\t\t\t}, \t\t\t\t}, \t\t\t}, \t\t}) \t\tpayload := url.Values{\"payload\": {string(data)}}.Encode() \t\trequest, err := http.NewRequest(\"POST\", \"\/interactivity\", strings.NewReader(payload)) \t\trequest.Header.Set(\"Content-Type\", \"application\/x-www-form-urlencoded\")  \t\trouter.ServeHTTP(responseRec, request)  \t\tassert.Nil(t, err) \t\tassert.Equal(t, http.StatusOK, responseRec.Code) \t\tassert.Empty(t, responseRec.Body.String()) \t\tassert.Equal(t, true, mockClient.Called) \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c <code>mock<\/code> \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449. \u041e\u0431\u043d\u043e\u0432\u0438\u043c \u0444\u0430\u0439\u043b <code>common_test.go<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>common_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">\/\/ \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043a\u043e\u0434  type MockUserStorage struct{}  func (s *MockUserStorage) All(sessionID string) map[string]string { \treturn map[string]string{\"user\": \"1\"} }  func (s *MockUserStorage) Save(sessionID string, username string, value string) error { \treturn nil }  type MockSessionStorage struct{}  func (s *MockSessionStorage) GetVisibility(sessionID string) bool { \treturn true }  func (s *MockSessionStorage) SetVisibility(sessionID string, state bool) error { \treturn nil } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0432 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440 \u043d\u043e\u0432\u044b\u0439 \u0445\u044d\u043d\u0434\u043b\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">\/\/ \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043a\u043e\u0434  func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t) \trouter.Handle( \t\t\"\/play-poker\", \t\thandlers.PlayPokerCommand(s.webClient, s.uiBuilder), \t) \trouter.Handle( \t\t\"\/interactivity\", \t\thandlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient), \t)  \treturn router }  \/\/ \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043a\u043e\u0434<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0412\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u043d\u043e \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0438\u043a\u0430\u043a \u043d\u0435 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u044f\u0435\u0442 \u043d\u0430\u0441 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u043a \u043d\u0435\u043c\u0443 \u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b \u0437\u0430\u043f\u0440\u043e\u0441 + \u0435\u0441\u043b\u0438 \u043c\u044b \u0433\u0434\u0435-\u0442\u043e \u043f\u043e\u0439\u043c\u0430\u0435\u043c \u043f\u0430\u043d\u0438\u043a\u0443, \u0442\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0443\u043f\u0430\u0441\u0442\u044c. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u043c \u0447\u0435\u0440\u0435\u0437 <code>middleware<\/code>. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043f\u0430\u043f\u043a\u0443 <code>web -&gt; server -&gt; middleware<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>log.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package middleware  import ( \t\"log\" \t\"net\/http\" )  func Log(logger *log.Logger) func(http.Handler) http.Handler { \treturn func(next http.Handler) http.Handler { \t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { \t\t\tdefer func() { \t\t\t\tlogger.Printf( \t\t\t\t\t\"Handle request: [%s]: %s - %s - %s\", \t\t\t\t\tr.Method, r.URL.Path, r.RemoteAddr, r.UserAgent(), \t\t\t\t) \t\t\t}() \t\t\tnext.ServeHTTP(w, r) \t\t}) \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0434\u043b\u044f \u043d\u0435\u0435 \u0442\u0435\u0441\u0442:<\/p>\n<details class=\"spoiler\">\n<summary>log_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package middleware_test  import ( \t\"bytes\" \t\"go-scrum-poker-bot\/web\/server\/middleware\" \t\"log\" \t\"net\/http\" \t\"net\/http\/httptest\" \t\"os\" \t\"strings\" \t\"testing\"  \t\"github.com\/stretchr\/testify\/assert\" )  type logHandler struct{}  func (h *logHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}  func TestLogMiddleware(t *testing.T) { \tvar buf bytes.Buffer \tlogger := log.New(os.Stdout, \"INFO: \", log.LstdFlags)   \/\/ \u0412\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0434\u043b\u044f \u043b\u043e\u0433\u0433\u0435\u0440\u0430 output \u043d\u0430\u0448 \u0431\u0443\u0444\u0444\u0435\u0440, \u0447\u0442\u043e\u0431\u044b \u0432\u0441\u0435 \u043f\u0438\u0441\u0430\u043b\u043e\u0441\u044c \u0432 \u043d\u0435\u0433\u043e \tlogger.SetOutput(&amp;buf)  \thandler := &amp;logHandler{}   \/\/ \u0411\u0435\u0440\u0435\u043c mock recorder \u0438\u0437 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 Go \tresponseRec := httptest.NewRecorder()  \trouter := http.NewServeMux() \trouter.Handle(\"\/test\", middleware.Log(logger)(handler))  \trequest, err := http.NewRequest(\"GET\", \"\/test\", strings.NewReader(\"\"))  \trouter.ServeHTTP(responseRec, request)  \tassert.Nil(t, err) \tassert.Equal(t, http.StatusOK, responseRec.Code)   \/\/ \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c, \u0447\u0442\u043e \u0432 \u0431\u0443\u0444\u0444\u0435\u0440 \u0447\u0442\u043e-\u0442\u043e \u043f\u0440\u0438\u0448\u043b\u043e. \u042d\u0442\u043e\u0433\u043e \u043d\u0430\u043c \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c, \u0447\u0442\u043e middleware \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \tassert.NotEmpty(t, buf.String()) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 <code>middleware<\/code> \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\/tree\/master\/web\/server\/middleware\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>.<\/p>\n<p>\u041d\u0443 \u0438 \u043d\u0430\u043a\u043e\u043d\u0435\u0446 \u0441\u043b\u043e\u0439 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445. \u042f \u0440\u0435\u0448\u0438\u043b \u0432\u0437\u044f\u0442\u044c Redis, \u0442\u0430\u043a \u043a\u0430\u043a \u044d\u0442\u043e \u043f\u0440\u043e\u0449\u0435, \u0434\u0430 \u0438 \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0434\u043b\u044f \u0442\u0430\u043a\u043e\u0433\u043e \u0440\u043e\u0434\u0430 \u0437\u0430\u0434\u0430\u0447 \u0447\u0442\u043e-\u0442\u043e \u0431\u043e\u043b\u044c\u0448\u0435\u0435, \u043a\u0430\u043a \u043c\u043d\u0435 \u043a\u0430\u0436\u0435\u0442\u0441\u044f. \u0412\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 <a href=\"https:\/\/github.com\/go-redis\/redis\" rel=\"noopener noreferrer nofollow\">go-redis<\/a> \u0438 \u0442\u0430\u043c \u0436\u0435 \u0432\u043e\u0437\u044c\u043c\u0435\u043c <a href=\"https:\/\/github.com\/go-redis\/redismock\" rel=\"noopener noreferrer nofollow\">redismock<\/a> \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0430\u0443\u0447\u0438\u043c\u0441\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0432\u0441\u0435\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u043e\u0439 Scrum Poker \u0441\u0435\u0441\u0441\u0438\u0438. \u0418\u0434\u0435\u043c \u0432 <code>storage<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>users.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package storage  import ( \t\"context\" \t\"fmt\"  \t\"github.com\/go-redis\/redis\/v8\" )  \/\/ \u0428\u0430\u0431\u043b\u043e\u043d\u044b \u043a\u043b\u044e\u0447\u0435\u0439 const SESSION_USERS_TPL = \"SESSION:%s:USERS\" const USER_VOTE_TPL = \"SESSION:%s:USERNAME:%s:VOTE\"  type UserRedisStorage struct { \tredis   *redis.Client \tcontext context.Context }  func NewUserRedisStorage(redisClient *redis.Client) *UserRedisStorage { \treturn &amp;UserRedisStorage{ \t\tredis:   redisClient, \t\tcontext: context.Background(), \t} }  func (s *UserRedisStorage) All(sessionID string) map[string]string { \tusers := make(map[string]string)    \/\/ \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0431\u0443\u0434\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432 set, \u0442\u0430\u043a \u043a\u0430\u043a \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0430 \u0434\u043b\u044f \u043d\u0430\u0441 \u043d\u0435 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u0430.    \/\/ \u0417\u0430\u043e\u0434\u043d\u043e \u0438\u0437\u0431\u0430\u0432\u0438\u043c\u0441\u044f \u043e\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043a\u0430\u0442\u044c \u0434\u0443\u0431\u043b\u0438\u043a\u0430\u0442\u044b \tfor _, username := range s.redis.SMembers(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID)).Val() { \t\tusers[username] = s.redis.Get(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username)).Val() \t} \treturn users }  func (s *UserRedisStorage) Save(sessionID string, username string, value string) error { \terr := s.redis.SAdd(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID), username).Err() \tif err != nil { \t\treturn err \t}    \/\/ \u0413\u043e\u043b\u043e\u0441\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0431\u0443\u0434\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432 \u043e\u0431\u044b\u0447\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0430\u0445.    \/\/ \u042f \u0441\u0434\u0435\u043b\u0430\u043b \u0432\u0435\u0447\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435, \u043d\u043e \u044d\u0442\u043e \u043b\u0435\u0433\u043a\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c, \u0438\u0437\u043c\u0435\u043d\u0438\u0432 -1 \u043d\u0430 \u043d\u0443\u0436\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \terr = s.redis.Set(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username), value, -1).Err() \tif err != nil { \t\treturn err \t}  \treturn nil } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>users_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package storage_test  import ( \t\"errors\" \t\"fmt\" \t\"go-scrum-poker-bot\/storage\" \t\"testing\"  \t\"github.com\/go-redis\/redismock\/v8\" \t\"github.com\/stretchr\/testify\/assert\" )  func TestAll(t *testing.T) { \tsessionID, username, value := \"test\", \"user\", \"1\"  \tredisClient, mock := redismock.NewClientMock() \tusersStorage := storage.NewUserRedisStorage(redisClient)    \/\/ Redis mock \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u043a\u043e\u043c\u0430\u043d\u0434 \u0438 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0438\u0445 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \tmock.ExpectSMembers( \t\tfmt.Sprintf(storage.SESSION_USERS_TPL, sessionID), \t).SetVal([]string{username}) \tmock.ExpectGet( \t\tfmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username), \t).SetVal(value)  \tassert.Equal(t, map[string]string{username: value}, usersStorage.All(sessionID)) }  func TestSave(t *testing.T) { \tsessionID, username, value := \"test\", \"user\", \"1\"  \tredisClient, mock := redismock.NewClientMock() \tusersStorage := storage.NewUserRedisStorage(redisClient)  \tmock.ExpectSAdd( \t\tfmt.Sprintf(storage.SESSION_USERS_TPL, sessionID), \t\tusername, \t).SetVal(1) \tmock.ExpectSet( \t\tfmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username), \t\tvalue, \t\t-1, \t).SetVal(value)  \tassert.Equal(t, nil, usersStorage.Save(sessionID, username, value)) }  func TestSaveSAddErr(t *testing.T) { \tsessionID, username, value, err := \"test\", \"user\", \"1\", errors.New(\"ERROR\")  \tredisClient, mock := redismock.NewClientMock() \tusersStorage := storage.NewUserRedisStorage(redisClient)  \tmock.ExpectSAdd( \t\tfmt.Sprintf(storage.SESSION_USERS_TPL, sessionID), \t\tusername, \t).SetErr(err)  \tassert.Equal(t, err, usersStorage.Save(sessionID, username, value)) }  func TestSaveSetErr(t *testing.T) { \tsessionID, username, value, err := \"test\", \"user\", \"1\", errors.New(\"ERROR\")  \tredisClient, mock := redismock.NewClientMock() \tusersStorage := storage.NewUserRedisStorage(redisClient)  \tmock.ExpectSAdd( \t\tfmt.Sprintf(storage.SESSION_USERS_TPL, sessionID), \t\tusername, \t).SetVal(1) \tmock.ExpectSet( \t\tfmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username), \t\tvalue, \t\t-1, \t).SetErr(err)  \tassert.Equal(t, err, usersStorage.Save(sessionID, username, value)) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0434\u043b\u044f &#171;\u043f\u043e\u043a\u0435\u0440\u043d\u043e\u0439&#187; \u0441\u0435\u0441\u0441\u0438\u0438. \u041f\u043e\u043a\u0430 \u0442\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043b\u0435\u0436\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0433\u043e\u043b\u043e\u0441\u043e\u0432:<\/p>\n<details class=\"spoiler\">\n<summary>sessions.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package storage  import ( \t\"context\" \t\"fmt\" \t\"strconv\"  \t\"github.com\/go-redis\/redis\/v8\" )  \/\/ \u0428\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043a\u043b\u044e\u0447\u0435\u0439 const SESSION_VOTES_HIDDEN_TPL = \"SESSION:%s:VOTES_HIDDEN\"  type SessionRedisStorage struct { \tredis   *redis.Client \tcontext context.Context }  func NewSessionRedisStorage(redisClient *redis.Client) *SessionRedisStorage { \treturn &amp;SessionRedisStorage{ \t\tredis:   redisClient, \t\tcontext: context.Background(), \t} }  func (s *SessionRedisStorage) GetVisibility(sessionID string) bool { \tvalue, _ := strconv.ParseBool( \t\ts.redis.Get(s.context, fmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID)).Val(), \t)  \treturn value }  func (s *SessionRedisStorage) SetVisibility(sessionID string, state bool) error { \treturn s.redis.Set( \t\ts.context, \t\tfmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID), \t\tstrconv.FormatBool(state), \t\t-1, \t).Err() } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418 \u0441\u0440\u0430\u0437\u0443 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0434\u043b\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432:<\/p>\n<details class=\"spoiler\">\n<summary>sessions_test.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package storage_test  import ( \t\"errors\" \t\"fmt\" \t\"go-scrum-poker-bot\/storage\" \t\"strconv\" \t\"testing\"  \t\"github.com\/go-redis\/redismock\/v8\" \t\"github.com\/stretchr\/testify\/assert\" )  func TestGetVisibility(t *testing.T) { \tsessionID, state := \"test\", true  \tredisClient, mock := redismock.NewClientMock()  \tmock.ExpectGet( \t\tfmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID), \t).SetVal(strconv.FormatBool(state))  \tsessionStorage := storage.NewSessionRedisStorage(redisClient)  \tassert.Equal(t, state, sessionStorage.GetVisibility(sessionID)) }  func TestSetVisibility(t *testing.T) { \tsessionID, state := \"test\", true  \tredisClient, mock := redismock.NewClientMock()  \tmock.ExpectSet( \t\tfmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID), \t\tstrconv.FormatBool(state), \t\t-1, \t).SetVal(\"1\")  \tsessionStorage := storage.NewSessionRedisStorage(redisClient)  \tassert.Equal(t, nil, sessionStorage.SetVisibility(sessionID, state)) }  func TestSetVisibilityErr(t *testing.T) { \tsessionID, state, err := \"test\", true, errors.New(\"ERROR\")  \tredisClient, mock := redismock.NewClientMock()  \tmock.ExpectSet( \t\tfmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID), \t\tstrconv.FormatBool(state), \t\t-1, \t).SetErr(err)  \tsessionStorage := storage.NewSessionRedisStorage(redisClient)  \tassert.Equal(t, err, sessionStorage.SetVisibility(sessionID, state)) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0442\u043b\u0438\u0447\u043d\u043e! \u041e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c main.go \u0438 server.go:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package server  import ( \t\"context\" \t\"go-scrum-poker-bot\/storage\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/clients\" \t\"go-scrum-poker-bot\/web\/server\/handlers\" \t\"log\" \t\"net\/http\" \t\"os\" \t\"os\/signal\" \t\"sync\/atomic\" \t\"time\" )  \/\/ \u041d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f middleware type Middleware func(next http.Handler) http.Handler  \/\/ \u0412\u0441\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0437\u0434\u0435\u0441\u044c type Server struct { \thealthy        int32 \tmiddleware     []Middleware \tlogger         *log.Logger \twebClient      clients.Client \tuiBuilder      *ui.Builder \tuserStorage    storage.UserStorage \tsessionStorage storage.SessionStorage }  \/\/ \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0438\u0445 \u043f\u0440\u0438 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 func NewServer( \tlogger *log.Logger, \twebClient clients.Client, \tuiBuilder *ui.Builder, \tuserStorage storage.UserStorage, \tsessionStorage storage.SessionStorage, \tmiddleware []Middleware, ) *Server { \treturn &amp;Server{ \t\tlogger:         logger, \t\twebClient:      webClient, \t\tuiBuilder:      uiBuilder, \t\tuserStorage:    userStorage, \t\tsessionStorage: sessionStorage, \t\tmiddleware:     middleware, \t} }  func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t) \trouter.Handle( \t\t\"\/play-poker\", \t\thandlers.PlayPokerCommand(s.webClient, s.uiBuilder), \t) \trouter.Handle( \t\t\"\/interactivity\", \t\thandlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient), \t)  \treturn router }  func (s *Server) setupMiddleware(router http.Handler) http.Handler { \thandler := router \tfor _, middleware := range s.middleware { \t\thandler = middleware(handler) \t}  \treturn handler }  func (s *Server) Serve(address string) { \tserver := &amp;http.Server{ \t\tAddr:         address, \t\tHandler:      s.setupMiddleware(s.setupRouter()), \t\tErrorLog:     s.logger, \t\tReadTimeout:  5 * time.Second, \t\tWriteTimeout: 10 * time.Second, \t\tIdleTimeout:  15 * time.Second, \t}  \tdone := make(chan bool) \tquit := make(chan os.Signal, 1) \tsignal.Notify(quit, os.Interrupt)  \tgo func() { \t\t&lt;-quit \t\ts.logger.Println(\"Server is shutting down...\") \t\tatomic.StoreInt32(&amp;s.healthy, 0)  \t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) \t\tdefer cancel()  \t\tserver.SetKeepAlivesEnabled(false) \t\tif err := server.Shutdown(ctx); err != nil { \t\t\ts.logger.Fatalf(\"Could not gracefully shutdown the server: %v\\n\", err) \t\t} \t\tclose(done) \t}()  \ts.logger.Println(\"Server is ready to handle requests at\", address) \tatomic.StoreInt32(&amp;s.healthy, 1) \tif err := server.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed { \t\ts.logger.Fatalf(\"Could not listen on %s: %v\\n\", address, err) \t}  \t&lt;-done \ts.logger.Println(\"Server stopped\") } <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>main.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package main  import ( \t\"fmt\" \t\"go-scrum-poker-bot\/config\" \t\"go-scrum-poker-bot\/storage\" \t\"go-scrum-poker-bot\/ui\" \t\"go-scrum-poker-bot\/web\/clients\" \tclients_middleware \"go-scrum-poker-bot\/web\/clients\/middleware\" \t\"go-scrum-poker-bot\/web\/server\" \tserver_middleware \"go-scrum-poker-bot\/web\/server\/middleware\" \t\"log\" \t\"net\/http\" \t\"os\" \t\"time\"  \t\"github.com\/go-redis\/redis\/v8\" )  func main() { \tlogger := log.New(os.Stdout, \"INFO: \", log.LstdFlags) \tconfig := config.NewConfig()   \/\/ \u041e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u043c Redis \u043a\u043b\u0438\u0435\u043d\u0442 \tredisCLI := redis.NewClient(&amp;redis.Options{ \t\tAddr: fmt.Sprintf(\"%s:%d\", config.Redis.Host, config.Redis.Port), \t\tDB:   config.Redis.DB, \t})   \/\/ \u041d\u0430\u0448 users storage \tuserStorage := storage.NewUserRedisStorage(redisCLI)   \/\/ \u041d\u0430\u0448 sessions storage \tsessionStorage := storage.NewSessionRedisStorage(redisCLI) \tbuilder := ui.NewBuilder(config) \twebClient := clients.NewBasicClient( \t\t&amp;http.Client{ \t\t\tTimeout: 5 * time.Second, \t\t}, \t\t[]clients.Middleware{ \t\t\tclients_middleware.Auth(config.Slack.Token), \t\t\tclients_middleware.JsonContentType, \t\t\tclients_middleware.Log(logger), \t\t}, \t)    \/\/ \u0412 Server \u0442\u0435\u043f\u0435\u0440\u044c \u0435\u0441\u0442\u044c middleware \tapp := server.NewServer( \t\tlogger, \t\twebClient, \t\tbuilder, \t\tuserStorage, \t\tsessionStorage, \t\t[]server.Middleware{server_middleware.Recover(logger), server_middleware.Log(logger), server_middleware.Json}, \t) \tapp.Serve(config.App.ServerAddress) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u043c \u0442\u0435\u0441\u0442\u044b:<\/p>\n<pre><code class=\"bash\">go test .\/... -race -coverpkg=.\/... -coverprofile=coverage.txt -covermode=atomic<\/code><\/pre>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442:<\/p>\n<details class=\"spoiler\">\n<summary>go tool cover -func coverage.txt<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ go tool cover -func coverage.txt  go-scrum-poker-bot\/config\/config.go:9:                                  NewConfig               100.0% go-scrum-poker-bot\/config\/helpers.go:10:                                getStrEnv               100.0% go-scrum-poker-bot\/config\/helpers.go:17:                                getIntEnv               100.0% go-scrum-poker-bot\/config\/helpers.go:26:                                getListStrEnv           100.0% go-scrum-poker-bot\/main.go:22:                                          main                    0.0% go-scrum-poker-bot\/storage\/sessions.go:18:                              NewSessionRedisStorage  100.0% go-scrum-poker-bot\/storage\/sessions.go:25:                              GetVisibility           100.0% go-scrum-poker-bot\/storage\/sessions.go:33:                              SetVisibility           100.0% go-scrum-poker-bot\/storage\/users.go:18:                                 NewUserRedisStorage     100.0% go-scrum-poker-bot\/storage\/users.go:25:                                 All                     100.0% go-scrum-poker-bot\/storage\/users.go:34:                                 Save                    100.0% go-scrum-poker-bot\/ui\/blocks\/action.go:9:                               BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/button.go:11:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/context.go:9:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/section.go:9:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/select.go:10:                              BlockType               100.0% go-scrum-poker-bot\/ui\/builder.go:14:                                    NewBuilder              100.0% go-scrum-poker-bot\/ui\/builder.go:18:                                    getGetResultsText       100.0% go-scrum-poker-bot\/ui\/builder.go:26:                                    getResults              100.0% go-scrum-poker-bot\/ui\/builder.go:41:                                    getOptions              100.0% go-scrum-poker-bot\/ui\/builder.go:50:                                    BuildBlocks             100.0% go-scrum-poker-bot\/ui\/builder.go:100:                                   Build                   100.0% go-scrum-poker-bot\/web\/clients\/client.go:22:                            NewBasicClient          100.0% go-scrum-poker-bot\/web\/clients\/client.go:26:                            makeRequest             78.9% go-scrum-poker-bot\/web\/clients\/client.go:65:                            Make                    66.7% go-scrum-poker-bot\/web\/clients\/middleware\/auth.go:8:                    Auth                    100.0% go-scrum-poker-bot\/web\/clients\/middleware\/json.go:5:                    JsonContentType         100.0% go-scrum-poker-bot\/web\/clients\/middleware\/log.go:8:                     Log                     87.5% go-scrum-poker-bot\/web\/clients\/request.go:12:                           ToBytes                 100.0% go-scrum-poker-bot\/web\/clients\/response.go:12:                          Json                    100.0% go-scrum-poker-bot\/web\/server\/handlers\/healthcheck.go:10:               Healthcheck             66.7% go-scrum-poker-bot\/web\/server\/handlers\/interaction_callback.go:12:      InteractionCallback     71.4% go-scrum-poker-bot\/web\/server\/handlers\/play_poker.go:13:                PlayPokerCommand        100.0% go-scrum-poker-bot\/web\/server\/middleware\/json.go:5:                     Json                    100.0% go-scrum-poker-bot\/web\/server\/middleware\/log.go:8:                      Log                     100.0% go-scrum-poker-bot\/web\/server\/middleware\/recover.go:9:                  Recover                 100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:52:                    getSessionID            100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:62:                    getSubject              100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:72:                    getAction               100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:82:                    SerializedData          92.3% go-scrum-poker-bot\/web\/server\/models\/errors.go:13:                      ResponseError           75.0% go-scrum-poker-bot\/web\/server\/server.go:31:                             NewServer               0.0% go-scrum-poker-bot\/web\/server\/server.go:49:                             setupRouter             0.0% go-scrum-poker-bot\/web\/server\/server.go:67:                             setupMiddleware         0.0% go-scrum-poker-bot\/web\/server\/server.go:76:                             Serve                   0.0% total:                                                                  (statements)            75.1%<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041d\u0435\u043f\u043b\u043e\u0445\u043e, \u043d\u043e \u043d\u0430\u043c \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c \u0432 coverage <code>main.go <\/code>(\u043c\u043e\u0435 \u043c\u043d\u0435\u043d\u0438\u0435) \u0438 <code>server.go<\/code> (\u0437\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043f\u043e\u0440\u0438\u0442\u044c), \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0435\u0441\u0442\u044c \u0445\u0430\u043a :). \u041d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 \u043d\u0430\u0447\u0430\u043b\u043e \u0444\u0430\u0439\u043b\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u043e\u0446\u0435\u043d\u043a\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0440\u043e\u0447\u043a\u0443 \u0441 \u0442\u0435\u0433\u0430\u043c\u0438:<\/p>\n<pre><code class=\"go\">\/\/+build !test<\/code><\/pre>\n<p>\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043c \u0441 \u0442\u0435\u0433\u043e\u043c:<\/p>\n<pre><code class=\"bash\">go test .\/... -race -coverpkg=.\/... -coverprofile=coverage.txt -covermode=atomic -tags=test<\/code><\/pre>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442:<\/p>\n<details class=\"spoiler\">\n<summary>go tool cover -func coverage.txt<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ go tool cover -func coverage.txt  go-scrum-poker-bot\/config\/config.go:9:                                  NewConfig               100.0% go-scrum-poker-bot\/config\/helpers.go:10:                                getStrEnv               100.0% go-scrum-poker-bot\/config\/helpers.go:17:                                getIntEnv               100.0% go-scrum-poker-bot\/config\/helpers.go:26:                                getListStrEnv           100.0% go-scrum-poker-bot\/storage\/sessions.go:18:                              NewSessionRedisStorage  100.0% go-scrum-poker-bot\/storage\/sessions.go:25:                              GetVisibility           100.0% go-scrum-poker-bot\/storage\/sessions.go:33:                              SetVisibility           100.0% go-scrum-poker-bot\/storage\/users.go:18:                                 NewUserRedisStorage     100.0% go-scrum-poker-bot\/storage\/users.go:25:                                 All                     100.0% go-scrum-poker-bot\/storage\/users.go:34:                                 Save                    100.0% go-scrum-poker-bot\/ui\/blocks\/action.go:9:                               BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/button.go:11:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/context.go:9:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/section.go:9:                              BlockType               100.0% go-scrum-poker-bot\/ui\/blocks\/select.go:10:                              BlockType               100.0% go-scrum-poker-bot\/ui\/builder.go:14:                                    NewBuilder              100.0% go-scrum-poker-bot\/ui\/builder.go:18:                                    getGetResultsText       100.0% go-scrum-poker-bot\/ui\/builder.go:26:                                    getResults              100.0% go-scrum-poker-bot\/ui\/builder.go:41:                                    getOptions              100.0% go-scrum-poker-bot\/ui\/builder.go:50:                                    BuildBlocks             100.0% go-scrum-poker-bot\/ui\/builder.go:100:                                   Build                   100.0% go-scrum-poker-bot\/web\/clients\/client.go:22:                            NewBasicClient          100.0% go-scrum-poker-bot\/web\/clients\/client.go:26:                            makeRequest             78.9% go-scrum-poker-bot\/web\/clients\/client.go:65:                            Make                    66.7% go-scrum-poker-bot\/web\/clients\/middleware\/auth.go:8:                    Auth                    100.0% go-scrum-poker-bot\/web\/clients\/middleware\/json.go:5:                    JsonContentType         100.0% go-scrum-poker-bot\/web\/clients\/middleware\/log.go:8:                     Log                     87.5% go-scrum-poker-bot\/web\/clients\/request.go:12:                           ToBytes                 100.0% go-scrum-poker-bot\/web\/clients\/response.go:12:                          Json                    100.0% go-scrum-poker-bot\/web\/server\/handlers\/healthcheck.go:10:               Healthcheck             66.7% go-scrum-poker-bot\/web\/server\/handlers\/interaction_callback.go:12:      InteractionCallback     71.4% go-scrum-poker-bot\/web\/server\/handlers\/play_poker.go:13:                PlayPokerCommand        100.0% go-scrum-poker-bot\/web\/server\/middleware\/json.go:5:                     Json                    100.0% go-scrum-poker-bot\/web\/server\/middleware\/log.go:8:                      Log                     100.0% go-scrum-poker-bot\/web\/server\/middleware\/recover.go:9:                  Recover                 100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:52:                    getSessionID            100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:62:                    getSubject              100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:72:                    getAction               100.0% go-scrum-poker-bot\/web\/server\/models\/callback.go:82:                    SerializedData          92.3% go-scrum-poker-bot\/web\/server\/models\/errors.go:13:                      ResponseError           75.0% total:                                                                  (statements)            90.9%<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0430\u043a\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043c\u043d\u0435 \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \ud83d\ude42 <\/p>\n<p>\u041d\u0430 \u044d\u0442\u043e\u043c \u043f\u043e\u0436\u0430\u043b\u0443\u0439 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0441\u044c. \u0412\u0435\u0441\u044c \u043a\u043e\u0434 \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>. \u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435!<\/p>\n<\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/545304\/\"> https:\/\/habr.com\/ru\/post\/545304\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text_v2\" id=\"post-content-body\">\n<p>\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u043c\u044b \u043d\u0430\u043f\u0438\u0448\u0435\u043c Slack \u0431\u043e\u0442\u0430 \u0434\u043b\u044f <a href=\"https:\/\/en.wikipedia.org\/wiki\/Planning_poker\" rel=\"noopener noreferrer nofollow\">Scrum \u043f\u043e\u043a\u0435\u0440\u0430<\/a> \u043d\u0430 \u044f\u0437\u044b\u043a\u0435 Go. \u041f\u0438\u0441\u0430\u0442\u044c \u0431\u0443\u0434\u0435\u043c \u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0431\u0435\u0437 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u0432 \u0438 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0430\u0448\u0430 \u0446\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c\u0441\u044f \u0441 \u044f\u0437\u044b\u043a\u043e\u043c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Go \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u043d\u0430\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e\u0442 \u044f\u0437\u044b\u043a \u0443\u0434\u043e\u0431\u0435\u043d \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432. <\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<h2>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440<\/h2>\n<p><em>\u042f \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0437\u043d\u0430\u044e Go \u0438 \u043c\u043d\u043e\u0433\u0438\u0445 \u0432\u0435\u0449\u0435\u0439 \u0435\u0449\u0435 \u043d\u0435 \u0437\u043d\u0430\u044e. \u041c\u043e\u0439 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u044f\u0437\u044b\u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 Python. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0447\u0430\u0441\u0442\u043e \u0431\u0443\u0434\u0443 \u043e\u0442\u0441\u044b\u043b\u0430\u0442\u044c \u043a \u043d\u0435\u043c\u0443 \u0432 \u0442\u0435\u0445 \u043c\u0435\u0441\u0442\u0430\u0445, \u0433\u0434\u0435 \u043f\u043e \u043c\u043e\u0435\u043c\u0443 \u043c\u043d\u0435\u043d\u0438\u044e \u0432 Python \u0447\u0442\u043e-\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u043d\u043e \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0438\u043b\u0438 \u043f\u0440\u043e\u0449\u0435. \u0426\u0435\u043b\u044c \u044d\u0442\u0438\u0445 \u043e\u0442\u0441\u044b\u043b\u043e\u043a \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0440\u043e\u0434\u0438\u0442\u044c \u0434\u0438\u0441\u043a\u0443\u0441\u0441\u0438\u044e, \u0432\u0435\u0434\u044c \u0432\u043f\u043e\u043b\u043d\u0435 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u0447\u0442\u043e \u044d\u0442\u0438 &#171;\u0443\u0434\u043e\u0431\u043d\u044b\u0435 \u0432\u0435\u0449\u0438&#187; \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0432 Go, \u043f\u0440\u043e\u0441\u0442\u043e \u044f \u0438\u0445 \u043d\u0435 \u043d\u0430\u0448\u0435\u043b. <\/em><\/p>\n<p><em>\u0422\u0430\u043a\u0436\u0435, \u043e\u0442\u043c\u0435\u0447\u0443, \u0447\u0442\u043e \u0432\u0441\u0435 \u0447\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0438\u0441\u0430\u043d\u043e \u043d\u0438\u0436\u0435, \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0433\u043e\u0440\u0430\u0437\u0434\u043e \u043f\u0440\u043e\u0449\u0435 (\u0431\u0435\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u043b\u043e\u0438 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435), \u043d\u043e \u043c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0441 \u0446\u0435\u043b\u044c\u044e \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u044f \u0438 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0432 &#171;\u0447\u0438\u0441\u0442\u043e\u0439&#187; \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435. \u0414\u0430 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a \u043f\u0440\u043e\u0449\u0435.<\/em><\/p>\n<p><em>\u0425\u0432\u0430\u0442\u0438\u0442 \u043f\u0440\u0435\u043b\u044e\u0434\u0438\u0439, \u0432\u043f\u0435\u0440\u0435\u0434 \u0432 \u0431\u043e\u0439!<\/em><\/p>\n<h2>\u0418\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442<\/h2>\n<details class=\"spoiler\">\n<summary>\u0410\u043d\u0438\u043c\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0431\u0443\u0434\u0443\u0449\u0435\u0433\u043e \u0431\u043e\u0442\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0442\u0435\u0445, \u043a\u043e\u043c\u0443 \u0447\u0438\u0442\u0430\u0442\u044c \u043a\u043e\u0434 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0435\u0439, \u0447\u0435\u043c \u0441\u0442\u0430\u0442\u044c\u044e \u2014 \u043f\u0440\u043e\u0448\u0443 <a href=\"https:\/\/github.com\/isayakhov\/go-scrum-poker-bot\" rel=\"noopener noreferrer nofollow\">\u0441\u044e\u0434\u0430<\/a>.<\/p>\n<h2>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<p>\u0420\u0430\u0437\u043e\u0431\u044c\u0435\u043c \u043d\u0430\u0448\u0443 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0441\u043b\u043e\u0438. \u0423 \u043d\u0430\u0441 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f \u0441\u043b\u043e\u0439 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (<em>web<\/em>), \u0441\u043b\u043e\u0439 \u0434\u043b\u044f \u0440\u0438\u0441\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 <a href=\"https:\/\/api.slack.com\/block-kit\" rel=\"noopener noreferrer nofollow\">Slack UI Block Kit <\/a>(<em>ui<\/em>), \u0441\u043b\u043e\u0439 \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 (<em>storage<\/em>), \u0430 \u0442\u0430\u043a\u0436\u0435 \u043c\u0435\u0441\u0442\u043e \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a (<em>config<\/em>). \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u0430\u043f\u043a\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435:<\/p>\n<pre><code class=\"go\">config\/ storage\/ ui\/ web\/ -- clients\/ -- server\/ main.go<\/code><\/pre>\n<h2>\u0421\u0435\u0440\u0432\u0435\u0440<\/h2>\n<p>\u0414\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0438\u0437 \u043f\u0430\u043a\u0435\u0442\u0430 <code>http<\/code>. \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 <code>Server<\/code> \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u0432\u0438\u0434\u0430 \u0432 <code>web -&gt; server<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package server  import ( \t\"context\" \t\"log\" \t\"net\/http\" \t\"os\" \t\"os\/signal\" \t\"sync\/atomic\" \t\"time\" )  type Server struct {   \/\/ \u0417\u0434\u0435\u0441\u044c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u043d\u0430\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0438\u0445 \u043d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 main.go \thealthy        int32 \tlogger         *log.Logger }  func NewServer(logger *log.Logger) *Server { \treturn &amp;Server{ \t\tlogger: logger, \t} } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432. \u0415\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u0432 \u0434\u043b\u044f \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u0430\u043c\u0438 \u0438 \u0438\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043c\u043e\u0436\u043d\u043e \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0442\u044c \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0432\u0441\u0435 \u0432 <code>main.go<\/code>, \u0442\u0430\u043c \u0436\u0435 \u0433\u0434\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u044b \u043d\u0430\u0448\u0438\u0445 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u0432. \u041d\u043e \u044d\u0442\u043e \u043f\u043b\u043e\u0445\u043e\u0439 \u043f\u0443\u0442\u044c. \u0415\u0449\u0435 \u0435\u0441\u0442\u044c \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438 \u043f\u0440\u043e\u0441\u0442\u043e \u0438\u0445 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c. \u041d\u043e \u0432 \u0442\u0430\u043a\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0441\u043b\u043e\u0436\u043d\u043e \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442 \u0442\u0435\u0441\u0442\u0430\u043c\u0438. \u0414\u0430\u043b\u044c\u0448\u0435 \u043c\u044b \u0443\u0432\u0438\u0434\u0438\u043c \u043f\u043b\u044e\u0441\u044b \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u043d\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434\u0430. \u0418\u0442\u0430\u043a, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440. \u041d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">func (s *Server) setupRouter() http.Handler {  \/\/ TODO \trouter := http.NewServeMux()   return router }  func (s *Server) Serve(address string) { \tserver := &amp;http.Server{ \t\tAddr:         address,     Handler:      s.setupRouter(), \t\tErrorLog:     s.logger, \/\/ \u041d\u0430\u0448 \u043b\u043e\u0433\u0433\u0435\u0440 \t\tReadTimeout:  5 * time.Second, \t\tWriteTimeout: 10 * time.Second, \t\tIdleTimeout:  15 * time.Second, \t}    \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043a\u0430\u043d\u0430\u043b\u044b \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \tdone := make(chan bool) \tquit := make(chan os.Signal, 1)   \/\/ \u041d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c \u0441\u0438\u0433\u043d\u0430\u043b \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \tsignal.Notify(quit, os.Interrupt)  \tgo func() { \t\t&lt;-quit \t\ts.logger.Println(\"Server is shutting down...\")     \/\/ \u042d\u0442\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0434\u043b\u044f healthcheck'\u0430 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \t\tatomic.StoreInt32(&amp;s.healthy, 0)      \/\/ \u0414\u0430\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u0434\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u0431\u0443\u0434\u0435\u0442 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) \t\tdefer cancel()      \/\/ \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u044b \t\tserver.SetKeepAlivesEnabled(false)     \/\/ \u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \t\tif err := server.Shutdown(ctx); err != nil { \t\t\ts.logger.Fatalf(\"Could not gracefully shutdown the server: %v\\n\", err) \t\t} \t\tclose(done) \t}()  \ts.logger.Println(\"Server is ready to handle requests at\", address)   \/\/ \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043b\u0441\u044f \u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e \tatomic.StoreInt32(&amp;s.healthy, 1)   \/\/ \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \tif err := server.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed { \t\ts.logger.Fatalf(\"Could not listen on %s: %v\\n\", address, err) \t}    \/\/ \u041a\u043e\u0433\u0434\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0441\u043d\u043e\u0432\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u043b\u043e\u0433\u0438\u0440\u0443\u0435\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \t&lt;-done \ts.logger.Println(\"Server stopped\") } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u0445\u044d\u043d\u0434\u043b\u0435\u0440. \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0430\u043f\u043a\u0443 \u0432 <code>web -&gt; server -&gt; handlers<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>healthcheck.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package handlers  import ( \t\"net\/http\" )  func Healthcheck() http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { \t\tw.Write(\"OK\") \t}) } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043d\u0430\u0448 \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">\/\/ \u041d\u0430\u0448 \u043a\u043e\u0434 \u0432\u044b\u0448\u0435  func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t)   return router }  \/\/ \u041d\u0430\u0448 \u043a\u043e\u0434 \u043d\u0438\u0436\u0435<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418\u0434\u0435\u043c \u0432 <code>main.go<\/code> \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440:<\/p>\n<pre><code class=\"go\">package main  import ( \t\"log\"   \"os\"   \"go-scrum-poker-bot\/web\/server\" )  func main() {   \/\/ \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043b\u043e\u0433\u0433\u0435\u0440 \u0441\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u043c\u0438 \u0444\u043b\u0430\u0433\u0430\u043c\u0438 \u0438 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \"INFO:\".    \/\/ \u041f\u0438\u0441\u0430\u0442\u044c \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 stdout \tlogger := log.New(os.Stdout, \"INFO: \", log.LstdFlags) \tapp := server.NewServer(logger)  \tapp.Serve(\":8000\") }<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0431\u0443\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442:<\/p>\n<pre><code class=\"bash\">go run main.go<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0442\u043e \u0441\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0441\u044f \u043d\u0430 <code>:8000<\/code> \u043f\u043e\u0440\u0442\u0443. \u041d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043a \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 \u043d\u0438\u0445 \u043b\u044e\u0431\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u042d\u0442\u043e \u043d\u0430\u043c \u0435\u0449\u0435 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b. \ud83d\ude09 \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0438\u0434\u0442\u0438 \u0434\u0430\u043b\u044c\u0448\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0430\u0448\u0443 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0440\u0435\u0434\u0443, \u0447\u0442\u043e\u0431\u044b Slack \u0441\u043c\u043e\u0433 \u0441 \u043d\u0430\u043c\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c.<\/p>\n<h2>NGROK<\/h2>\n<p>\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u0443 \u043d\u0430\u0448\u0435\u0433\u043e \u0431\u043e\u0442\u0430, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0435\u0431\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c <a href=\"https:\/\/ngrok.com\/\" rel=\"noopener noreferrer nofollow\">ngrok<\/a>. \u0412\u043e\u043e\u0431\u0449\u0435 \u043c\u043e\u0436\u043d\u043e \u043b\u044e\u0431\u043e\u0439 \u0434\u0440\u0443\u0433\u043e\u0439, \u043d\u043e \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u0438 \u043f\u0440\u043e\u0441\u0442 \u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438. \u0414\u0430 \u0438 Slack \u0435\u0433\u043e <a href=\"https:\/\/api.slack.com\/tutorials\/tunneling-with-ngrok\" rel=\"noopener noreferrer nofollow\">\u0441\u043e\u0432\u0435\u0442\u0443\u0435\u0442<\/a>. \u0412 \u043e\u0431\u0449\u0435\u043c, \u043a\u043e\u0433\u0434\u0430 \u0432\u0441\u0435 \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439:<\/p>\n<pre><code>ngrok http 8000<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0435 \u0445\u043e\u0440\u043e\u0448\u043e, \u0442\u043e \u0432\u044b \u0443\u0432\u0438\u0434\u0438\u0442\u0435 \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 \u044d\u0442\u043e\u0433\u043e:<\/p>\n<pre><code>ngrok by @inconshreveable                                                                                                            (Ctrl+C to quit)                                                                                                                                                       Session Status                online                                                                                                                  Account                       Sayakhov Ilya (Plan: Free)                                                                                              Version                       2.3.35                                                                                                                  Region                        United States (us)                                                                                                      Web Interface                 http:\/\/127.0.0.1:4040                                                                                                   Forwarding                    http:\/\/ffd3cfcc460c.ngrok.io -&gt; http:\/\/localhost:8000                                                                   Forwarding                    https:\/\/ffd3cfcc460c.ngrok.io -&gt; http:\/\/localhost:8000                                                                                                                                                                                                                        Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                           0       0       0.00    0.00    0.00    0.00     <\/code><\/pre>\n<p>\u041d\u0430\u0441 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u0435\u0442 \u0441\u0442\u0440\u043e\u0447\u043a\u0430 <code>https:\/\/ffd3cfcc460c.ngrok.io<\/code>. \u041e\u043d\u0430 \u043d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0434\u0430\u043b\u044c\u0448\u0435.<\/p>\n<h2>Slash commands<\/h2>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 Slack. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 <a href=\"https:\/\/api.slack.com\/apps\" rel=\"noopener noreferrer nofollow\">\u0441\u044e\u0434\u0430<\/a><code> -&gt; Create New App<\/code>. \u0414\u0430\u043b\u0435\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u0438\u043c\u044f <code>GoScrumPokerBot<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0435\u0433\u043e \u0432 \u0441\u0432\u043e\u0439 <code>Workspace<\/code>. \u0414\u0430\u043b\u0435\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0434\u0430\u0442\u044c \u043d\u0430\u0448\u0435\u043c\u0443 \u0431\u043e\u0442\u0443 \u043f\u0440\u0430\u0432\u0430. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0434\u0435\u043c \u0432 <code>OAuth &amp; Permissions -&gt; Scopes<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u0440\u0430\u0432\u0430: <code>chat:write<\/code>, <code>commands<\/code>. \u041f\u0435\u0440\u0432\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043f\u0440\u0430\u0432 \u043d\u0443\u0436\u0435\u043d, \u0447\u0442\u043e\u0431\u044b \u0431\u043e\u0442 \u043c\u043e\u0433 \u043f\u0438\u0441\u0430\u0442\u044c \u0432 \u043a\u0430\u043d\u0430\u043b\u044b, \u0430 \u0432\u0442\u043e\u0440\u043e\u0439 \u0434\u043b\u044f slash \u043a\u043e\u043c\u0430\u043d\u0434. \u0418 \u043d\u0430\u043a\u043e\u043d\u0435\u0446 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u043d\u0430 <code>Reinstall to Workspace<\/code>. \u0413\u043e\u0442\u043e\u0432\u043e! \u0422\u0435\u043f\u0435\u0440\u044c \u0438\u0434\u0435\u043c \u0432 \u0440\u0430\u0437\u0434\u0435\u043b <code>Slash commands<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 <code>\/poker<\/code> . <\/p>\n<p>\u0412 Request URL \u043d\u0443\u0436\u043d\u043e \u0432\u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0438\u0437 \u043f\u0443\u043d\u043a\u0442\u0430 \u0432\u044b\u0448\u0435 + \u043f\u0443\u0442\u044c. \u041f\u0443\u0441\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u0442\u0430\u043a: <code>https:\/\/ffd3cfcc460c.ngrok.io\/play-poker<\/code>.<\/p>\n<h2>Slash command handler<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u043a\u043e\u043c\u0430\u043d\u0434\u0443. \u0418\u0434\u0435\u043c \u0432 <code>web -&gt; server -&gt; handlers<\/code> \u0438 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <code>play_poker.go<\/code>:<\/p>\n<pre><code class=\"go\">func PlayPokerCommand() http.Handler { \treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {     w.Header().Set(\"Content-Type\", \"application\/json\")     w.Write([]byte(`{\"response_type\": \"ephemeral\", \"text\": \"Hello world!\"}`)) \t}) }<\/code><\/pre>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448 \u0445\u044d\u043d\u0434\u043b\u0435\u0440 \u0432 \u0440\u043e\u0443\u0442\u0435\u0440:<\/p>\n<details class=\"spoiler\">\n<summary>server.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">func (s *Server) setupRouter() http.Handler { \trouter := http.NewServeMux() \trouter.Handle( \t\t\"\/healthcheck\", \t\thandlers.Healthcheck(), \t) \trouter.Handle( \t\t\"\/play-poker\", \t\thandlers.PlayPokerCommand(), \t)   return router }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0418\u0434\u0435\u043c \u0432 Slack \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443: <code>\/poker<\/code>. \u0412 \u043e\u0442\u0432\u0435\u0442 \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 \u044d\u0442\u043e\u0433\u043e:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u041d\u043e \u044d\u0442\u043e \u043d\u0435 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u043e Slack. \u041c\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u043c \u0441\u043b\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043a\u0430\u043d\u0430\u043b. \u042d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043c\u043d\u0435 \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u0438 \u043f\u043b\u044e\u0441 \u0443 \u043d\u0435\u0433\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0435\u0439 \u0432 \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0438 \u0441 \u043e\u0442\u0432\u0435\u0442\u043e\u043c \u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0443. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432 \u0444\u043e\u043d\u0435 (\u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u043e\u043b\u0433\u0438\u0445 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439). \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043d\u0430\u0448 <code>http<\/code> \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0418\u0434\u0435\u043c \u0432 <code>web -&gt; clients<\/code>. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <code>client.go<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>client.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package clients  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f \u043d\u0430\u0448\u0438\u0445 \u0445\u044d\u043d\u0434\u043b\u0435\u0440\u043e\u0432 type Handler func(request *Request) *Response  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f middleware (\u043e \u043d\u0438\u0445 \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435) type Middleware func(handler Handler, request *Request) Handler  \/\/ \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 http \u043a\u043b\u0438\u0435\u043d\u0442\u0430 type Client interface { \tMake(request *Request) *Response }  \/\/ \u041d\u0430\u0448\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 type BasicClient struct { \tclient     *http.Client \tmiddleware []Middleware }  func NewBasicClient(client *http.Client, middleware []Middleware) Client { \treturn &amp;BasicClient{client: client, middleware: middleware} }  \/\/ \u041f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0432\u0441\u0435\u0439 \u0433\u0440\u044f\u0437\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b func (c *BasicClient) makeRequest(request *Request) *Response { \tpayload, err := request.ToBytes() \/\/ TODO \tif err != nil { \t\treturn &amp;Response{Er<\/code><\/pre>\n<\/div>\n<\/details>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-318983","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/318983","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=318983"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/318983\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=318983"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=318983"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=318983"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}