Skip to content

Commit

Permalink
Merge pull request #5 from gouline/slack-refactor
Browse files Browse the repository at this point in the history
Refactor Slack auth into separate package
  • Loading branch information
gouline authored Sep 9, 2024
2 parents 2aaf609 + 4dd0b5c commit b9d8ef4
Show file tree
Hide file tree
Showing 21 changed files with 706 additions and 369 deletions.
27 changes: 0 additions & 27 deletions internal/pkg/format/format.go

This file was deleted.

36 changes: 0 additions & 36 deletions internal/pkg/format/format_test.go

This file was deleted.

207 changes: 62 additions & 145 deletions internal/pkg/server/api.go
Original file line number Diff line number Diff line change
@@ -1,196 +1,113 @@
package server

import (
"fmt"
"net/http"
"regexp"
"strings"
"time"

"github.com/gouline/blaster/internal/pkg/format"
"github.com/gouline/blaster/internal/pkg/scache"
"github.com/gouline/blaster/internal/pkg/slack"
"github.com/labstack/echo/v4"
"github.com/slack-go/slack"
)

var suggestCache = scache.New(5*time.Minute, 10*time.Minute)

// APISuggest handles /api/suggest.
func (s *Server) handleAPISuggest(c echo.Context) error {
token := s.authorizedToken(c)
if token == "" {
slackCtx := s.slack.Context(c)
if !slackCtx.Authorized {
return c.String(http.StatusUnauthorized, "no token")
}

cacheResponse := <-buildSuggestCache(token)
if cacheResponse.Error != nil {
return c.String(http.StatusInternalServerError, cacheResponse.Error.Error())
destinations, err := slackCtx.Destinations()
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}

allSuggestions := cacheResponse.Value.([]suggestion)

// Filter out all symbols from term
symbolReg := format.NewAllSymbolsRegexp()
term := strings.ToLower(" " + c.QueryParam("term"))
term = symbolReg.ReplaceAllString(term, "")

// Filter users by term
suggestions := []suggestion{}
for _, suggestion := range allSuggestions {
if strings.Contains(suggestion.Search, term) {
suggestions = append(suggestions, suggestion)
if len(suggestions) == 10 {
break
}
}
}
suggestions := suggestDestinations(c.QueryParam("term"), destinations)

return c.JSON(http.StatusOK, suggestions)
}

// handleAPISend handles /api/send.
func (s *Server) handleAPISend(c echo.Context) error {
token := s.authorizedToken(c)
if token == "" {
return c.String(http.StatusUnauthorized, "no token")
slackCtx := s.slack.Context(c)
if !slackCtx.Authorized {
return c.NoContent(http.StatusUnauthorized)
}

client := slack.New(token)

// Bind JSON request
var request sendRequest
err := c.Bind(&request)
if err != nil {
if err := c.Bind(&request); err != nil {
return c.String(http.StatusBadRequest, err.Error())
}

// Open/get channel by user ID
channel, _, _, err := client.OpenConversation(&slack.OpenConversationParameters{
Users: []string{
request.User,
},
})
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}

// Post message to opened channel
_, _, err = client.PostMessage(
channel.ID,
slack.MsgOptionText(request.Message, false),
slack.MsgOptionAsUser(request.AsUser),
)
if err != nil {
if err := slackCtx.SendMessage(request.User, request.Message, request.AsUser); err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}

return c.JSON(http.StatusOK, struct{}{})
}

func buildSuggestCache(token string) <-chan scache.Response {
return suggestCache.ResponseChan(format.HashToken(token), func(key string) (interface{}, error) {
client := slack.New(token)

symbolReg := format.NewAllSymbolsRegexp()

var suggestions []suggestion

userLookup := map[string]suggestion{}

// Get all users
users, err := client.GetUsers()
if err != nil {
return nil, err
}

suggestions = []suggestion{}

for _, user := range users {
if user.Deleted || user.IsBot {
continue
}

realName := user.Profile.RealName
displayName := user.Profile.DisplayName

// Format label based on availability
label := realName
if displayName != "" {
label += " (" + displayName + ")"
// suggestDestinations filters destionations into suggestions by search term.
func suggestDestinations(term string, destinations []*slack.Destination) []*suggestion {
term = " " + sanitizeSearchTerm(term)

suggestions := []*suggestion{}
for _, dest := range destinations {
searchable := " " + sanitizeSearchTerm(strings.ToLower(dest.Name)+" "+strings.ToLower(dest.DisplayName))

if strings.Contains(searchable, term) {
children := []*suggestion{}
for _, child := range dest.Children {
children = append(children, &suggestion{
Type: sanitizeCSV(child.Type),
Label: sanitizeCSV(suggestionLabel(child.Name, child.DisplayName)),
Value: sanitizeCSV(child.ID),
})
}

// Filter out all symbols from search string
search := fmt.Sprintf(" %s %s", strings.ToLower(realName), strings.ToLower(displayName))
search = symbolReg.ReplaceAllString(search, "")

// Sanitize labels and values
sanitize := func(s string) string {
return strings.Replace(s, ",", "", -1)
}
suggestions = append(suggestions, &suggestion{
Type: sanitizeCSV(dest.Type),
Label: sanitizeCSV(suggestionLabel(dest.Name, dest.DisplayName)),
Value: sanitizeCSV(dest.ID),
Children: children,
})

s := suggestion{
Type: "user",
Label: sanitize(label),
Value: sanitize(user.ID),
Search: search,
if len(suggestions) == 10 {
break
}

suggestions = append(suggestions, s)

userLookup[user.ID] = s
}

usergroups, err := client.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(true))
if err != nil {
return nil, err
}
}

for _, usergroup := range usergroups {
if !usergroup.IsUserGroup {
continue
}

children := []suggestion{}

for _, userID := range usergroup.Users {
user, found := userLookup[userID]
if !found {
continue
}

children = append(children, user)
}

name := usergroup.Name
handle := usergroup.Handle
label := name + " (" + handle + ")"

// Filter out all symbols from search string
search := fmt.Sprintf(" %s %s", strings.ToLower(name), strings.ToLower(handle))
search = symbolReg.ReplaceAllString(search, "")
return suggestions
}

suggestions = append(suggestions, suggestion{
Type: "usergroup",
Label: label,
Value: "null",
Search: search,
Children: children,
})
}
// sanitizeSearchTerm removes lowercases search term and removes skippable characters.
func sanitizeSearchTerm(s string) string {
symbolReg := regexp.MustCompile("[!-/:-@[-`{-~]+")
return symbolReg.ReplaceAllString(strings.ToLower(s), "")
}

return suggestions, nil
})
// sanitizeCSV removes commas for comma-separated values.
func sanitizeCSV(s string) string {
return strings.Replace(s, ",", "", -1)
}

type suggestion struct {
Type string `json:"type"`
Label string `json:"label"`
Value string `json:"value"`
Children []suggestion `json:"children,omitempty"`
Search string `json:"-"`
// suggestionLabel formats label with a mandatory name and an optional display name.
func suggestionLabel(name, displayName string) string {
label := name
if displayName != "" {
label += " (" + displayName + ")"
}
return label
}

type sendRequest struct {
User string `json:"user"`
Message string `json:"message"`
AsUser bool `json:"as_user"`
}

type suggestion struct {
Type string `json:"type"`
Label string `json:"label"`
Value string `json:"value"`
Children []*suggestion `json:"children,omitempty"`
}
Loading

0 comments on commit b9d8ef4

Please sign in to comment.