42wim/matterbridge

View on GitHub
matterhook/matterhook.go

Summary

Maintainability
A
0 mins
Test Coverage
//Package matterhook provides interaction with mattermost incoming/outgoing webhooks
package matterhook

import (
    "bytes"
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "time"

    "github.com/gorilla/schema"
    "github.com/slack-go/slack"
)

// OMessage for mattermost incoming webhook. (send to mattermost)
type OMessage struct {
    Channel     string                 `json:"channel,omitempty"`
    IconURL     string                 `json:"icon_url,omitempty"`
    IconEmoji   string                 `json:"icon_emoji,omitempty"`
    UserName    string                 `json:"username,omitempty"`
    Text        string                 `json:"text"`
    Attachments []slack.Attachment     `json:"attachments,omitempty"`
    Type        string                 `json:"type,omitempty"`
    Props       map[string]interface{} `json:"props"`
}

// IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct {
    BotID       string `schema:"bot_id"`
    BotName     string `schema:"bot_name"`
    Token       string `schema:"token"`
    TeamID      string `schema:"team_id"`
    TeamDomain  string `schema:"team_domain"`
    ChannelID   string `schema:"channel_id"`
    ChannelName string `schema:"channel_name"`
    Timestamp   string `schema:"timestamp"`
    UserID      string `schema:"user_id"`
    UserName    string `schema:"user_name"`
    PostId      string `schema:"post_id"` //nolint:golint
    RawText     string `schema:"raw_text"`
    ServiceId   string `schema:"service_id"` //nolint:golint
    Text        string `schema:"text"`
    TriggerWord string `schema:"trigger_word"`
    FileIDs     string `schema:"file_ids"`
}

// Client for Mattermost.
type Client struct {
    // URL for incoming webhooks on mattermost.
    Url        string // nolint:golint
    In         chan IMessage
    Out        chan OMessage
    httpclient *http.Client
    Config
}

// Config for client.
type Config struct {
    BindAddress        string // Address to listen on
    Token              string // Only allow this token from Mattermost. (Allow everything when empty)
    InsecureSkipVerify bool   // disable certificate checking
    DisableServer      bool   // Do not start server for outgoing webhooks from Mattermost.
}

// New Mattermost client.
func New(url string, config Config) *Client {
    c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec
    }
    c.httpclient = &http.Client{Transport: tr}
    if !c.DisableServer {
        _, _, err := net.SplitHostPort(c.BindAddress)
        if err != nil {
            log.Fatalf("incorrect bindaddress %s", c.BindAddress)
        }
        go c.StartServer()
    }
    return c
}

// StartServer starts a webserver listening for incoming mattermost POSTS.
func (c *Client) StartServer() {
    mux := http.NewServeMux()
    mux.Handle("/", c)
    srv := &http.Server{
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        Handler:      mux,
        Addr:         c.BindAddress,
    }
    log.Printf("Listening on http://%v...\n", c.BindAddress)
    if err := srv.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

// ServeHTTP implementation.
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        log.Println("invalid " + r.Method + " connection from " + r.RemoteAddr)
        http.NotFound(w, r)
        return
    }
    msg := IMessage{}
    err := r.ParseForm()
    if err != nil {
        log.Println(err)
        http.NotFound(w, r)
        return
    }
    defer r.Body.Close()
    decoder := schema.NewDecoder()
    err = decoder.Decode(&msg, r.PostForm)
    if err != nil {
        log.Println(err)
        http.NotFound(w, r)
        return
    }
    if msg.Token == "" {
        log.Println("no token from " + r.RemoteAddr)
        http.NotFound(w, r)
        return
    }
    if c.Token != "" {
        if msg.Token != c.Token {
            log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
            http.NotFound(w, r)
            return
        }
    }
    c.In <- msg
}

// Receive returns an incoming message from mattermost outgoing webhooks URL.
func (c *Client) Receive() IMessage {
    var msg IMessage
    for msg := range c.In {
        return msg
    }
    return msg
}

// Send sends a msg to mattermost incoming webhooks URL.
func (c *Client) Send(msg OMessage) error {
    buf, err := json.Marshal(msg)
    if err != nil {
        return err
    }
    resp, err := c.httpclient.Post(c.Url, "application/json", bytes.NewReader(buf))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // Read entire body to completion to re-use keep-alive connections.
    io.Copy(ioutil.Discard, resp.Body)

    if resp.StatusCode != 200 {
        return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }
    return nil
}