├── .gitignore ├── slack ├── users.go ├── postMessage.go ├── slack.go └── groupMembers.go ├── pagerduty ├── users.go ├── pagerduty.go ├── schedules.go └── request.go ├── glide.yaml ├── updater ├── schedules.go ├── users.go ├── fetchdata.go ├── start.go ├── updater.go ├── updateschedules.go ├── updateusers.go └── updateGroups.go ├── .goxc.json ├── config ├── config.go ├── load.go └── validate.go ├── glide.lock ├── LICENCE.txt ├── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /config.yml 3 | /pagerbot 4 | /.goxc.local.json 5 | /outputs 6 | -------------------------------------------------------------------------------- /slack/users.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import( 4 | "github.com/nlopes/slack" 5 | ) 6 | 7 | func (a *Api) Users() ([]slack.User, error){ 8 | var usr []slack.User 9 | usr, err := a.api.GetUsers() 10 | if err != nil { 11 | return usr, err 12 | } 13 | 14 | return usr, nil 15 | } 16 | -------------------------------------------------------------------------------- /slack/postMessage.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import( 4 | "github.com/nlopes/slack" 5 | ) 6 | 7 | func (a *Api) PostMessage(channel string, message string) error{ 8 | mp := slack.NewPostMessageParameters() 9 | mp.Username = "Pagerduty Bot" 10 | mp.LinkNames = 1 11 | 12 | _, _, err := a.api.PostMessage(channel, message, mp) 13 | return err 14 | } 15 | -------------------------------------------------------------------------------- /pagerduty/users.go: -------------------------------------------------------------------------------- 1 | package pagerduty 2 | 3 | type Users []User 4 | 5 | type User struct{ 6 | Id string 7 | Name string 8 | Email string 9 | } 10 | 11 | func (a *Api) Users() (Users, error){ 12 | var usr Users 13 | 14 | err := a.requestThing("users", &usr) 15 | if err != nil { 16 | return usr, err 17 | } 18 | 19 | return usr, nil 20 | } 21 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/yosmudge/pagerbot 2 | import: 3 | - package: github.com/Sirupsen/logrus 4 | - package: github.com/voxelbrain/goptions 5 | - package: gopkg.in/yaml.v2 6 | - package: github.com/stretchr/testify 7 | - package: github.com/nlopes/slack 8 | repo: github.com/yosmudge/slack 9 | - package: golang.org/x/net 10 | subpackages: 11 | - websocket 12 | - package: gopkg.in/jmcvetta/napping.v3 13 | -------------------------------------------------------------------------------- /updater/schedules.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "github.com/yosmudge/pagerbot/pagerduty" 5 | ) 6 | 7 | type ScheduleList struct{ 8 | schedules []*pagerduty.Schedule 9 | } 10 | 11 | func (s *ScheduleList) ById(id string) *pagerduty.Schedule{ 12 | var schd *pagerduty.Schedule 13 | 14 | for _,sc := range s.schedules{ 15 | if sc.Id == id{ 16 | schd = sc 17 | break 18 | } 19 | } 20 | 21 | return schd 22 | } 23 | -------------------------------------------------------------------------------- /updater/users.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | type UserList struct{ 4 | users []*User 5 | } 6 | 7 | type User struct{ 8 | Name string 9 | SlackId string 10 | SlackName string 11 | PagerdutyId string 12 | Email string 13 | } 14 | 15 | func (u *UserList) ById(id string) *User{ 16 | var usr *User 17 | 18 | for _,us := range u.users{ 19 | if us.PagerdutyId == id{ 20 | usr = us 21 | break 22 | } 23 | } 24 | 25 | return usr 26 | } 27 | -------------------------------------------------------------------------------- /.goxc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ArtifactsDest": "outputs/", 3 | "Tasks": [ 4 | "xc", 5 | "codesign", 6 | "copy-resources", 7 | "archive-zip", 8 | "archive-tar-gz", 9 | "deb", 10 | "deb-dev", 11 | "rmbin", 12 | "downloads-page", 13 | "publish-github" 14 | ], 15 | "BuildConstraints": "linux,windows,darwin,freebsd", 16 | "PackageVersion": "1.1.0", 17 | "TaskSettings": { 18 | "publish-github": { 19 | "owner": "YoSmudge", 20 | "repository": "pagerbot" 21 | } 22 | }, 23 | "ConfigVersion": "0.9" 24 | } 25 | -------------------------------------------------------------------------------- /updater/fetchdata.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "time" 5 | "sync" 6 | log "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | func (u *Updater) fetchData(){ 10 | log.Debug("Fetching data") 11 | 12 | w := sync.WaitGroup{} 13 | w.Add(2) 14 | go u.updateUsers(&w) 15 | go u.updateSchedules(&w) 16 | 17 | w.Wait() 18 | log.WithFields(log.Fields{ 19 | "users": len(u.Users.users), 20 | "schedules": len(u.Schedules.schedules), 21 | }).Debug("Update done") 22 | u.LastFetch = time.Now().UTC() 23 | } 24 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type config struct{ 4 | ApiKeys struct{ 5 | Slack string 6 | Pagerduty struct{ 7 | Key string 8 | Org string 9 | } 10 | } `yaml:"api_keys"` 11 | Groups []struct{ 12 | Name string 13 | Schedules []string 14 | UpdateMessage struct{ 15 | Message string 16 | Channels []string 17 | } `yaml:"update_message"` 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import( 4 | "os" 5 | "fmt" 6 | "io/ioutil" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | var Config config 11 | 12 | func Load(filePath string) error{ 13 | Config = config{} 14 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 15 | return fmt.Errorf("Config file not found") 16 | } 17 | 18 | configContent, err := ioutil.ReadFile(filePath) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | err = yaml.Unmarshal(configContent, &Config) 24 | if err != nil { 25 | return fmt.Errorf("Error parsing config file: %s", err) 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import( 4 | "fmt" 5 | log "github.com/Sirupsen/logrus" 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | type Api struct{ 10 | api *slack.Client 11 | } 12 | 13 | func New(key string) (*Api, error){ 14 | a := Api{} 15 | 16 | a.api = slack.New(key) 17 | auth, err := a.api.AuthTest() 18 | if err != nil { 19 | return &a, fmt.Errorf("Error authenticating with Slack: %s", err) 20 | } 21 | 22 | log.WithFields(log.Fields{ 23 | "teamName": auth.Team, 24 | "userId": auth.UserID, 25 | "teamUrl": auth.URL, 26 | }).Info("Authenticated with Slack") 27 | 28 | return &a, nil 29 | } 30 | -------------------------------------------------------------------------------- /updater/start.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "time" 5 | ) 6 | 7 | // Start the updater process 8 | func (u *Updater) Start(){ 9 | u.Wg.Add(1) 10 | go u.run() 11 | } 12 | 13 | // Loop for updater 14 | // Will call for new data then call the update function 15 | // Runs on each `updateEvery` interval 16 | const updateEvery time.Duration = time.Minute*5 17 | func (u *Updater) run(){ 18 | defer u.Wg.Done() 19 | 20 | for { 21 | u.fetchData() 22 | u.updateGroups() 23 | 24 | nextInterval := time.Unix((time.Now().UTC().Unix()/int64(updateEvery.Seconds())+1)*int64(updateEvery.Seconds()), 0) 25 | waitTime := nextInterval.Sub(time.Now().UTC()) 26 | time.Sleep(waitTime) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pagerduty/pagerduty.go: -------------------------------------------------------------------------------- 1 | package pagerduty 2 | 3 | import( 4 | "fmt" 5 | "net/http" 6 | "gopkg.in/jmcvetta/napping.v3" 7 | ) 8 | 9 | type Api struct{ 10 | key string 11 | org string 12 | http *napping.Session 13 | timezone string 14 | } 15 | 16 | // Pagerduty API doesn't provide a sane way of checking for auth 17 | // so we just get the schedules at setup time 18 | func New(key string, org string) (*Api, error){ 19 | a := Api{} 20 | a.key = key 21 | a.org = org 22 | a.timezone = "UTC" 23 | 24 | a.http = &napping.Session{ 25 | Header: &http.Header{ 26 | "Authorization": []string{fmt.Sprintf("Token token=%s", a.key)}, 27 | "User-Agent": []string{"PagerBot +https://github.com/yosmudge/pagerbot"}, 28 | }, 29 | } 30 | 31 | _, err := a.request("schedules") 32 | if err != nil { 33 | return &a, err 34 | } 35 | 36 | return &a, nil 37 | } 38 | -------------------------------------------------------------------------------- /updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "time" 5 | "sync" 6 | "github.com/yosmudge/pagerbot/config" 7 | "github.com/yosmudge/pagerbot/slack" 8 | "github.com/yosmudge/pagerbot/pagerduty" 9 | ) 10 | 11 | type Updater struct{ 12 | Wg *sync.WaitGroup 13 | Slack *slack.Api 14 | Pagerduty *pagerduty.Api 15 | Users *UserList 16 | Schedules *ScheduleList 17 | LastFetch time.Time 18 | } 19 | 20 | func New() (*Updater, error){ 21 | u := Updater{} 22 | u.Wg = &sync.WaitGroup{} 23 | 24 | var err error 25 | u.Slack, err = slack.New(config.Config.ApiKeys.Slack) 26 | if err != nil { 27 | return &u, err 28 | } 29 | 30 | u.Pagerduty, err = pagerduty.New(config.Config.ApiKeys.Pagerduty.Key, config.Config.ApiKeys.Pagerduty.Org) 31 | if err != nil { 32 | return &u, err 33 | } 34 | 35 | u.Users = &UserList{} 36 | u.Schedules = &ScheduleList{} 37 | 38 | return &u, nil 39 | } 40 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 42d77bccba2438ac8a7a42fd2916790dfb87a362908bc346ebf00b4d811b8c27 2 | updated: 2016-06-01T18:22:28.234527011+01:00 3 | imports: 4 | - name: github.com/nlopes/slack 5 | version: 67c6939dd382a85ccbbe2f2e133fd245fe424098 6 | - name: github.com/Sirupsen/logrus 7 | version: f3cfb454f4c209e6668c95216c4744b8fddb2356 8 | - name: github.com/stretchr/testify 9 | version: 8d64eb7173c7753d6419fd4a9caf057398611364 10 | - name: github.com/voxelbrain/goptions 11 | version: 26cb8b04692384f4dc269de3b5fcf3e2ef78573e 12 | - name: golang.org/x/net 13 | version: c4c3ea71919de159c9e246d7be66deb7f0a39a58 14 | subpackages: 15 | - websocket 16 | - name: golang.org/x/sys 17 | version: 076b546753157f758b316e59bcb51e6807c04057 18 | subpackages: 19 | - unix 20 | - name: gopkg.in/jmcvetta/napping.v3 21 | version: 5be8267a453ffa5b80c4af7fa4cfc1d04d3cfd5c 22 | - name: gopkg.in/yaml.v2 23 | version: a83829b6f1293c91addabc89d0571c246397bbf4 24 | devImports: [] 25 | -------------------------------------------------------------------------------- /config/validate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import( 4 | "fmt" 5 | log "github.com/Sirupsen/logrus" 6 | ) 7 | 8 | // Validate the configuration file for sanity 9 | func (c *config) Validate() error{ 10 | if c.ApiKeys.Slack == "" || c.ApiKeys.Pagerduty.Key == ""{ 11 | return fmt.Errorf("You must provide API keys for Slack and Pagerduty") 12 | } 13 | 14 | if c.ApiKeys.Pagerduty.Org == ""{ 15 | return fmt.Errorf("You must provide an org name for Pagerduty (.pagerduty.com)") 16 | } 17 | 18 | if len(c.Groups) == 0{ 19 | return fmt.Errorf("You must specify at least one group") 20 | } 21 | 22 | for i,group := range c.Groups{ 23 | if group.Name == ""{ 24 | return fmt.Errorf("Must specify group name for group %d", i) 25 | } 26 | 27 | if len(group.Schedules) == 0{ 28 | return fmt.Errorf("Must specify at least one schedule for group %s", group.Name) 29 | } 30 | } 31 | 32 | log.WithFields(log.Fields{"groups":len(c.Groups)}).Debug("Loaded config") 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Sam Rudge, Songkick.com Ltd. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /slack/groupMembers.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import( 4 | "strings" 5 | ) 6 | 7 | func (a *Api) groupId(groupName string) (string, error){ 8 | var ugId string 9 | g, err := a.api.GetUserGroups() 10 | if err != nil { 11 | return ugId, err 12 | } 13 | 14 | for _,g := range g{ 15 | if g.Handle != groupName{ 16 | continue 17 | } 18 | ugId = g.ID 19 | } 20 | 21 | return ugId, nil 22 | } 23 | 24 | func (a *Api) GroupMembers(groupName string) ([]string, error){ 25 | var usr []string 26 | ugId, err := a.groupId(groupName) 27 | if err != nil { 28 | return usr, err 29 | } 30 | 31 | m, err := a.api.GetUserGroupMembers(ugId) 32 | if err != nil { 33 | return usr, err 34 | } 35 | 36 | for _,id := range m{ 37 | usr = append(usr, id) 38 | } 39 | 40 | return usr, nil 41 | } 42 | 43 | func (a *Api) UpdateMembers(groupName string, users []string) error{ 44 | var userList string = strings.Join(users, ",") 45 | ugId, err := a.groupId(groupName) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | _, err = a.api.UpdateUserGroupMembers(ugId, userList) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /updater/updateschedules.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "sync" 5 | log "github.com/Sirupsen/logrus" 6 | "github.com/yosmudge/pagerbot/config" 7 | ) 8 | 9 | // Updates the schedules from Pagerduty, check that all schedules listed 10 | // in config exist 11 | func (u *Updater) updateSchedules(w *sync.WaitGroup){ 12 | defer w.Done() 13 | pdSchedules, err := u.Pagerduty.Schedules() 14 | if err != nil { 15 | log.WithFields(log.Fields{ 16 | "error": err, 17 | }).Warning("Could not fetch schedules from Pagerduty") 18 | return 19 | } 20 | 21 | var schds ScheduleList 22 | for i,_ := range pdSchedules{ 23 | schds.schedules = append(schds.schedules, &pdSchedules[i]) 24 | } 25 | 26 | u.Schedules = &schds 27 | 28 | for _,group := range config.Config.Groups{ 29 | for _,schdId := range group.Schedules{ 30 | s := u.Schedules.ById(schdId) 31 | if s == nil || s.Id == ""{ 32 | log.WithFields(log.Fields{ 33 | "scheduleId": schdId, 34 | "group": group.Name, 35 | }).Warning("Could not find schedule specified in group") 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "os" 5 | log "github.com/Sirupsen/logrus" 6 | "github.com/voxelbrain/goptions" 7 | "github.com/yosmudge/pagerbot/config" 8 | "github.com/yosmudge/pagerbot/updater" 9 | ) 10 | 11 | type options struct { 12 | Verbose bool `goptions:"-v, --verbose, description='Log verbosely'"` 13 | Help goptions.Help `goptions:"-h, --help, description='Show help'"` 14 | Config string `goptions:"-c, --config, description='Config Yaml file to use'"` 15 | } 16 | 17 | func main() { 18 | parsedOptions := options{} 19 | 20 | parsedOptions.Config = "./config.yml" 21 | 22 | goptions.ParseAndFail(&parsedOptions) 23 | 24 | if parsedOptions.Verbose{ 25 | log.SetLevel(log.DebugLevel) 26 | } else { 27 | log.SetLevel(log.InfoLevel) 28 | } 29 | 30 | log.SetFormatter(&log.TextFormatter{FullTimestamp:true}) 31 | 32 | log.Debug("Logging verbosely!") 33 | 34 | err := config.Load(parsedOptions.Config) 35 | if err == nil { 36 | err = config.Config.Validate() 37 | } 38 | 39 | if err != nil{ 40 | log.WithFields(log.Fields{ 41 | "configFile": parsedOptions.Config, 42 | "error": err, 43 | }).Error("Could not load config file") 44 | os.Exit(1) 45 | } 46 | 47 | u, err := updater.New() 48 | if err != nil { 49 | log.WithFields(log.Fields{ 50 | "error": err, 51 | }).Error("Could not start updater") 52 | os.Exit(1) 53 | } 54 | 55 | u.Start() 56 | u.Wg.Wait() 57 | } 58 | -------------------------------------------------------------------------------- /updater/updateusers.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "sync" 5 | "github.com/nlopes/slack" 6 | log "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | // Fetch the users from Pagerduty and slack, and make sure we can match them 10 | // all up. We match Pagerduty users to Slack users based on their email address 11 | func (u *Updater) updateUsers(w *sync.WaitGroup){ 12 | defer w.Done() 13 | 14 | var err error 15 | pdUsers, err := u.Pagerduty.Users() 16 | if err != nil { 17 | log.WithFields(log.Fields{ 18 | "error": err, 19 | }).Warning("Could not fetch users from Pagerduty") 20 | return 21 | } 22 | 23 | slackUsers, err := u.Slack.Users() 24 | if err != nil { 25 | log.WithFields(log.Fields{ 26 | "error": err, 27 | }).Warning("Could not fetch users from Slack") 28 | return 29 | } 30 | 31 | // Create a map of slack email -> user for searching 32 | slackUserMap := make(map[string]*slack.User) 33 | for i,_ := range slackUsers{ 34 | u := slackUsers[i] 35 | slackUserMap[u.Profile.Email] = &u 36 | } 37 | 38 | var users UserList 39 | for _,u := range pdUsers{ 40 | su, found := slackUserMap[u.Email] 41 | if !found{ 42 | log.WithFields(log.Fields{ 43 | "email": u.Email, 44 | "pagerdutyId": u.Id, 45 | }).Warning("Could not find Slack account for Pagerduty user") 46 | continue 47 | } 48 | 49 | usr := User{} 50 | usr.Name = u.Name 51 | usr.SlackId = su.ID 52 | usr.PagerdutyId = u.Id 53 | usr.SlackName = su.Name 54 | usr.Email = u.Email 55 | users.users = append(users.users, &usr) 56 | } 57 | 58 | u.Users = &users 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PagerBot 2 | 3 | Update your Slack user groups based on your PagerDuty Schedules. 4 | 5 | At [Songkick](https://www.songkick.com/), we use PagerDuty for managing our on call schedules. We also have a Slack user group pointing to the people currently on call, so anyone can ping them to alert them of any problems. But updating those user groups every week is both slightly boring, and easy to forget. So when you're working with two services that have APIs, why not automate it? 6 | 7 | PagerBot is a simple program to do this. Provided with your PagerDuty and Slack API credentials, and some simple configuration, it will update the usergroups automatically, as well as posting a message to channels you select informing everyone who's currently on the rota. 8 | 9 | # Installation 10 | 11 | Install the dependencies with `Glide` 12 | 13 | `glide install` 14 | 15 | Then build 16 | 17 | `go build` 18 | 19 | You should have a nice `pagerbot` binary ready to go. You can also download prebuild binaries from the [releases](https://github.com/YoSmudge/pagerbot/releases) page. 20 | 21 | # Configuration 22 | 23 | A basic configuration file will look like 24 | 25 | ```yaml 26 | api_keys: 27 | slack: "abcd123" 28 | pagerduty: 29 | org: "songkick" 30 | key: "qwerty567" 31 | 32 | groups: 33 | - name: firefighter 34 | schedules: 35 | - PAAAAAA 36 | - PBBBBBB 37 | update_message: 38 | message: ":fire_engine: Your firefighters are %s :fire_engine:" 39 | channels: 40 | - general 41 | - name: fielder 42 | schedules: 43 | - PCCCCCC 44 | update_message: 45 | message: "Your :baseball: TechOps @Fielder :baseball: this week is %s" 46 | channels: 47 | - team-engineering 48 | ``` 49 | 50 | The configuration should be fairly straightforward, under API keys provide your Slack and Pagerduty keys. Under groups configure the Slack groups you'd like to update. Schedules is a list of PagerDuty schedule IDs, update_message is the message you'd like to post, and the channels you'd like to post them in. 51 | 52 | Once done, you can run PagerBot with `./pagerbot --config /path/to/config.yml` 53 | 54 | It's recommended to run PagerBot under Upstart or some other process manager. 55 | 56 | N.B. PagerBot matches PagerDuty users to Slack users by their email addresses, so your users must have the same email address in Slack as in PagerDuty. PagerBot will log warnings for any users it finds in PagerDuty but not in Slack. 57 | -------------------------------------------------------------------------------- /pagerduty/schedules.go: -------------------------------------------------------------------------------- 1 | package pagerduty 2 | 3 | import( 4 | "fmt" 5 | "time" 6 | log "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | type Schedules []Schedule 10 | 11 | type Schedule struct{ 12 | Id string 13 | Name string 14 | Timezone string `json:"time_zone"` 15 | CurrentPeriod *CallPeriod 16 | NextPeriod *CallPeriod 17 | } 18 | 19 | type CallPeriod struct{ 20 | Start time.Time 21 | User string 22 | } 23 | 24 | // Fetch the main schedule list then the details about specific schedules 25 | func (a *Api) Schedules() (Schedules, error){ 26 | var schdList Schedules 27 | 28 | err := a.requestThing("schedules", &schdList) 29 | if err != nil { 30 | return schdList, err 31 | } 32 | 33 | var today string = time.Now().UTC().Format("2006-01-02") 34 | var nextWeek string = time.Now().UTC().Add(time.Hour*24*7).Format("2006-01-02") 35 | 36 | for i,_ := range schdList{ 37 | schd := &schdList[i] 38 | rsp, err := a.request(fmt.Sprintf("schedules/%s/entries?since=%s&until=%s&time_zone=%s&overflow=true", schd.Id, today, nextWeek, a.timezone)) 39 | if err != nil { 40 | return schdList, err 41 | } 42 | 43 | var schdInfo struct{ 44 | Entries []struct{ 45 | User struct{ 46 | Id string 47 | } 48 | Start time.Time 49 | End time.Time 50 | } 51 | } 52 | 53 | rsp.Unmarshal(&schdInfo) 54 | 55 | var activeEntries int 56 | for _,se := range schdInfo.Entries{ 57 | if se.Start.Before(time.Now().UTC()) && se.End.After(time.Now().UTC()){ 58 | if activeEntries == 0{ 59 | p := CallPeriod{} 60 | p.Start = se.Start 61 | p.User = se.User.Id 62 | schd.CurrentPeriod = &p 63 | } 64 | activeEntries += 1 65 | } 66 | 67 | if se.Start.After(time.Now().UTC()) && (schd.NextPeriod == nil || se.Start.Before(schd.NextPeriod.Start)){ 68 | p := CallPeriod{} 69 | p.Start = se.Start 70 | p.User = se.User.Id 71 | schd.NextPeriod = &p 72 | } 73 | } 74 | 75 | lf := log.Fields{ 76 | "id": schd.Id, 77 | } 78 | 79 | if schd.CurrentPeriod == nil{ 80 | log.WithFields(lf).Warning("No active current period for schedule") 81 | } else { 82 | lf["currentCall"] = schd.CurrentPeriod.User 83 | } 84 | 85 | if schd.NextPeriod == nil{ 86 | log.WithFields(lf).Warning("No active next period for schedule") 87 | } else { 88 | lf["nextCall"] = schd.NextPeriod.User 89 | lf["changeover"] = schd.NextPeriod.Start 90 | } 91 | 92 | if activeEntries > 1{ 93 | log.WithFields(lf).Warning("Multiple active schedules") 94 | } 95 | log.WithFields(lf).Debug("Got schedule entries") 96 | } 97 | 98 | return schdList, nil 99 | } 100 | -------------------------------------------------------------------------------- /pagerduty/request.go: -------------------------------------------------------------------------------- 1 | package pagerduty 2 | 3 | import( 4 | "fmt" 5 | "sync" 6 | "time" 7 | "net/url" 8 | "encoding/json" 9 | log "github.com/Sirupsen/logrus" 10 | "gopkg.in/jmcvetta/napping.v3" 11 | ) 12 | 13 | type response interface{ 14 | Add(interface{}) 15 | } 16 | 17 | type pagination struct{ 18 | Limit int 19 | Offset int 20 | Total int 21 | } 22 | 23 | // Request a list of items (users, schedules etc.) and paginate 24 | // Pagerduty has a weird response format so we make the assumption that the 25 | // data we want is the same key as the URL (I.E. schedules, users etc.) 26 | // There is a super weird JSON hack in here to decode the responses properly 27 | func (a *Api) requestThing(thing string, r interface{}) error{ 28 | var err error 29 | var items []interface{} 30 | 31 | var page int = 0 32 | var perPage int = 25 33 | 34 | for{ 35 | q := url.Values{} 36 | q.Set("limit", fmt.Sprintf("%d", perPage)) 37 | q.Set("offset", fmt.Sprintf("%d", page*perPage)) 38 | 39 | path := fmt.Sprintf("%s?%s", thing, q.Encode()) 40 | 41 | var resp *napping.Response 42 | resp, err = a.request(path) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | var pages pagination 48 | resp.Unmarshal(&pages) 49 | 50 | var rawResponse map[string]interface{} 51 | resp.Unmarshal(&rawResponse) 52 | 53 | for _,i := range rawResponse[thing].([]interface{}){ 54 | items = append(items, i) 55 | } 56 | 57 | if len(items) == pages.Total{ 58 | break 59 | } 60 | 61 | page += 1 62 | } 63 | 64 | itemsEncoded, _ := json.Marshal(&items) 65 | json.Unmarshal(itemsEncoded, &r) 66 | // Told you 67 | 68 | return nil 69 | } 70 | 71 | var requestLock sync.Mutex 72 | const requestPause time.Duration = 2*time.Second 73 | var limiter <-chan time.Time = time.Tick(requestPause) 74 | 75 | // Send request to Pagerduty 76 | // Ensures requests are no more frequent than `requestPause` to permit Goroutines calling without hitting rate limit 77 | func (a *Api) request(path string) (*napping.Response, error){ 78 | requestLock.Lock() 79 | defer func(){ 80 | go func(){ 81 | <- limiter 82 | requestLock.Unlock() 83 | }() 84 | }() 85 | 86 | var resp *napping.Response 87 | var err error 88 | var u url.URL 89 | 90 | u.Host = fmt.Sprintf("%s.pagerduty.com", a.org) 91 | u.Scheme = "https" 92 | 93 | fullPath := fmt.Sprintf("%s/api/v1/%s", u.String(), path) 94 | 95 | resp, err = a.http.Get(fullPath, &url.Values{}, nil, nil) 96 | if err != nil || resp.Status() != 200 { 97 | fields := log.Fields{ 98 | "url": fullPath, 99 | "error": err, 100 | } 101 | var status int 102 | if err == nil{ 103 | fields["status"] = resp.Status() 104 | status = resp.Status() 105 | } 106 | log.WithFields(fields).Warning("Error from Pagerduty") 107 | 108 | return resp, fmt.Errorf("Got error from Pagerduty: %d (%s)", status, err) 109 | } 110 | 111 | log.WithFields(log.Fields{ 112 | "url": fullPath, 113 | "status": resp.Status(), 114 | }).Debug("Pagerduty request") 115 | 116 | return resp, nil 117 | } 118 | -------------------------------------------------------------------------------- /updater/updateGroups.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import( 4 | "fmt" 5 | "time" 6 | "sort" 7 | "strings" 8 | "reflect" 9 | log "github.com/Sirupsen/logrus" 10 | "github.com/yosmudge/pagerbot/config" 11 | "github.com/yosmudge/pagerbot/pagerduty" 12 | ) 13 | 14 | // Ensure all the slack groups are up to date 15 | func (u *Updater) updateGroups(){ 16 | for _,group := range config.Config.Groups{ 17 | lf := log.Fields{ 18 | "group": group.Name, 19 | } 20 | 21 | var currentUsers []*User 22 | var changeover time.Time 23 | for _,s := range group.Schedules{ 24 | lf["scheduleId"] = s 25 | schd := u.Schedules.ById(s) 26 | if schd == nil{ 27 | log.WithFields(lf).Warning("Could not find schedule with ID") 28 | continue 29 | } 30 | 31 | var activePeriod *pagerduty.CallPeriod 32 | 33 | if schd.NextPeriod != nil{ 34 | if changeover.IsZero() || schd.NextPeriod.Start.Before(changeover){ 35 | changeover = schd.NextPeriod.Start 36 | } 37 | } 38 | 39 | if !changeover.IsZero() && time.Now().UTC().After(changeover){ 40 | activePeriod = schd.NextPeriod 41 | } else if schd.CurrentPeriod != nil{ 42 | activePeriod = schd.CurrentPeriod 43 | } 44 | 45 | if activePeriod != nil{ 46 | lf["userId"] = activePeriod.User 47 | usr := u.Users.ById(activePeriod.User) 48 | if usr == nil{ 49 | log.WithFields(lf).Warning("Could not find user with ID") 50 | continue 51 | } 52 | currentUsers = append(currentUsers, usr) 53 | } 54 | } 55 | 56 | lf["scheduleId"] = nil 57 | lf["userId"] = nil 58 | lf["changeover"] = changeover 59 | 60 | var pdUsers []string 61 | var slackUsers []string 62 | var userNames []string 63 | 64 | for _,u := range currentUsers{ 65 | pdUsers = append(pdUsers, u.PagerdutyId) 66 | slackUsers = append(slackUsers, u.SlackId) 67 | userNames = append(userNames, fmt.Sprintf("@%s", u.SlackName)) 68 | } 69 | 70 | lf["pdUsers"] = pdUsers 71 | lf["slackUsers"] = slackUsers 72 | 73 | currentMembers, err := u.Slack.GroupMembers(group.Name) 74 | if err != nil { 75 | lf["err"] = err 76 | log.WithFields(lf).Warning("Could not get Slack group members") 77 | continue 78 | } 79 | 80 | lf["currentMembers"] = currentMembers 81 | log.WithFields(lf).Debug("Group status") 82 | sort.Strings(currentMembers) 83 | sort.Strings(slackUsers) 84 | if !reflect.DeepEqual(currentMembers, slackUsers){ 85 | err := u.Slack.UpdateMembers(group.Name, slackUsers) 86 | if err != nil { 87 | lf["err"] = err 88 | log.WithFields(lf).Warning("Could not update Slack group members") 89 | continue 90 | } 91 | log.WithFields(lf).Info("Updating group members") 92 | 93 | var userList string 94 | if len(userNames) > 1{ 95 | userList = strings.Join(userNames[:len(userNames)-1], ", ") 96 | } 97 | 98 | if len(userNames) > 1{ 99 | userList = fmt.Sprintf("%s & %s", userList, userNames[len(userNames)-1]) 100 | } else { 101 | userList = userNames[0] 102 | } 103 | 104 | msgText := fmt.Sprintf(group.UpdateMessage.Message, userList) 105 | for _,c := range group.UpdateMessage.Channels{ 106 | u.Slack.PostMessage(c, msgText) 107 | } 108 | } 109 | } 110 | } 111 | --------------------------------------------------------------------------------