func (c *Client) registerOnBotmetrics() { shouldRegister := true key := fmt.Sprintf("botmetrics:%s", c.TeamId) redisClient := redisclient.Client() boolCmd := redisClient.HSetNX(os.Getenv("RELAX_MUTEX_KEY"), key, "ok") if boolCmd != nil { shouldRegister = boolCmd.Val() } if shouldRegister { bm, err := botmetrics.NewBotmetricsClient() if err == nil { status := bm.RegisterBot(c.Token, -1) if status { log.WithFields(log.Fields{ "teamId": c.TeamId, }).Info("registered on botmetrics") } else { log.WithFields(log.Fields{ "teamId": c.TeamId, }).Error("error registering on botmetrics") } } } }
// InitClients initializes all clients which are present in Redis on boot. // This looks for all hashes in the key RELAX_BOTS_KEY and calls NewClient for each // hash present in the redis key. It also starts a loop to listen to REDIS_BOTS_PUBSUB // when new clients need to be started. It listens to Redis on pubsub instead of a queue // because there can be multiple instances of Relax running and they all need to start // a Slack client. func InitClients() { redisClient := redisclient.Client() resultCmd := redisClient.HGetAll(os.Getenv("RELAX_BOTS_KEY")) result := resultCmd.Val() for i := 0; i < len(result); i += 2 { key := result[i] val := result[i+1] c, err := NewClient(val) if err != nil { log.WithFields(log.Fields{ "team": val, "error": err, }).Error("starting slack client") } else { go c.LoginAndStart() Clients[key] = c } } go startReadFromRedisPubSubLoop() }
// Start starts a websocket connection to Slack's servers and starts listening for messages // If it detects an "invalid_auth" or "inactive_account" message, it means that the token provided by the user // has expired or is incorrect and so it sends a "disable_bot" event back to the user // so that they can take remedial action. func (c *Client) Start() error { // Make connection to redis now c.redisClient = redisclient.Client() if c.data.Ok == true { conn, _, err := websocket.DefaultDialer.Dial(c.data.Url, http.Header{}) if err != nil { log.WithFields(log.Fields{ "team": c.TeamId, "error": err, }).Error("dialing connection to Slack websocket") return err } else { c.conn = conn } go c.startReadFromSlackLoop() } else { // Bot has been disabled by the user, // so we need to mark it as disabled if c.data.Error == "invalid_auth" || c.data.Error == "account_inactive" { var msg Message msg.User = User{} msg.Channel = Channel{} err := c.sendEvent("disable_bot", &msg, "", "", "") if err != nil { return err } } log.WithFields(log.Fields{ "team": c.TeamId, "error": c.data.Error, }).Error("starting slack client") return fmt.Errorf(fmt.Sprintf("error connecting to slack websocket server: %s", c.data.Error)) } return nil }
// startReadFromRedisPubSubLoop is the method invoked by InitClients that listens for new // clients that need to be started via a Redis Pubsub channel. func startReadFromRedisPubSubLoop() { redisClient := redisclient.Client() pubsub := redisClient.PubSub() defer pubsub.Close() pubsubChannel := os.Getenv("RELAX_BOTS_PUBSUB") err := pubsub.Subscribe(pubsubChannel) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("error subscribing to redis pubsub") return } for { msgi, err := pubsub.ReceiveTimeout(100 * time.Millisecond) if err != nil { if neterr, ok := err.(net.Error); ok && neterr.Timeout() { continue } } else { switch msg := msgi.(type) { case *redis.Message: if msg.Channel != pubsubChannel { break } payload := msg.Payload var cmd Command if err := json.Unmarshal([]byte(payload), &cmd); err != nil { break } switch cmd.Type { case "message": shouldSend := true var key string var c *Client if cmd.TeamId == "" { break } // if Id is not present, then populate an ID if cmd.Id == "" { cmd.Id = fmt.Sprintf("%d", time.Now().Nanosecond()) } if cmd.Namespace == "" { key = cmd.TeamId } else { key = fmt.Sprintf("%s-%s", cmd.Namespace, cmd.TeamId) } if _c, ok := Clients.Get(key); ok { c = _c.(*Client) } if c != nil && c.conn != nil { key := fmt.Sprintf("send_slack_message:%s", cmd.Id) boolCmd := redisClient.HSetNX(os.Getenv("RELAX_MUTEX_KEY"), key, "ok") if boolCmd != nil { shouldSend = boolCmd.Val() } if shouldSend { c.conn.WriteMessage(websocket.TextMessage, []byte(cmd.Payload)) log.WithFields(log.Fields{ "team": cmd.TeamId, "command_id": cmd.Id, }).Debug("sent message to slack") } else { log.WithFields(log.Fields{ "team": cmd.TeamId, "command_id": cmd.Id, }).Debug("ignoring, not sending message to slack") } } case "team_added": var key string var c *Client if cmd.TeamId == "" { break } if cmd.Namespace == "" { key = cmd.TeamId } else { key = fmt.Sprintf("%s-%s", cmd.Namespace, cmd.TeamId) } result := redisClient.HGet(os.Getenv("RELAX_BOTS_KEY"), key) if result == nil { break } val := result.Val() if _c, ok := Clients.Get(key); ok { c = _c.(*Client) } if c != nil { err := c.Stop() if err != nil { log.WithFields(log.Fields{ "team": cmd.TeamId, "error": err, }).Error("closing websocket connection") } } c, err := NewClient(val) if err == nil { c.LoginAndStart() } else { log.WithFields(log.Fields{ "team": cmd.TeamId, "error": err, }).Error("starting client") } case "team_removed": var key string var c *Client if cmd.TeamId == "" { break } if cmd.Namespace == "" { key = cmd.TeamId } else { key = fmt.Sprintf("%s-%s", cmd.Namespace, cmd.TeamId) } result := redisClient.HGet(os.Getenv("RELAX_BOTS_KEY"), key) if result == nil { break } if _c, ok := Clients.Get(key); ok { c = _c.(*Client) } if c != nil { err := c.Stop() if err != nil { log.WithFields(log.Fields{ "team": cmd.TeamId, "error": err, }).Error("closing websocket connection") } Clients.Remove(key) } } } } } }
// Start starts a websocket connection to Slack's servers and starts listening for messages // If it detects an "invalid_auth" or "inactive_account" message, it means that the token provided by the user // has expired or is incorrect and so it sends a "disable_bot" event back to the user // so that they can take remedial action. func (c *Client) Start() error { var key string // Make connection to redis now c.redisClient = redisclient.Client() if c.data.Ok == true { conn, _, err := websocket.DefaultDialer.Dial(c.data.Url, http.Header{}) if err != nil { log.WithFields(log.Fields{ "team": c.TeamId, "error": err, }).Error("dialing connection to Slack websocket") return err } else { c.conn = conn } c.pingTicker = time.NewTicker(time.Millisecond * 5000) go c.startReadFromSlackLoop() go c.startPingPump() } else { // Bot has been disabled by the user, // so we need to mark it as disabled if c.data.Error == "invalid_auth" || c.data.Error == "account_inactive" { var msg Message msg.User = User{} msg.Channel = Channel{} err := c.sendEvent("disable_bot", &msg, "", "", "") if err != nil { return err } } else if c.data.Error == "migration_in_progress" { go c.LoginAndStart() return nil } log.WithFields(log.Fields{ "team": c.TeamId, "error": c.data.Error, }).Error("starting slack client") return fmt.Errorf(fmt.Sprintf("error connecting to slack websocket server: %s", c.data.Error)) } // This serves no real purpose other than to let tests know that a certain client has been initialized c.redisClient.HSet(os.Getenv("RELAX_MUTEX_KEY"), fmt.Sprintf("bot-%s-started", c.TeamId), fmt.Sprintf("%d", time.Now().Nanosecond())) if c.Namespace == "" { key = c.TeamId } else { key = fmt.Sprintf("%s-%s", c.Namespace, c.TeamId) } Clients.Set(key, c) return nil }
// startReadFromRedisPubSubLoop is the method invoked by InitClients that listens for new // clients that need to be started via a Redis Pubsub channel. func startReadFromRedisPubSubLoop() { redisClient := redisclient.Client() pubsub := redisClient.PubSub() defer pubsub.Close() pubsubChannel := os.Getenv("RELAX_BOTS_PUBSUB") err := pubsub.Subscribe(pubsubChannel) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("error subscribing to redis pubsub") return } for { msgi, err := pubsub.ReceiveTimeout(100 * time.Millisecond) if err != nil { if neterr, ok := err.(net.Error); ok && neterr.Timeout() { continue } } else { switch msg := msgi.(type) { case *redis.Message: if msg.Channel != pubsubChannel { break } payload := msg.Payload var cmd Command if err := json.Unmarshal([]byte(payload), &cmd); err != nil { break } switch cmd.Type { case "team_added": if cmd.TeamId == "" { break } result := redisClient.HGet(os.Getenv("RELAX_BOTS_KEY"), cmd.TeamId) if result == nil { break } val := result.Val() c := Clients[cmd.TeamId] if c != nil { err := c.Stop() if err != nil { log.WithFields(log.Fields{ "team": cmd.TeamId, "error": err, }).Error("closing websocket connection") } } c, err := NewClient(val) if err == nil { c.LoginAndStart() Clients[cmd.TeamId] = c } else { log.WithFields(log.Fields{ "team": cmd.TeamId, "error": err, }).Error("starting client") } } } } } }