Last active
January 6, 2021 23:21
-
-
Save keshihoriuchi/0920586ef7ec5f0dece5efe9081ff8f1 to your computer and use it in GitHub Desktop.
Slack created channel notifier
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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