├── .gitignore ├── internal ├── errors │ └── api_error.go ├── messages │ ├── emoji.go │ └── content.go ├── responses │ └── api │ │ └── v1 │ │ └── conversations.go └── server │ └── server.go ├── cmd └── fossteams-backend │ └── main.go ├── README.md ├── LICENSE.txt ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /internal/errors/api_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type ApiError struct { 4 | Message string `json:"message"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/messages/emoji.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | type Emoji struct { 4 | Code string 5 | Text string 6 | } 7 | 8 | var emojiMap = map[string]string{ 9 | "smile": "🙂", 10 | "laugh": "😀", 11 | } 12 | 13 | func GetEmojiByCode(code string) *Emoji { 14 | val, ok := emojiMap[code] 15 | if !ok { 16 | return nil 17 | } 18 | return &Emoji{ 19 | Code: code, 20 | Text: val, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/fossteams-backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alexflint/go-arg" 5 | "github.com/fossteams/fossteams-backend/internal/server" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | var args struct { 10 | Debug *bool `arg:"-d"` 11 | } 12 | 13 | func main() { 14 | arg.MustParse(&args) 15 | 16 | logger := logrus.New() 17 | s, err := server.New(logger) 18 | 19 | if err != nil { 20 | logger.Fatalf("unable to create server: %v", err) 21 | } 22 | 23 | logger.Fatal(s.Start("0.0.0.0:8050")) 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fossteams-backend 2 | 3 | A Golang based web server that exposes some endpoints to interact with a 4 | Microsoft Teams session. This allows different clients (such as 5 | [fossteams-frontend](https://github.com/fossteams/fossteams-frontend)) to 6 | interact with Microsoft Teams backends without having to deal with 7 | changing APIs and authentication. 8 | 9 | ## Requirements 10 | 11 | - A Microsoft Teams account 12 | - Go 13 | - [teams-token](https://github.com/fossteams/teams-token) 14 | 15 | ## Running the server 16 | 17 | 1. Get a token with [teams-token](https://github.com/fossteams/teams-token) 18 | 1. `go run ./cmd/fossteams-backend/` 19 | 20 | ## Check if the server is working properly 21 | 22 | Visit: http://127.0.0.1:8050/api/v1/conversations, if you get a non-empty JSON 23 | and no errors on the console (or a 200 OK status code), all good! 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | fossteams-backend 2 | Copyright © 2021 Denys Vitali 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fossteams/fossteams-backend 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ReneKroon/ttlcache/v2 v2.7.0 7 | github.com/alexflint/go-arg v1.4.2 8 | github.com/fossteams/teams-api v0.0.0-20210608193737-ead87df795c2 9 | github.com/gin-contrib/cors v1.3.1 10 | github.com/gin-gonic/gin v1.7.2 11 | github.com/sirupsen/logrus v1.8.1 12 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 13 | ) 14 | 15 | require ( 16 | github.com/alexflint/go-scalar v1.0.0 // indirect 17 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.13.0 // indirect 20 | github.com/go-playground/universal-translator v0.17.0 // indirect 21 | github.com/go-playground/validator/v10 v10.4.1 // indirect 22 | github.com/golang/protobuf v1.3.3 // indirect 23 | github.com/json-iterator/go v1.1.9 // indirect 24 | github.com/leodido/go-urn v1.2.0 // indirect 25 | github.com/mattn/go-isatty v0.0.12 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 27 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 28 | github.com/ugorji/go/codec v1.1.7 // indirect 29 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 30 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 31 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect 32 | gopkg.in/yaml.v2 v2.2.8 // indirect 33 | ) 34 | 35 | replace github.com/fossteams/teams-api => ../teams-api 36 | -------------------------------------------------------------------------------- /internal/messages/content.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/net/html" 6 | "strings" 7 | ) 8 | 9 | func ParseMessageContent(content string) string { 10 | t := html.NewTokenizer(strings.NewReader(content)) 11 | outputString := "" 12 | 13 | var blockQuoteCount = 0 14 | var emojiBlockCount = 0 15 | 16 | for { 17 | tokenType := t.Next() 18 | if tokenType == html.ErrorToken { 19 | return outputString 20 | } 21 | 22 | tagName, hasAttr := t.TagName() 23 | text := t.Text() 24 | 25 | switch tokenType { 26 | case html.StartTagToken: 27 | fmt.Printf("Start %s: hasAttr=%v\n", string(tagName), hasAttr) 28 | switch string(tagName) { 29 | case "span": 30 | if hasAttr { 31 | // Check if it's an emoji 32 | attributes := getAttributesByToken(t) 33 | emoticon := getEmoticon(attributes) 34 | if emoticon == nil { 35 | continue 36 | } else { 37 | outputString += emoticon.Text 38 | emojiBlockCount++ 39 | } 40 | } 41 | case "blockquote": 42 | fmt.Printf("blockquote=%s\n", text) 43 | blockQuoteCount++ 44 | } 45 | case html.TextToken: 46 | if emojiBlockCount > 0 { 47 | continue 48 | } 49 | if blockQuoteCount > 0 { 50 | outputString += strings.Repeat("> ", blockQuoteCount) 51 | } 52 | outputString += string(text) 53 | 54 | case html.EndTagToken: 55 | switch string(tagName) { 56 | case "blockquote": 57 | blockQuoteCount-- 58 | case "span": 59 | if emojiBlockCount > 0 { 60 | emojiBlockCount-- 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | func getAttributesByToken(t *html.Tokenizer) map[string]string { 68 | attributes := map[string]string{} 69 | var hasMoreAttr = true 70 | for hasMoreAttr { 71 | var key, val []byte 72 | key, val, hasMoreAttr = t.TagAttr() 73 | attributes[string(key)] = string(val) 74 | } 75 | return attributes 76 | } 77 | 78 | func getEmoticon(attributes map[string]string) *Emoji { 79 | emojiCode := "" 80 | for k, v := range attributes { 81 | switch k { 82 | case "class": 83 | if !strings.HasPrefix(v, "animated-emoticon-") { 84 | return nil 85 | } 86 | case "type": 87 | emojiCode = v 88 | } 89 | } 90 | 91 | return GetEmojiByCode(emojiCode) 92 | } 93 | -------------------------------------------------------------------------------- /internal/responses/api/v1/conversations.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/fossteams/teams-api/pkg/models" 4 | 5 | type Conversations struct { 6 | Chats []Chat `json:"chats"` 7 | Teams []Team `json:"teams"` 8 | } 9 | 10 | type Chat struct { 11 | Id string `json:"id"` 12 | Title string `json:"title"` 13 | LastMessage ShortMessage `json:"lastMessage"` 14 | IsOneOnOne bool `json:"isOneOnOne"` 15 | Creator string `json:"creator"` 16 | IsRead bool `json:"isRead"` 17 | IsLastMessageFromMe bool `json:"isLastMessageFromMe"` 18 | Members []ChatMember `json:"members"` 19 | } 20 | 21 | type Team struct { 22 | Creator string `json:"creator"` 23 | Id string `json:"id"` 24 | DisplayName string `json:"displayName"` 25 | Channels []Channel `json:"channels"` 26 | } 27 | 28 | type Channel struct { 29 | Id string `json:"id"` 30 | DisplayName string `json:"displayName"` 31 | LastMessage ShortMessage `json:"lastMessage"` 32 | Description string `json:"description"` 33 | Creator string `json:"creator"` 34 | ParentTeamId string `json:"parentTeamId"` 35 | } 36 | 37 | type ChatMember struct { 38 | Mri string `json:"mri"` 39 | Role string `json:"role"` 40 | TenantId string `json:"tenantId"` 41 | ObjectId string `json:"objectId"` 42 | } 43 | 44 | type ShortMessage struct { 45 | Id string `json:"id"` 46 | CleanContent string `json:"cleanContent"` 47 | Content string `json:"content"` 48 | From string `json:"from"` 49 | } 50 | 51 | type Message struct { 52 | ShortMessage 53 | ImDisplayName string `json:"imDisplayName"` 54 | OriginalArrivalTime models.RFC3339Time `json:"originalArrivalTime"` 55 | ConversationId string `json:"conversationId"` 56 | ParentID string `json:"parentID"` 57 | SequenceId int64 `json:"sequenceId"` 58 | MessageType string `json:"messageType"` 59 | Type string `json:"type"` 60 | Subject string `json:"subject,omitempty"` 61 | Title string `json:"title,omitempty"` 62 | Reactions map[string]int `json:"reactions,omitempty"` 63 | Replies []Message `json:"replies,omitempty"` 64 | } 65 | 66 | type ConversationResponse struct { 67 | Messages []Message `json:"messages"` 68 | } 69 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ReneKroon/ttlcache/v2" 6 | "github.com/fossteams/fossteams-backend/internal/errors" 7 | "github.com/fossteams/fossteams-backend/internal/messages" 8 | v1 "github.com/fossteams/fossteams-backend/internal/responses/api/v1" 9 | teams "github.com/fossteams/teams-api" 10 | models "github.com/fossteams/teams-api/pkg/models" 11 | "github.com/gin-contrib/cors" 12 | "github.com/gin-gonic/gin" 13 | "github.com/sirupsen/logrus" 14 | "net/http" 15 | "net/url" 16 | "sort" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | type Server struct { 22 | logger *logrus.Logger 23 | cache *ttlcache.Cache 24 | e *gin.Engine 25 | teams *teams.TeamsClient 26 | } 27 | 28 | const cacheTTL = 300 29 | 30 | func (s *Server) setupRoutes() { 31 | apiEndpoint := s.e.Group("/api") 32 | v1Endpoint := apiEndpoint.Group("/v1") 33 | 34 | s.setupApiV1(v1Endpoint) 35 | } 36 | 37 | func (s *Server) setupApiV1(endpoint *gin.RouterGroup) { 38 | endpoint.GET("/conversations", s.v1GetConversations) 39 | endpoint.GET("/conversations/:id", s.v1GetSingleConversation) 40 | endpoint.GET("/conversations/:id/profilePicture", s.v1GetConversationProfilePicture) 41 | } 42 | 43 | func parseMembers(members []models.ChatMember) []v1.ChatMember { 44 | var pMembers []v1.ChatMember 45 | 46 | for _, m := range members { 47 | pMembers = append(pMembers, v1.ChatMember{ 48 | Mri: m.Mri, 49 | Role: string(m.Role), 50 | TenantId: m.TenantId, 51 | ObjectId: m.ObjectId, 52 | }) 53 | } 54 | return pMembers 55 | } 56 | 57 | func parseChannels(channels []models.Channel) []v1.Channel { 58 | var chans []v1.Channel 59 | for _, c := range channels { 60 | chans = append(chans, v1.Channel{ 61 | Id: c.Id, 62 | DisplayName: c.DisplayName, 63 | Description: c.Description, 64 | Creator: c.Creator, 65 | ParentTeamId: c.ParentTeamId, 66 | LastMessage: processMessage(c.LastMessage), 67 | }) 68 | } 69 | return chans 70 | } 71 | 72 | func processMessage(message models.Message) v1.ShortMessage { 73 | return v1.ShortMessage{ 74 | Id: message.Id, 75 | Content: message.Content, 76 | From: message.From, 77 | } 78 | 79 | } 80 | 81 | func New(logger *logrus.Logger) (*Server, error) { 82 | if logger == nil { 83 | logger = logrus.New() 84 | } 85 | 86 | engine := gin.Default() 87 | teams, err := teams.New() 88 | 89 | if err != nil { 90 | return nil, fmt.Errorf("unable to initialize Teams client: %v", err) 91 | } 92 | 93 | cache := ttlcache.NewCache() 94 | err = cache.SetTTL(cacheTTL * time.Second) 95 | if err != nil { 96 | return nil, fmt.Errorf("unable to set ttlcache TTL: %v", err) 97 | } 98 | 99 | s := Server{ 100 | logger: logger, 101 | e: engine, 102 | teams: teams, 103 | cache: cache, 104 | } 105 | s.setupCors() 106 | s.setupRoutes() 107 | 108 | return &s, nil 109 | } 110 | 111 | func (s *Server) Start(addr string) error { 112 | return s.e.Run(addr) 113 | } 114 | 115 | func (s *Server) v1GetConversations(c *gin.Context) { 116 | const conversationsKey = "conversations" 117 | if v, err := s.cache.Get(conversationsKey); err != ttlcache.ErrNotFound { 118 | // cache hit 119 | c.JSON(http.StatusOK, v) 120 | return 121 | } 122 | 123 | // Fetch conversations 124 | if !s.checkTeams(c) { 125 | return 126 | } 127 | 128 | conv, err := s.teams.GetConversations() 129 | if err != nil { 130 | s.logger.Errorf("unable to get conversations: %v", err) 131 | c.JSON(http.StatusInternalServerError, errors.ApiError{ 132 | Message: "unable to fetch conversations", 133 | }) 134 | return 135 | } 136 | 137 | // Process conversations 138 | var chats []v1.Chat 139 | var teams []v1.Team 140 | 141 | for _, c := range conv.Chats { 142 | pc := v1.Chat{ 143 | Id: c.Id, 144 | Title: c.Title, 145 | LastMessage: processMessage(c.LastMessage), 146 | IsOneOnOne: c.IsOneOnOne, 147 | Creator: c.Creator, 148 | IsRead: c.IsRead, 149 | Members: parseMembers(c.Members), 150 | IsLastMessageFromMe: c.IsLastMessageFromMe, 151 | } 152 | 153 | chats = append(chats, pc) 154 | } 155 | 156 | for _, t := range conv.Teams { 157 | pt := v1.Team{ 158 | Creator: t.Creator, 159 | Id: t.Id, 160 | DisplayName: t.DisplayName, 161 | Channels: parseChannels(t.Channels), 162 | } 163 | teams = append(teams, pt) 164 | } 165 | 166 | resp := v1.Conversations{ 167 | Chats: chats, 168 | Teams: teams, 169 | } 170 | 171 | err = s.cache.Set(conversationsKey, resp) 172 | if err != nil { 173 | s.logger.Warnf("unable to set cache entry: %v", err) 174 | } 175 | 176 | c.JSON(http.StatusOK, resp) 177 | return 178 | } 179 | 180 | func (s *Server) v1GetSingleConversation(c *gin.Context) { 181 | // Conversation id is :id 182 | convId := c.Param("id") 183 | cacheKey := "conversations/" + convId 184 | 185 | if v, err := s.cache.Get(cacheKey); err != ttlcache.ErrNotFound { 186 | // cache hit 187 | c.JSON(http.StatusOK, v) 188 | return 189 | } 190 | 191 | if !s.checkTeams(c) { 192 | return 193 | } 194 | if convId == "" { 195 | c.JSON(http.StatusBadRequest, errors.ApiError{Message: "invalid conversation ID"}) 196 | return 197 | } 198 | 199 | s.teams.Debug(true) 200 | s.teams.ChatSvc().DebugDisallowUnknownFields(false) 201 | 202 | chatMessages, err := s.teams.GetMessages(&models.Channel{Id: convId}) 203 | if err != nil { 204 | s.logger.Errorf("unable to get messages for convId=%s: %v", convId, err) 205 | c.JSON(http.StatusInternalServerError, errors.ApiError{Message: "unable to get messages"}) 206 | return 207 | } 208 | 209 | pMessages := s.parseMessages(chatMessages) 210 | resp := v1.ConversationResponse{ 211 | Messages: pMessages, 212 | } 213 | err = s.cache.Set(cacheKey, resp) 214 | if err != nil { 215 | s.logger.Warnf("unable to set cache entry: %v", err) 216 | } 217 | c.JSON(http.StatusOK, resp) 218 | return 219 | } 220 | 221 | func (s *Server) v1GetConversationProfilePicture(c *gin.Context) { 222 | id := c.Param("id") 223 | if id == "" { 224 | c.JSON(http.StatusBadRequest, gin.H{"error": "please provide an id"}) 225 | return 226 | } 227 | 228 | buff, err := s.teams.GetTeamsProfilePicture(id) 229 | if err != nil { 230 | s.logger.Errorf("unable to get profile picture: %v", err) 231 | c.JSON(http.StatusNotFound, gin.H{"error": "unable to get profile picture"}) 232 | return 233 | } 234 | 235 | c.Status(http.StatusOK) 236 | _, _ = c.Writer.Write(buff) 237 | } 238 | 239 | func (s *Server) parseMessages(msgs []models.ChatMessage) []v1.Message { 240 | var pMessages []v1.Message 241 | threads := map[string][]v1.Message{} 242 | parentMessages := map[string]v1.Message{} 243 | 244 | seenParent := map[string]bool{} 245 | 246 | for _, m := range msgs { 247 | msg := v1.Message{ 248 | ShortMessage: v1.ShortMessage{ 249 | Id: m.Id, 250 | CleanContent: messages.ParseMessageContent(m.Content), 251 | Content: m.Content, 252 | From: m.From, 253 | }, 254 | ImDisplayName: m.ImDisplayName, 255 | OriginalArrivalTime: m.OriginalArrivalTime, 256 | ConversationId: m.ConversationId, 257 | ParentID: parseParentId(m.ConversationLink), 258 | SequenceId: m.SequenceId, 259 | MessageType: string(m.Type), 260 | Type: m.MessageType, 261 | Subject: m.Properties.Subject, 262 | Title: m.Properties.Title, 263 | Reactions: parseReactions(m.Properties.Emotions), 264 | } 265 | 266 | if msg.ParentID == msg.Id { 267 | parentMessages[msg.Id] = msg 268 | } else { 269 | threads[msg.ParentID] = append(threads[msg.ParentID], msg) 270 | } 271 | } 272 | 273 | for threadId, threadMessages := range threads { 274 | if val, ok := parentMessages[threadId]; !ok { 275 | s.logger.Warnf("thread %s doesn't exist", threadId) 276 | continue 277 | } else { 278 | val.Replies = append(val.Replies, threadMessages...) 279 | sort.Sort(bySequenceId(val.Replies)) 280 | pMessages = append(pMessages, val) 281 | seenParent[threadId] = true 282 | } 283 | } 284 | 285 | for k, msg := range parentMessages { 286 | if _, ok := seenParent[k]; !ok { 287 | pMessages = append(pMessages, msg) 288 | } 289 | } 290 | 291 | sort.Sort(bySequenceId(pMessages)) 292 | 293 | return pMessages 294 | } 295 | 296 | func parseReactions(emotions []models.Emotion) map[string]int { 297 | reactions := map[string]int{} 298 | for _, em := range emotions { 299 | reactions[strings.ToLower(em.Key)] = len(em.Users) 300 | } 301 | return reactions 302 | } 303 | 304 | func parseParentId(link string) string { 305 | mUrl, err := url.Parse(link) 306 | if err != nil { 307 | return "" 308 | } 309 | 310 | // get message id 311 | kv := strings.Split(mUrl.Path, ";") 312 | if len(kv) != 2 { 313 | return "" 314 | } 315 | 316 | messageId := strings.Split(kv[1], "=") 317 | if messageId[0] != "messageid" { 318 | return "" 319 | } 320 | if len(messageId) != 2 { 321 | return "" 322 | } 323 | 324 | return messageId[1] 325 | } 326 | 327 | type bySequenceId []v1.Message 328 | 329 | func (b bySequenceId) Len() int { 330 | return len(b) 331 | } 332 | 333 | func (b bySequenceId) Less(i, j int) bool { 334 | return b[i].SequenceId < b[j].SequenceId 335 | } 336 | 337 | func (b bySequenceId) Swap(i, j int) { 338 | b[i], b[j] = b[j], b[i] 339 | } 340 | 341 | var _ sort.Interface = bySequenceId{} 342 | 343 | func (s *Server) checkTeams(c *gin.Context) bool { 344 | if s.teams == nil { 345 | c.JSON(http.StatusInternalServerError, errors.ApiError{ 346 | Message: "teams client not ready", 347 | }) 348 | return false 349 | } 350 | return true 351 | } 352 | 353 | func (s *Server) setupCors() { 354 | s.e.Use(cors.New(cors.Config{ 355 | AllowOrigins: []string{"http://127.0.0.1:8080"}, 356 | AllowMethods: []string{"GET", "POST"}, 357 | AllowHeaders: []string{"Origin"}, 358 | MaxAge: 12 * time.Hour, 359 | })) 360 | } 361 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ReneKroon/ttlcache/v2 v2.7.0 h1:sZeaSwA2UN/y/h7CvkW15Kovd2Oiy76CBDORiOwHPwI= 2 | github.com/ReneKroon/ttlcache/v2 v2.7.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY= 3 | github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= 4 | github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= 5 | github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= 6 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 11 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 12 | github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= 13 | github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= 14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 17 | github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= 18 | github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 19 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 20 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 21 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 22 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 23 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 24 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 25 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 26 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 27 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 28 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 29 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 31 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 34 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 35 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 36 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 42 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 43 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 44 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 45 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 46 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 47 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 48 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 49 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 52 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 56 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 61 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 65 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 66 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 67 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 68 | go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= 69 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 71 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 72 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 73 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 74 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 75 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= 76 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 77 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 78 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 79 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 80 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 81 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 82 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 83 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= 84 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 85 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 88 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= 97 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 99 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 100 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 101 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 104 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 105 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 106 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 107 | golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I= 108 | golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 109 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 114 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 116 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 117 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 118 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 119 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 121 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | --------------------------------------------------------------------------------