Skip to content

Instantly share code, notes, and snippets.

@keshihoriuchi
Last active January 6, 2021 23:21
Show Gist options
  • Save keshihoriuchi/0920586ef7ec5f0dece5efe9081ff8f1 to your computer and use it in GitHub Desktop.
Save keshihoriuchi/0920586ef7ec5f0dece5efe9081ff8f1 to your computer and use it in GitHub Desktop.
Slack created channel notifier
package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type partialEvent struct {
Type string `json:"type"`
Token string `json:"token"`
}
type urlVerificationEvent struct {
partialEvent
Challenge string `json:"challenge"`
}
type eventCallbackEvent struct {
partialEvent
TeamID string `json:"team_id"`
APIAppID string `json:"api_app_id"`
EventID string `json:"event_id"`
EventTime int `json:"event_time"`
Event *channelCreatedEvent `json:"event"`
}
type channelCreatedEvent struct {
Type string `json:"type"`
Channel *channelObject `json:"channel"`
}
type channelObject struct {
ID string `json:"id"`
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general,omitempty"`
Name string `json:"name"`
NameNormalized string `json:"name_normalized"`
Created int `json:"created"`
Creator string `json:"creator"`
IsShared bool `json:"is_shared"`
IsOrgShared bool `json:"is_org_shared"`
}
type chatMessage struct {
Text string `json:"text"`
Channel string `json:"channel"`
LinkNames bool `json:"link_names"`
IconEmoji string `json:"icon_emoji"`
}
type callbackQuery struct {
Code string `query:"code"`
State string `query:"state"`
}
type tokenResponse struct {
Ok bool `json:"ok"`
AccessToken string `json:"access_token"`
Team *tokenResponseTeam `json:"team"`
}
type tokenResponseTeam struct {
Name string `json:"name"`
ID string `json:"id"`
}
type event struct {
EventID string
Time time.Time
}
type eventHandler struct {
EventCh chan event
ToBeSentCh chan bool
}
func main() {
godotenv.Load()
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/authorize", authorize)
e.GET("/authorize/callback", authorizeCallback)
ech := make(chan event)
tch := make(chan bool)
eh := &eventHandler{EventCh: ech, ToBeSentCh: tch}
go checkRetriedEvent(ech, tch)
e.POST("/", eh.processEvent)
e.Logger.Fatal(e.Start(":" + port))
}
func authorize(c echo.Context) error {
state, err := generateRandomString(10)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusInternalServerError, "Internal Error")
}
redirectURI := url.QueryEscape(os.Getenv("URL_PREFIX") + "/authorize/callback")
return c.Redirect(http.StatusTemporaryRedirect, "https://slack.com/oauth/v2/authorize?client_id="+os.Getenv("CLIENT_ID")+"&scope=channels%3Aread%20chat%3Awrite%20chat%3Awrite.public%20chat%3Awrite.customize&user_scope=channels%3Aread&state="+state+"&redirect_uri="+redirectURI)
}
func authorizeCallback(c echo.Context) error {
cbq := new(callbackQuery)
if err := c.Bind(cbq); err != nil {
return c.String(http.StatusBadRequest, "Invalid Query")
}
res, err := requestTokenEndpoint(c, cbq.Code)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, "Failed to get token")
}
return c.String(http.StatusOK, "Please inform following information to your administrator\nTeam ID: "+res.Team.ID+"\nAccess Token: "+res.AccessToken)
}
func (eh *eventHandler) processEvent(c echo.Context) error {
bodyBytes, err := ioutil.ReadAll(c.Request().Body)
c.Logger().Print(string(bodyBytes))
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, "Invalid body")
}
var p partialEvent
err = json.Unmarshal(bodyBytes, &p)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, "Invalid body")
}
if p.Token != os.Getenv("VERIFICATION_TOKEN") {
c.Logger().Error("got: " + p.Token + "saved:" + os.Getenv("VERIFICATION_TOKEN"))
return c.String(http.StatusBadRequest, "Bad token")
}
if p.Type == "url_verification" {
var ue urlVerificationEvent
err = json.Unmarshal(bodyBytes, &ue)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, "Invalid challenge")
}
return c.String(http.StatusOK, ue.Challenge)
} else if p.Type == "event_callback" {
var ee eventCallbackEvent
err = json.Unmarshal(bodyBytes, &ee)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusOK, "ok")
}
go postCreatedChannel(c, ee, eh.EventCh, eh.ToBeSentCh)
return c.String(http.StatusOK, "ok")
}
return c.String(http.StatusBadRequest, "Invalid request")
}
func postCreatedChannel(c echo.Context, ee eventCallbackEvent, ech chan event, tch chan bool) error {
ev := event{EventID: ee.EventID, Time: time.Now()}
ech <- ev
toBeSent := <-tch
if !toBeSent {
return nil
}
client := new(http.Client)
reqBody, err := json.Marshal(chatMessage{
Text: "New channel #" + ee.Event.Channel.Name, Channel: os.Getenv("CHANNEL_TO_POST_" + ee.TeamID), LinkNames: true, IconEmoji: ":tada:",
})
if err != nil {
c.Logger().Error(err)
return err
}
req, err := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewBuffer(reqBody))
if err != nil {
c.Logger().Error(err)
return err
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("OAUTH_TOKEN_"+ee.TeamID))
req.Header.Add("Content-Type", "application/json; charset=utf-8")
resp, err := client.Do(req)
if err != nil {
c.Logger().Error(err)
return err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.Logger().Error(err)
return err
}
c.Logger().Print(string(body))
return nil
}
func checkRetriedEvent(ech chan event, tch chan bool) {
evs := make([]event, 0)
for {
ev := <-ech
isExist := false
for i := range evs {
if evs[i].EventID == ev.EventID {
isExist = true
break
}
}
if !isExist {
evs = append(evs, ev)
}
tch <- !isExist
newEvs := make([]event, 0)
for _, e := range evs {
if time.Since(e.Time).Minutes() < 10 {
newEvs = append(newEvs, e)
}
}
evs = newEvs
}
}
func requestTokenEndpoint(c echo.Context, code string) (tokenResponse, error) {
data := url.Values{}
data.Set("code", code)
data.Set("redirect_uri", os.Getenv("URL_PREFIX")+"/authorize/callback")
client := new(http.Client)
req, err := http.NewRequest("POST", "https://slack.com/api/oauth.v2.access", strings.NewReader(data.Encode()))
if err != nil {
return tokenResponse{}, err
}
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return tokenResponse{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tokenResponse{}, err
}
c.Logger().Print(string(body))
var tr tokenResponse
err = json.Unmarshal(body, &tr)
if err != nil {
return tokenResponse{}, err
}
if !tr.Ok {
return tokenResponse{}, errors.New("request failed")
}
return tr, nil
}
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
func generateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
bytes, err := generateRandomBytes(n)
if err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment