diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a92263d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.4 + +install: + - go get github.com/cpalone/dronehook + - go get github.com/cpalone/travishook + - go get github.com/cpalone/gohook + - go get github.com/Sirupsen/logrus + - go get github.com/gorilla/websocket + +script: go test ./... \ No newline at end of file diff --git a/README.md b/README.md index 04cb9a2..ea6ce1e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# fireside-gitbot +# gitbot Listens for github webhook events and posts them as messages to a Euphoria room. diff --git a/ci.go b/ci.go new file mode 100644 index 0000000..9085b80 --- /dev/null +++ b/ci.go @@ -0,0 +1,70 @@ +package githubbot + +import ( + "fmt" + "strconv" + + "github.com/cpalone/dronehook" + "github.com/cpalone/travishook" +) + +func (s *Session) ciHandler() error { + // spin off travis server + tServer := travishook.NewServer(8085, "/travishook") + s.logger.Info("Starting travis server...") + tServer.GoListenAndServe() + + //spin off drone server + dServer := dronehook.NewServer(8082, "/dronehook") + s.logger.Info("Starting drone server...") + dServer.GoListenAndServe() + + for { + select { + case p := <-tServer.Out: + var parent string + parent, ok := s.commitParent[p.Commit] + if !ok { + parent = "" + } + var emoji string + if p.StatusMessage == "Passed" || p.StatusMessage == "Fixed" { + emoji = ":white_check_mark:" + } else { + emoji = ":no_entry:" + } + s.sendMessage(fmt.Sprintf( + "%s [ travis-ci.org | %s | Branch: %s ] (%s)", + emoji, p.Repository.Name, p.Branch, p.BuildURL), + parent, strconv.Itoa(s.msgID)) + s.msgID++ + case p := <-dServer.Out: + var parent string + parent, ok := s.commitParent[p.Build.Commit] + if !ok { + parent = "" + } + var emoji string + if p.Build.Status == "success" { + emoji = ":white_check_mark:" + } else { + emoji = ":no_entry:" + } + // https://drone.in.euphoria.io/euphoria-io/heim/77 + url := fmt.Sprintf("drone.in.euphoria.io/%s/%s/%v", + p.Repo.Name, + p.Build.Branch, + p.Build.Number, + ) + str := fmt.Sprintf("%s [ drone.io | %s | Branch: %s ] (%s)", + emoji, + p.Repo.Name, + p.Build.Branch, + url, + ) + s.sendMessage(str, + parent, strconv.Itoa(s.msgID)) + s.msgID++ + } + } +} diff --git a/github.go b/github.go index 0575977..1d419f6 100644 --- a/github.go +++ b/github.go @@ -2,69 +2,201 @@ package githubbot import ( "fmt" + "strconv" + "time" "github.com/cpalone/gohook" ) -func (s *Session) hookServer(port int, secret string) { - server := gohook.NewServer(port, secret, "/postreceive") - s.logger.Debug("Starting webhook server...") - server.GoListenAndServe() - s.logger.Debug("...started.") +func (s *Session) hookServer(port int, secret string, sendReplyChan chan PacketEvent) { + // spin off github server + gServer := gohook.NewServer(port, secret, "/postreceive") + s.logger.Info("Starting github server...") + gServer.GoListenAndServe() + + // Wait for github hook event for { - et := <-server.EventAndTypes + et := <-gServer.EventAndTypes s.logger.Infof("Received hook event of type '%s'.", et.Type) switch et.Type { - case gohook.PingEventType: - continue - case gohook.PushEventType: - s.logger.Debug("Entering PushEventType case.") - payload, ok := et.Event.(*gohook.PushEvent) - if !ok { - panic("Malformed *PushEvent.") - } - msg := fmt.Sprintf("[ %s | %s ] Commit: %s (%s)", - payload.Repository.Name, - payload.Ref[11:], // this discards "refs/heads/" - payload.HeadCommit.Message, - payload.HeadCommit.URL, - ) - s.sendMessage(msg, "") case gohook.CommitCommentEventType: payload, ok := et.Event.(*gohook.CommitCommentEvent) if !ok { panic("Malformed *CommitCommentEvent.") } + // TODO: can we get the branch here? msg := fmt.Sprintf("[ %s ] Comment on commit: %s (%s)", payload.Repository.Name, payload.Comment.Body, payload.Comment.HTMLURL, ) - s.sendMessage(msg, "") + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ + case gohook.CreateEventType: + payload, ok := et.Event.(*gohook.CreateEvent) + if !ok { + panic("Malformed *CreateEvent.") + } + msg := fmt.Sprintf("[ %s | Branch/Tag: %s] Created.", + payload.Repository.Name, + payload.RefType, + ) + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ + case gohook.DeleteEventType: + payload, ok := et.Event.(*gohook.DeleteEvent) + if !ok { + panic("Malformed *DeleteEvent.") + } + msg := fmt.Sprintf("[ %s | Branch/Tag: %s] Deleted.", + payload.Repository, + payload.RefType, + ) + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ case gohook.IssueCommentEventType: payload, ok := et.Event.(*gohook.IssueCommentEvent) if !ok { panic("Malformed *CommitCommentEvent.") } - msg := fmt.Sprintf("[ %s ] Comment on issue '%s': %s (%s)", + msg := fmt.Sprintf("[ %s | Issue: %s ] Comment: %s (%s)", payload.Repository.Name, payload.Issue.Title, payload.Comment.Body, payload.Comment.HTMLURL, ) - s.sendMessage(msg, "") + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ case gohook.IssuesEventType: payload, ok := et.Event.(*gohook.IssuesEvent) if !ok { panic("Malformed *IssuesEvent.") } - msg := fmt.Sprintf("[ %s ] Issue '%s' was %s. (%s)", + msg := fmt.Sprintf("[ %s | Issue: %s ] Action: %s. (%s)", payload.Repository.Name, payload.Issue.Title, payload.Action, payload.Issue.HTMLURL, ) - s.sendMessage(msg, "") + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ + case gohook.PullRequestEventType: + payload, ok := et.Event.(*gohook.PullRequestEvent) + if !ok { + panic("Malformed *PullRequestEvent.") + } + action := payload.Action + if action == "synchronize" { + action = "New commits made to synced branch." + } + msg := fmt.Sprintf(":pencil: [ %s | PR: %s ] %s (%s)", + payload.Repository.Name, + payload.PullRequest.Title, + action, + payload.PullRequest.HTMLURL, + ) + s.msgID++ + t := strconv.Itoa(int(time.Now().Unix())) + s.waiting = true + s.sendMessage(msg, "", t) + var reply PacketEvent + for s.waiting { + reply = <-sendReplyChan + if reply.ID == t { + s.waiting = false + } + } + srPayload, err := reply.Payload() + if err != nil { + s.logger.Fatalln(err) + } + + // need send-reply for msgID to reply to + data, ok := srPayload.(*SendReply) + if !ok { + s.logger.Fatalln("Could not assert *SendReply as such.") + } + s.commitParent[payload.PullRequest.Head.SHA] = data.ID + case gohook.PullRequestReviewCommentEventType: + payload, ok := et.Event.(*gohook.PullRequestReviewCommentEvent) + if !ok { + panic("Malformed *PullRequestReviewCommentEvent.") + } + msg := fmt.Sprintf(":speech_balloon: [ %s | PR: %s ] Comment: %s: %s (%s)", + payload.Repository.Name, + payload.PullRequest.Title, + payload.Sender.Login, + payload.Comment.Body, + payload.PullRequest.HTMLURL, + ) + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ + case gohook.RepositoryEventType: + payload, ok := et.Event.(*gohook.RepositoryEvent) + if !ok { + panic("Malformed *RepositoryEvent.") + } + msg := fmt.Sprintf("[ Repository: %s ] Action: created. (%s) ", + payload.Repository.Name, + payload.Repository.HTMLURL, + ) + s.sendMessage(msg, "", strconv.Itoa(s.msgID)) + s.msgID++ + case gohook.PushEventType: + s.logger.Info("Entering PushEventType case.") + payload, ok := et.Event.(*gohook.PushEvent) + if !ok { + panic("Malformed *PushEvent.") + } + if payload.HeadCommit.Message == "" { + continue + } + var msg string + if len(payload.Commits) > 1 { + msg = fmt.Sprintf(":repeat: [ %s | Branch: %s ] %v Commits: %s (%s)", + payload.Repository.Name, + payload.Ref[11:], // this discards "refs/heads/" + len(payload.Commits), + payload.HeadCommit.Message, + payload.Compare, + ) + } else { + msg = fmt.Sprintf(":repeat: [ %s | Branch: %s ] Commit: %s (%s)", + payload.Repository.Name, + payload.Ref[11:], // this discards "refs/heads/" + payload.HeadCommit.Message, + payload.HeadCommit.URL, + ) + } + t := strconv.Itoa(int(time.Now().Unix())) + s.waiting = true + s.sendMessage(msg, "", t) + var reply PacketEvent + for s.waiting { + reply = <-sendReplyChan + if reply.ID == t { + s.waiting = false + } + } + srPayload, err := reply.Payload() + if err != nil { + s.logger.Fatalln(err) + } + + // need send-reply for msgID to reply to + data, ok := srPayload.(*SendReply) + if !ok { + s.logger.Fatalln("Could not assert *SendReply as such.") + } + s.commitParent[payload.HeadCommit.ID] = data.ID + case gohook.PingEventType: + //payload, ok := et.Event.(*gohook.PingEvent) + //if !ok { + // panic("Malformed *PingEvent") + //} + s.sendMessage("[ Github Webhook | ping ]", "", strconv.Itoa(s.msgID)) + s.msgID++ } + } } diff --git a/main/main.go b/main/main.go index bd7c353..3d2728f 100644 --- a/main/main.go +++ b/main/main.go @@ -5,17 +5,21 @@ import ( "os" "github.com/Sirupsen/logrus" - gb "github.com/fireside-chat/githubbot" + gb "github.com/cpalone/githubbot" ) var roomName string var password string var verbose bool +var port int +var secret string func init() { flag.StringVar(&roomName, "room", "test", "room for the bot to join.") flag.StringVar(&password, "pass", "", "optional password for the bot to join.") flag.BoolVar(&verbose, "v", false, "Toggle whether debug statements are displayed.") + flag.IntVar(&port, "port", 8081, "Specify the port to listen on for webhook events.") + flag.StringVar(&secret, "secret", "", "Secret string used to encrypt webhook events.") } func main() { @@ -32,11 +36,11 @@ func main() { } else { logrus.SetLevel(logrus.InfoLevel) } - logger.Debugln("Flags parsed. Creating session...") - s, err := gb.NewSession(roomName, password, logger) + logger.Infoln("Flags parsed. Creating session...") + s, err := gb.NewSession(roomName, password, port, secret, logger) if err != nil { logger.Fatalf("Fatal error: creating session: %s", err) } - logger.Debugln("Session created.") + logger.Infoln("Session created.") s.Run() } diff --git a/packet.go b/packet.go index 7c424b1..ba4395f 100644 --- a/packet.go +++ b/packet.go @@ -49,6 +49,28 @@ type SendCommand struct { Parent string `json:"parent"` } +type Message struct { + ID string `json:"id"` + Parent string `json:"parent"` + PreviousEditID string `json:"previous_edit_id,omitempty"` + Time int64 `json:"time"` + Sender User `json:"sender"` + Content string `json:"content"` + EncryptionKeyID string `json:"encryption_key_id,omitempty"` + Edited int `json:"edited,omitempty"` + Deleted int `json:"deleted,omitempty"` +} + +type User struct { + ID string `json:"id"` + Name string `json:"name"` + ServerID string `json:"server_id"` + ServerEra string `json:"server_era"` +} + +type SendEvent Message +type SendReply Message + type NickCommand struct { Name string `json:"name"` } @@ -69,6 +91,10 @@ func (p *PacketEvent) Payload() (interface{}, error) { payload = &PingReply{} case AuthType: payload = &AuthCommand{} + case SendEventType: + payload = &SendEvent{} + case SendReplyType: + payload = &SendReply{} default: return p.Data, fmt.Errorf("Unexpected packet type: %s", p.Type) } diff --git a/session.go b/session.go index 274718b..28f534c 100644 --- a/session.go +++ b/session.go @@ -7,23 +7,29 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/Sirupsen/logrus" "github.com/gorilla/websocket" ) type Session struct { - RoomName string - password string - conn *websocket.Conn - inbound chan *PacketEvent - outbound chan *PacketEvent - errChan chan error - msgID int - logger *logrus.Logger + RoomName string + password string + conn *websocket.Conn + inbound chan *PacketEvent + outbound chan *PacketEvent + errChan chan error + msgID int + port int + secret string + logger *logrus.Logger + uptime time.Time + waiting bool + commitParent map[string]string } -func (s *Session) connect() error { +func (s *Session) connectOnce() error { s.logger.Debugln("Connecting to euphoria via TLS...") tlsConn, err := tls.Dial("tcp", "euphoria.io:443", &tls.Config{}) if err != nil { @@ -41,11 +47,30 @@ func (s *Session) connect() error { return nil } +func (s *Session) connect() error { + var err error + for i := 0; i < 5; i++ { + if err = s.connectOnce(); err == nil { + go s.sendNick() + return nil + } else { + s.logger.Infof("Error while connecting: %s\n", err) + time.Sleep(time.Duration(i+1) * time.Second * 5) + } + } + return err +} + func (s *Session) receivePacket() error { var packet PacketEvent err := s.conn.ReadJSON(&packet) if err != nil { - return err + if err := s.connect(); err != nil { + return err + } + if err := s.conn.ReadJSON(&packet); err != nil { + return nil + } } s.inbound <- &packet return nil @@ -60,13 +85,13 @@ func (s *Session) receiver() { } } -func (s *Session) sendPayload(payload interface{}, pType PacketType) { +func (s *Session) sendPayload(payload interface{}, pType PacketType, packetID string) { rawPayload, err := json.Marshal(payload) if err != nil { s.logger.Fatalf("Could not marshal payload: %s\n", err) } packet := &PacketEvent{ - ID: strconv.Itoa(s.msgID), + ID: packetID, Type: pType, } if err := packet.Data.UnmarshalJSON(rawPayload); err != nil { @@ -76,26 +101,28 @@ func (s *Session) sendPayload(payload interface{}, pType PacketType) { } func (s *Session) sendAuth() { - s.logger.Debugln("Sending auth.") + s.logger.Infoln("Sending auth.") payload := AuthCommand{ Type: "passcode", Passcode: s.password} - s.sendPayload(payload, AuthType) + s.sendPayload(payload, AuthType, strconv.Itoa(s.msgID)) + s.msgID++ } func (s *Session) sendNick() { - s.logger.Debugln("Sending nick.") + s.logger.Infoln("Sending nick.") payload := NickCommand{Name: "GithubBot"} - s.sendPayload(payload, NickType) + s.sendPayload(payload, NickType, strconv.Itoa(s.msgID)) + s.msgID++ } -func (s *Session) sendMessage(text string, parent string) { - s.logger.Debugf("Sending text message: '%s'", text) +func (s *Session) sendMessage(text string, parent string, packetID string) { + s.logger.Infof("Sending text message: '%s'", text) payload := SendCommand{ Content: text, Parent: parent, } - s.sendPayload(payload, SendType) + s.sendPayload(payload, SendType, packetID) } func (s *Session) handlePing(p *PacketEvent) { @@ -109,16 +136,44 @@ func (s *Session) handlePing(p *PacketEvent) { logrus.Fatalln("Cannot assert *PingEvent as such.") } out := PingReply{UnixTime: payload.Time} - s.sendPayload(out, PingReplyType) + s.sendPayload(out, PingReplyType, strconv.Itoa(s.msgID)) + s.msgID++ } -func (s *Session) inboundHandler() { +func (s *Session) handleSend(p *PacketEvent) { + s.logger.Debugln("Handling send-event.") + data, err := p.Payload() + if err != nil { + panic(err) + } + payload, ok := data.(*SendEvent) + if !ok { + logrus.Fatalln("Cannot assert *SendEvent as such.") + } + if payload.Content == "!uptime" { + since := time.Since(s.uptime) + s.sendMessage(fmt.Sprintf( + "This bot has been up for %s.", + since.String()), + p.ID, strconv.Itoa(s.msgID)) + s.msgID++ + } +} + +func (s *Session) inboundHandler(sendReplyChan chan PacketEvent) { for { packet := <-s.inbound - s.logger.Debugf("Receiving packet of type '%s'\n", packet.Type) + s.logger.Infof("Receiving packet of type '%s'\n", packet.Type) switch packet.Type { case PingEventType: s.handlePing(packet) + case SendEventType: + s.handleSend(packet) + case SendReplyType: + if !s.waiting { + continue + } + sendReplyChan <- *packet default: s.logger.Infof("Unhandled packet type '%s'", packet.Type) } @@ -128,26 +183,35 @@ func (s *Session) inboundHandler() { func (s *Session) outboundHandler() { for { packet := <-s.outbound - s.logger.Debugf("Sending packet of type '%s'\n", packet.Type) + s.logger.Infof("Sending packet of type '%s'\n", packet.Type) err := s.conn.WriteJSON(packet) if err != nil { - s.logger.Fatalf("Error sending packet: %s\n", err) + if err := s.connect(); err != nil { + s.logger.Fatalf("Error sending packet: %s\n", err) + } + if err := s.conn.WriteJSON(packet); err != nil { + s.logger.Fatalf("Error sending packet: %s\n", err) + } } } } -func NewSession(roomName, password string, logger *logrus.Logger) (*Session, error) { +func NewSession(roomName, password string, port int, secret string, logger *logrus.Logger) (*Session, error) { inbound := make(chan *PacketEvent) outbound := make(chan *PacketEvent) errChan := make(chan error) s := Session{ - RoomName: roomName, - password: password, - inbound: inbound, - outbound: outbound, - errChan: errChan, - msgID: 0, - logger: logger, + RoomName: roomName, + password: password, + inbound: inbound, + outbound: outbound, + errChan: errChan, + msgID: 0, + logger: logger, + port: port, + secret: secret, + uptime: time.Now(), + commitParent: make(map[string]string), } if err := s.connect(); err != nil { return nil, err @@ -156,13 +220,17 @@ func NewSession(roomName, password string, logger *logrus.Logger) (*Session, err } func (s *Session) Run() { + if s.password != "" { go s.sendAuth() } + sendReplyChan := make(chan PacketEvent) go s.outboundHandler() - go s.inboundHandler() + go s.inboundHandler(sendReplyChan) go s.receiver() go s.sendNick() - go s.hookServer(8888, "secret") - <-s.errChan + go s.hookServer(s.port, s.secret, sendReplyChan) + go s.ciHandler() + err := <-s.errChan + s.logger.Fatalln("Session.Run: %s", err) }