slack: mark channels read, keep a current marker

This commit is contained in:
cws 2017-07-25 13:58:04 -04:00
parent 760ee1ca94
commit 755cfc38cd
3 changed files with 205 additions and 45 deletions

View File

@ -7,11 +7,9 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"regexp"
"strings" "strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/msglog" "github.com/velour/catbase/bot/msglog"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
@ -54,25 +52,8 @@ type Variable struct {
Variable, Value string Variable, Value string
} }
func init() {
regex := func(re, s string) (bool, error) {
return regexp.MatchString(re, s)
}
sql.Register("sqlite3_custom",
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
return conn.RegisterFunc("REGEXP", regex, true)
},
})
}
// Newbot creates a bot for a given connection and set of handlers. // Newbot creates a bot for a given connection and set of handlers.
func New(config *config.Config, connector Connector) Bot { func New(config *config.Config, connector Connector) Bot {
sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File)
if err != nil {
log.Fatal(err)
}
logIn := make(chan msg.Message) logIn := make(chan msg.Message)
logOut := make(chan msg.Messages) logOut := make(chan msg.Messages)
@ -91,7 +72,7 @@ func New(config *config.Config, connector Connector) Bot {
conn: connector, conn: connector,
users: users, users: users,
me: users[0], me: users[0],
db: sqlDB, db: config.DBConn,
logIn: logIn, logIn: logIn,
logOut: logOut, logOut: logOut,
version: config.Version, version: config.Version,

View File

@ -2,13 +2,23 @@
package config package config
import "encoding/json" import (
import "fmt" "database/sql"
import "io/ioutil" "encoding/json"
"fmt"
"io/ioutil"
"log"
"regexp"
"github.com/jmoiron/sqlx"
sqlite3 "github.com/mattn/go-sqlite3"
)
// Config stores any system-wide startup information that cannot be easily configured via // Config stores any system-wide startup information that cannot be easily configured via
// the database // the database
type Config struct { type Config struct {
DBConn *sqlx.DB
DB struct { DB struct {
File string File string
Name string Name string
@ -81,6 +91,18 @@ type Config struct {
} }
} }
func init() {
regex := func(re, s string) (bool, error) {
return regexp.MatchString(re, s)
}
sql.Register("sqlite3_custom",
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
return conn.RegisterFunc("REGEXP", regex, true)
},
})
}
// Readconfig loads the config data out of a JSON file located in cfile // Readconfig loads the config data out of a JSON file located in cfile
func Readconfig(version, cfile string) *Config { func Readconfig(version, cfile string) *Config {
fmt.Printf("Using %s as config file.\n", cfile) fmt.Printf("Using %s as config file.\n", cfile)
@ -102,5 +124,11 @@ func Readconfig(version, cfile string) *Config {
fmt.Printf("godeepintir version %s running.\n", c.Version) fmt.Printf("godeepintir version %s running.\n", c.Version)
sqlDB, err := sqlx.Open("sqlite3_custom", c.DB.File)
if err != nil {
log.Fatal(err)
}
c.DBConn = sqlDB
return &c return &c
} }

View File

@ -32,6 +32,8 @@ type Slack struct {
id string id string
ws *websocket.Conn ws *websocket.Conn
lastRecieved time.Time
users map[string]string users map[string]string
emoji map[string]string emoji map[string]string
@ -50,22 +52,74 @@ type slackUserInfoResp struct {
} `json:"user"` } `json:"user"`
} }
type slackChannelListItem struct {
ID string `json:"id"`
Name string `json:"name"`
IsChannel bool `json:"is_channel"`
Created int `json:"created"`
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
IsGeneral bool `json:"is_general"`
NameNormalized string `json:"name_normalized"`
IsShared bool `json:"is_shared"`
IsOrgShared bool `json:"is_org_shared"`
IsMember bool `json:"is_member"`
Members []string `json:"members"`
Topic struct {
Value string `json:"value"`
Creator string `json:"creator"`
LastSet int `json:"last_set"`
} `json:"topic"`
Purpose struct {
Value string `json:"value"`
Creator string `json:"creator"`
LastSet int `json:"last_set"`
} `json:"purpose"`
PreviousNames []interface{} `json:"previous_names"`
NumMembers int `json:"num_members"`
}
type slackChannelListResp struct {
Ok bool `json:"ok"`
Channels []slackChannelListItem `json:"channels"`
}
type slackChannelInfoResp struct { type slackChannelInfoResp struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Channel struct { Channel struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
IsChannel bool `json:"is_channel"`
Created int64 `json:"created"` Created int `json:"created"`
Creator string `json:"creator"` Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
Members []string `json:"members"` IsGeneral bool `json:"is_general"`
NameNormalized string `json:"name_normalized"`
Topic struct { IsReadOnly bool `json:"is_read_only"`
IsShared bool `json:"is_shared"`
IsOrgShared bool `json:"is_org_shared"`
IsMember bool `json:"is_member"`
LastRead string `json:"last_read"`
Latest struct {
Type string `json:"type"`
User string `json:"user"`
Text string `json:"text"`
Ts string `json:"ts"`
} `json:"latest"`
UnreadCount int `json:"unread_count"`
UnreadCountDisplay int `json:"unread_count_display"`
Members []string `json:"members"`
Topic struct {
Value string `json:"value"` Value string `json:"value"`
Creator string `json:"creator"` Creator string `json:"creator"`
LastSet int64 `json:"last_set"` LastSet int64 `json:"last_set"`
} `json:"topic"` } `json:"topic"`
Purpose struct {
Value string `json:"value"`
Creator string `json:"creator"`
LastSet int `json:"last_set"`
} `json:"purpose"`
PreviousNames []string `json:"previous_names"`
} `json:"channel"` } `json:"channel"`
} }
@ -102,9 +156,10 @@ type rtmStart struct {
func New(c *config.Config) *Slack { func New(c *config.Config) *Slack {
return &Slack{ return &Slack{
config: c, config: c,
users: make(map[string]string), lastRecieved: time.Now(),
emoji: make(map[string]string), users: make(map[string]string),
emoji: make(map[string]string),
} }
} }
@ -194,11 +249,18 @@ func (s *Slack) receiveMessage() (slackMessage, error) {
log.Println("Error decoding WS message") log.Println("Error decoding WS message")
return m, err return m, err
} }
log.Printf("Raw response from Slack: %s", msg)
err2 := json.Unmarshal(msg, &m) err2 := json.Unmarshal(msg, &m)
return m, err2 return m, err2
} }
// I think it's horseshit that I have to do this
func slackTStoTime(t string) time.Time {
ts := strings.Split(t, ".")
sec, _ := strconv.ParseInt(ts[0], 10, 64)
nsec, _ := strconv.ParseInt(ts[1], 10, 64)
return time.Unix(sec, nsec)
}
func (s *Slack) Serve() { func (s *Slack) Serve() {
s.connect() s.connect()
s.populateEmojiList() s.populateEmojiList()
@ -212,9 +274,14 @@ func (s *Slack) Serve() {
} }
switch msg.Type { switch msg.Type {
case "message": case "message":
log.Printf("msg: %+v", msg)
if !msg.Hidden { if !msg.Hidden {
s.messageReceived(s.buildMessage(msg)) m := s.buildMessage(msg)
if m.Time.Before(s.lastRecieved) {
log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time)
} else {
s.lastRecieved = m.Time
s.messageReceived(s.buildMessage(msg))
}
} else { } else {
log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID)
} }
@ -225,7 +292,9 @@ func (s *Slack) Serve() {
case "presence_change": case "presence_change":
case "user_typing": case "user_typing":
case "reconnect_url": case "reconnect_url":
case "desktop_notification":
// squeltch this stuff // squeltch this stuff
continue
default: default:
log.Printf("Unhandled Slack message type: '%s'", msg.Type) log.Printf("Unhandled Slack message type: '%s'", msg.Type)
} }
@ -236,7 +305,6 @@ var urlDetector = regexp.MustCompile(`<(.+)://([^|^>]+).*>`)
// Convert a slackMessage to a msg.Message // Convert a slackMessage to a msg.Message
func (s *Slack) buildMessage(m slackMessage) msg.Message { func (s *Slack) buildMessage(m slackMessage) msg.Message {
log.Printf("DEBUG: msg: %#v", m)
text := html.UnescapeString(m.Text) text := html.UnescapeString(m.Text)
// remove <> from URLs, URLs may also be <url|description> // remove <> from URLs, URLs may also be <url|description>
@ -254,11 +322,7 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message {
u = m.Username u = m.Username
} }
// I think it's horseshit that I have to do this tstamp := slackTStoTime(m.Ts)
ts := strings.Split(m.Ts, ".")
sec, _ := strconv.ParseInt(ts[0], 10, 64)
nsec, _ := strconv.ParseInt(ts[1], 10, 64)
tstamp := time.Unix(sec, nsec)
return msg.Message{ return msg.Message{
User: &user.User{ User: &user.User{
@ -278,9 +342,94 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message {
} }
} }
// markAllChannelsRead gets a list of all channels and marks each as read
func (s *Slack) markAllChannelsRead() {
chs := s.getAllChannels()
log.Printf("Got list of channels to mark read: %+v", chs)
for _, ch := range chs {
s.markChannelAsRead(ch.ID)
}
log.Printf("Finished marking channels read")
}
// getAllChannels returns info for all channels joined
func (s *Slack) getAllChannels() []slackChannelListItem {
u := s.url + "channels.list"
resp, err := http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}})
if err != nil {
log.Printf("Error posting user info request: %s",
err)
return nil
}
if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d",
resp.StatusCode)
return nil
}
defer resp.Body.Close()
var chanInfo slackChannelListResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo)
if err != nil || !chanInfo.Ok {
log.Println("Error decoding response: ", err)
return nil
}
return chanInfo.Channels
}
// markAsRead marks a channel read
func (s *Slack) markChannelAsRead(slackChanId string) error {
u := s.url + "channels.info"
resp, err := http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}})
if err != nil {
log.Printf("Error posting user info request: %s",
err)
return err
}
if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d",
resp.StatusCode)
return err
}
defer resp.Body.Close()
var chanInfo slackChannelInfoResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo)
log.Printf("%+v, %+v", err, chanInfo)
if err != nil || !chanInfo.Ok {
log.Println("Error decoding response: ", err)
return err
}
u = s.url + "channels.mark"
resp, err = http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}, "ts": {chanInfo.Channel.Latest.Ts}})
if err != nil {
log.Printf("Error posting user info request: %s",
err)
return err
}
if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d",
resp.StatusCode)
return err
}
defer resp.Body.Close()
var markInfo map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&markInfo)
log.Printf("%+v, %+v", err, markInfo)
if err != nil {
log.Println("Error decoding response: ", err)
return err
}
log.Printf("Marked %s as read", slackChanId)
return nil
}
func (s *Slack) connect() { func (s *Slack) connect() {
token := s.config.Slack.Token token := s.config.Slack.Token
url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token) url := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return return
@ -306,6 +455,8 @@ func (s *Slack) connect() {
s.url = "https://slack.com/api/" s.url = "https://slack.com/api/"
s.id = rtm.Self.ID s.id = rtm.Self.ID
s.markAllChannelsRead()
s.ws, err = websocket.Dial(rtm.URL, "", s.url) s.ws, err = websocket.Dial(rtm.URL, "", s.url)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)