commit fec3bad50e8efc44bd0dd3aeec53155d66807a53 Author: Zachary Seguin Date: Sun May 28 20:03:18 2017 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5870d95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cscsysbot +ENV diff --git a/ENV.sample b/ENV.sample new file mode 100644 index 0000000..d026b05 --- /dev/null +++ b/ENV.sample @@ -0,0 +1,27 @@ +# IRC Configuration +export IRC_SERVER=chat.freenode.net +export IRC_PORT=6697 +export IRC_CHANNELS="#channel" +export IRC_USER=user +export IRC_NICK=nick +export IRC_PASSWORD=password + +# Set this variable to enable debug +export IRC_DEBUG + +# Background Messages +export SYSCOM_CHANNELS="#channel" + +# Twitter Configuration +export TWITTER_CONSUMER_KEY="" +export TWITTER_CONSUMER_SECRET="" +export TWITTER_ACCESS_TOKEN="" +export TWITTER_ACCESS_SECRET="" +export TWITTER_USERS="UWNetworkAlert" + +# Uptime Robot +export UPTIME_ROBOT_API_KEY="" + +# UPS +export UPSES="" +export UPS_COMMUNITY_STRING="" diff --git a/cscsysbot.go b/cscsysbot.go new file mode 100644 index 0000000..0ccf25e --- /dev/null +++ b/cscsysbot.go @@ -0,0 +1,38 @@ +package main + +import ( + "os" + "strings" + "fmt" + + "github.com/go-chat-bot/bot/irc" + + // Plugins + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/hello" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/url" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/ups" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/member" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/club" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/ping" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/greetings" + + // Backgrounds tasks + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/background" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/tweets" + _ "git.uwaterloo.ca/csc/cscsysbot/plugins/uptimerobot" +) + +func main() { + _, debug := os.LookupEnv("IRC_DEBUG") + + irc.Run(&irc.Config{ + Server: fmt.Sprintf("%s:%s", os.Getenv("IRC_SERVER"), os.Getenv("IRC_PORT")), + Channels: strings.Split(os.Getenv("IRC_CHANNELS"), ","), + User: os.Getenv("IRC_USER"), + Nick: os.Getenv("IRC_NICK"), + Password: os.Getenv("IRC_PASSWORD"), + UseTLS: true, + TLSServerName: os.Getenv("IRC_SERVER"), + Debug: debug, + }) +} diff --git a/plugins/background/background.go b/plugins/background/background.go new file mode 100644 index 0000000..7373287 --- /dev/null +++ b/plugins/background/background.go @@ -0,0 +1,32 @@ +package background + +import ( + "os" + "strings" + + "github.com/go-chat-bot/bot" +) + +var Messages chan string = make(chan string, 100) +func processMessages(channel string) (string, error) { + select { + case message, _ := <- Messages: + return message, nil + default: + return "", nil + } +} + +func init() { + channels := strings.Split(os.Getenv("SYSCOM_CHANNELS"), ",") + + if len(channels) > 0 { + config := bot.PeriodicConfig{ + CronSpec: "@every 3s", + Channels: channels, + CmdFunc: processMessages, + } + bot.RegisterPeriodicCommand("background_messages", config) + } + +} diff --git a/plugins/club/club.go b/plugins/club/club.go new file mode 100644 index 0000000..54200e9 --- /dev/null +++ b/plugins/club/club.go @@ -0,0 +1,123 @@ +package club + +import ( + "crypto/tls" + "fmt" + "strconv" + "strings" + "sort" + + "git.uwaterloo.ca/csc/cscsysbot/utils" + + "github.com/go-chat-bot/bot" + "gopkg.in/ldap.v2" +) + +const ( + ldapServer = "ldap-master.csclub.uwaterloo.ca" +) + +type ByTerm []string +func (s ByTerm) Len() int { + return len(s) +} + +func (s ByTerm) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ByTerm) Less(i, j int) bool { + a := s[i] + b := s[j] + + ya, _ := strconv.ParseInt(a[1:5], 10, 32) + yb, _ := strconv.ParseInt(b[1:5], 10, 32) + + if (ya == yb) { + // w, s, f (happens to be in reverse order) + return a[0] > b[0] + } + + return ya < yb +} + +func club(command *bot.Cmd) (string, error) { + var lines []string + + if len(command.Args) != 1 { + return "An invalid number of arguments was provided. Usage is: !club userid", nil + } + + authorized, _ := utils.SyscomNicks() + if (!utils.InList(authorized, command.User.Nick)) { + return "Sorry, you are not authorized to request membership information from me.", nil + } + + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, 636), &tls.Config{ + ServerName: ldapServer, + }) + if err != nil { + return "", err + } + defer l.Close() + + req := ldap.NewSearchRequest("ou=People,dc=csclub,dc=uwaterloo,dc=ca", ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(uid=%s)(objectClass=club))", command.Args[0]), + []string{"*"}, nil) + + res, err := l.Search(req) + if err != nil { + return "", err + } + + if len(res.Entries) == 0 { + lines = append(lines, fmt.Sprintf("No clubs matched userid %q", command.Args[0])) + } else { + entry := res.Entries[0] + lines = append(lines, fmt.Sprintf("Club: %s (%s) -- %s", + entry.GetAttributeValue("uid"), + entry.GetAttributeValue("uidNumber"), + entry.GetAttributeValue("cn"))) + + lines = append(lines, fmt.Sprintf("Login Shell: %s, Home Directory: %s", entry.GetAttributeValue("loginShell"), entry.GetAttributeValue("homeDirectory"))) + } + + // Get members + req = ldap.NewSearchRequest("ou=Group,dc=csclub,dc=uwaterloo,dc=ca", ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(cn=%s)(objectClass=posixGroup))", command.Args[0]), + []string{"*"}, nil) + res, err = l.Search(req) + if err != nil { + return "", err + } + + if len(res.Entries) == 0 { + lines = append(lines, fmt.Sprintf("No groups matched name %q", command.Args[0])) + } else { + entry := res.Entries[0] + + lines = append(lines, fmt.Sprintf("Group: %s (%s)", entry.GetAttributeValue("cn"), entry.GetAttributeValue("gidNumber"))) + + members := entry.GetAttributeValues("uniqueMember") + memberUids := make([]string, len(members)) + for i, member := range members { + memberUids[i] = strings.Replace(strings.Replace(member, "uid=", "", -1), ",ou=People,dc=csclub,dc=uwaterloo,dc=ca", "", -1) + } + + smemberUids := sort.StringSlice(memberUids[0:]) + sort.Sort(smemberUids) + lines = append(lines, fmt.Sprintf("Members: %s", strings.Join(smemberUids, ", "))) + } + + return strings.Join(lines, "\n"), nil +} + +func init() { + bot.RegisterCommand( + "club", + "Prints club information", + "userid", + club) +} diff --git a/plugins/greetings/greetings.go b/plugins/greetings/greetings.go new file mode 100644 index 0000000..72df42c --- /dev/null +++ b/plugins/greetings/greetings.go @@ -0,0 +1,55 @@ +package greetings + +import ( + "fmt" + "os" + "strings" + + "git.uwaterloo.ca/csc/cscsysbot/utils/uptimerobot" + + "github.com/go-chat-bot/bot" +) + +func goodMorning(channel string) (string, error) { + var lines []string + + lines = append(lines, fmt.Sprintf("Good morning, %s!", channel)) + + // Get UptimeRobot status + monitors, err := uptimerobot.GetMonitors() + if err == nil { + statuses := make(map[uptimerobot.MonitorStatus]int) + for _, mon := range monitors.Monitors { + _, ok := statuses[mon.Status] + if !ok { + statuses[mon.Status] = 1 + } else { + statuses[mon.Status] += 1 + } + } + + var sts []string + for status, num := range statuses { + sts = append(sts, fmt.Sprintf("%d %s", num, strings.ToLower(status.String()))) + } + + lines = append(lines, fmt.Sprintf("Uptime Robot Monitors: %s", strings.Join(sts, ", "))) + } else { + lines = append(lines, "Unable to get Uptime Robot information") + } + + return strings.Join(lines, "\n"), nil +} + +func init() { + channels := strings.Split(os.Getenv("SYSCOM_CHANNELS"), ",") + + if len(channels) > 0 { + config := bot.PeriodicConfig{ + CronSpec: "0 0 8 * * *", + Channels: channels, + CmdFunc: goodMorning, + } + bot.RegisterPeriodicCommand("good_morning", config) + } +} diff --git a/plugins/hello/hello.go b/plugins/hello/hello.go new file mode 100644 index 0000000..eebcadf --- /dev/null +++ b/plugins/hello/hello.go @@ -0,0 +1,20 @@ +package hello + +import ( + "fmt" + + "github.com/go-chat-bot/bot" +) + +func hello(command *bot.Cmd) (msg string, err error) { + msg = fmt.Sprintf("Hello %s", command.User.Nick) + return +} + +func init() { + bot.RegisterCommand( + "hello", + "Sends a 'Hello' message to you on the channel.", + "", + hello) +} diff --git a/plugins/member/member.go b/plugins/member/member.go new file mode 100644 index 0000000..3346a01 --- /dev/null +++ b/plugins/member/member.go @@ -0,0 +1,134 @@ +package member + +import ( + "crypto/tls" + "fmt" + "sort" + "strconv" + "strings" + + "git.uwaterloo.ca/csc/cscsysbot/utils" + + "github.com/go-chat-bot/bot" + "gopkg.in/ldap.v2" +) + +const ( + ldapServer = "ldap-master.csclub.uwaterloo.ca" +) + +type ByTerm []string +func (s ByTerm) Len() int { + return len(s) +} + +func (s ByTerm) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ByTerm) Less(i, j int) bool { + a := s[i] + b := s[j] + + ya, _ := strconv.ParseInt(a[1:5], 10, 32) + yb, _ := strconv.ParseInt(b[1:5], 10, 32) + + if (ya == yb) { + // w, s, f (happens to be in reverse order) + return a[0] > b[0] + } + + return ya < yb +} + +func member(command *bot.Cmd) (string, error) { + var lines []string + + if len(command.Args) != 1 { + return "An invalid number of arguments was provided. Usage is: !member userid", nil + } + + authorized, _ := utils.SyscomNicks() + if (!utils.InList(authorized, command.User.Nick)) { + return "Sorry, you are not authorized to request membership information from me.", nil + } + + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, 636), &tls.Config{ + ServerName: ldapServer, + }) + if err != nil { + return "", err + } + defer l.Close() + + req := ldap.NewSearchRequest("ou=People,dc=csclub,dc=uwaterloo,dc=ca", ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(uid=%s)(objectClass=member))", command.Args[0]), + []string{"*"}, nil) + + res, err := l.Search(req) + if err != nil { + return "", err + } + + if len(res.Entries) == 0 { + return fmt.Sprintf("No members matched userid %q", command.Args[0]), nil + } + + entry := res.Entries[0] + lines = append(lines, fmt.Sprintf("%s (%s) -- %s", + entry.GetAttributeValue("uid"), + entry.GetAttributeValue("uidNumber"), + entry.GetAttributeValue("cn"))) + lines = append(lines, entry.GetAttributeValue("program")) + + positions := sort.StringSlice(entry.GetAttributeValues("position")[0:]) + positions.Sort() + if len(positions) > 0 { + lines = append(lines, fmt.Sprintf("Position: %s", strings.Join(positions, ", "))) + } + + // Get groups + groupReq := ldap.NewSearchRequest("ou=Group,dc=csclub,dc=uwaterloo,dc=ca", ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(uniqueMember=%s)(objectClass=group))", entry.DN), + []string{"*"}, nil) + groupRes, err := l.Search(groupReq) + if err != nil { + return "", err + } + if len(groupRes.Entries) > 0 { + groups := make([]string, len(groupRes.Entries)) + for i, entry := range groupRes.Entries { + groups[i] = entry.GetAttributeValue("cn") + } + + sgroups := sort.StringSlice(groups[0:]) + sort.Sort(sgroups) + lines = append(lines, fmt.Sprintf("Clubs/Groups: %s", strings.Join(sgroups, ", "))) + } + + terms := entry.GetAttributeValues("term") + sort.Sort(ByTerm(terms)) + if len(terms) > 0 { + lines = append(lines, fmt.Sprintf("Terms: %s", strings.Join(terms, ", "))) + } + + nonMemberTerms := entry.GetAttributeValues("nonMemberTerm") + sort.Sort(ByTerm(terms)) + if len(nonMemberTerms) > 0 { + lines = append(lines, fmt.Sprintf("Non Member Terms: %s", strings.Join(nonMemberTerms, ", "))) + } + + lines = append(lines, fmt.Sprintf("Login Shell: %s, Home Directory: %s", entry.GetAttributeValue("loginShell"), entry.GetAttributeValue("homeDirectory"))) + + return strings.Join(lines, "\n"), nil +} + +func init() { + bot.RegisterCommand( + "member", + "Prints member information", + "userid", + member) +} diff --git a/plugins/ping/ping.go b/plugins/ping/ping.go new file mode 100644 index 0000000..83ddc74 --- /dev/null +++ b/plugins/ping/ping.go @@ -0,0 +1,48 @@ +package club + +import ( + "fmt" + "strings" + + "git.uwaterloo.ca/csc/cscsysbot/utils" + + "github.com/go-chat-bot/bot" +) + +type InsensitiveSlice []string +func (s InsensitiveSlice) Len() int { + return len(s) +} + +func (s InsensitiveSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s InsensitiveSlice) Less(i, j int) bool { + a := strings.ToLower(s[i]) + b := strings.ToLower(s[j]) + + return a < b +} + +func ping(command *bot.Cmd) (string, error) { + nicks, _ := utils.SyscomNicks() + nicksStr := strings.Join(nicks, ", ") + if len(command.RawArgs) > 0 && len(command.RawArgs) + len(nicksStr) < 400 { + return fmt.Sprintf("%s (attn: %s)", command.RawArgs, nicksStr), nil + } else if len(command.RawArgs) > 0 { + return fmt.Sprintf("%s\n^ %s", command.RawArgs, nicksStr), nil + } else { + return fmt.Sprintf("Ping %s", nicksStr), nil + } + + return "", nil +} + +func init() { + bot.RegisterCommand( + "ping", + "Highlights all Syscom members.", + "Optionally a message to prepend the list of syscom members.", + ping) +} diff --git a/plugins/tweets/tweets.go b/plugins/tweets/tweets.go new file mode 100644 index 0000000..43fa7c2 --- /dev/null +++ b/plugins/tweets/tweets.go @@ -0,0 +1,86 @@ +package tweets + +import ( + "fmt" + "log" + "os" + "strings" + + "git.uwaterloo.ca/csc/cscsysbot/plugins/background" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" +) + +func lookup(tw *twitter.Client, sn string) (*twitter.User, error) { + params := &twitter.UserLookupParams{ + ScreenName: []string{sn}, + } + users, _, err := tw.Users.Lookup(params) + + if err != nil { + return nil, err + } + + if len(users) > 0 { + return &users[0], nil + } else { + return nil, nil + } +} + +func userInList(users []*twitter.User, user *twitter.User) bool { + for _, u := range users { + if u.ID == user.ID { + return true + } + } + + return false +} + +func init() { + // Start Twitter stream + authConfig := oauth1.NewConfig(os.Getenv("TWITTER_CONSUMER_KEY"), os.Getenv("TWITTER_CONSUMER_SECRET")) + authToken := oauth1.NewToken(os.Getenv("TWITTER_ACCESS_TOKEN"), os.Getenv("TWITTER_ACCESS_SECRET")) + + httpClient := authConfig.Client(oauth1.NoContext, authToken) + tw := twitter.NewClient(httpClient) + + watchScreenames := strings.Split(os.Getenv("TWITTER_USERS"), ",") + var users []*twitter.User + for _, screename := range watchScreenames { + user, err := lookup(tw, screename) + if err != nil { + log.Println("Unable to get Twitter user", screename, err) + continue + } + + users = append(users, user) + } + + demux := twitter.NewSwitchDemux() + demux.Tweet = func(tweet *twitter.Tweet) { + if userInList(users, tweet.User) { + background.Messages <- fmt.Sprintf("%s: %s", tweet.User.Name, strings.Replace(tweet.Text, "\n", " ", -1)) + } + } + demux.StreamDisconnect = func(disconnect *twitter.StreamDisconnect) { + log.Println("Stream disconnected", disconnect) + } + + userIds := make([]string, len(users)) + for i, user := range users { + userIds[i] = user.IDStr + } + params := &twitter.StreamFilterParams{ + Follow: userIds, + StallWarnings: twitter.Bool(true), + } + stream, err := tw.Streams.Filter(params) + if err != nil { + return + } + + go demux.HandleChan(stream.Messages) +} diff --git a/plugins/ups/ups.go b/plugins/ups/ups.go new file mode 100644 index 0000000..5c32998 --- /dev/null +++ b/plugins/ups/ups.go @@ -0,0 +1,30 @@ +package ups + +import ( + "fmt" + "strings" + "os" + + "github.com/go-chat-bot/bot" +) + +func ups(command *bot.Cmd) (msg string, err error) { + upses := strings.Split(os.Getenv("UPSES"), ",") + + var messages []string + for _, upsName := range upses { + ups := UPSStatus(upsName, os.Getenv("UPS_COMMUNITY_STRING")) + messages = append(messages, fmt.Sprintf("[%s] Runtime: %s, Status: %s, Battery: %s - %s\n", strings.Split(ups.Name, ".")[0], ups.EstimatedRuntime, ups.OutputStatus, ups.BatteryStatus, ups.BatteryReplaceIndicator)) + } + + msg = strings.Join(messages, "\n") + return +} + +func init() { + bot.RegisterCommand( + "ups", + "Prints current UPS status information. (Note: this command may take some time to execute)", + "", + ups) +} diff --git a/plugins/ups/upssnmp.go b/plugins/ups/upssnmp.go new file mode 100644 index 0000000..5e3d9bf --- /dev/null +++ b/plugins/ups/upssnmp.go @@ -0,0 +1,182 @@ +package ups + +import( + "fmt" + "time" + + "github.com/alouca/gosnmp" +) + +type BatteryStatus int +const ( + BatteryStatusUnknown BatteryStatus = 1 + BatteryStatusNormal BatteryStatus = 2 + BatteryStatusLow BatteryStatus = 3 + BatteryStatusFault BatteryStatus = 4 +) + +func (st BatteryStatus) String() string { + str := "Invalid" + switch(st) { + case BatteryStatusUnknown: + str = "Unknown" + case BatteryStatusNormal: + str = "Normal" + case BatteryStatusLow: + str = "Low" + case BatteryStatusFault: + str = "Fault" + } + + return str +} + +type BatteryReplaceIndicator int +const ( + BatteryReplaceIndicatorOK BatteryReplaceIndicator = 1 + BatteryReplaceIndicatorReplace BatteryReplaceIndicator = 2 +) + +func (st BatteryReplaceIndicator) String() string { + str := "Invalid" + switch(st) { + case BatteryReplaceIndicatorOK: + str = "Ok" + case BatteryReplaceIndicatorReplace: + str = "Replace" + } + + return str +} + +type OutputStatus int +const ( + OutputStatusUnknown OutputStatus = 1 + OutputStatusOnLine OutputStatus = 2 + OutputStatusOnBattery OutputStatus = 3 + OutputStatusOnSmartBoost OutputStatus = 4 + OutputStatusTimedSleeping OutputStatus = 5 + OutputStatusSoftwareBypass OutputStatus = 6 + OutputStatusOff OutputStatus = 7 + OutputStatusRebooting OutputStatus = 8 + OutputStatusSwitchedBypass OutputStatus = 9 + OutputStatusHardwareFailureBypass OutputStatus = 10 + OutputStatusSleepingUntilPowerReturn OutputStatus = 11 + OutputStatusOnSmartTrim OutputStatus = 12 +) + +func (st OutputStatus) String() string { + str := "Invalid" + switch(st) { + case OutputStatusUnknown: + str = "Unknown" + case OutputStatusOnLine: + str = "On Line" + case OutputStatusOnBattery: + str = "On Battery" + case OutputStatusOnSmartBoost: + str = "On Smart Boost" + case OutputStatusTimedSleeping: + str = "Timed Sleeping" + case OutputStatusSoftwareBypass: + str = "Software Bypass" + case OutputStatusOff: + str = "Off" + case OutputStatusRebooting: + str = "Rebooting" + case OutputStatusSwitchedBypass: + str = "Switched Bypass" + case OutputStatusHardwareFailureBypass: + str = "Hardware Failure Bypass" + case OutputStatusSleepingUntilPowerReturn: + str = "Sleeping Until Power Return" + case OutputStatusOnSmartTrim: + str = "On Smart Trim" + } + + return str +} + +func getRuntime(s *gosnmp.GoSNMP) (* time.Duration, error) { + resp, err := s.Get("1.3.6.1.4.1.318.1.1.1.2.2.3.0") + if err != nil { + return nil, err + } + + if len(resp.Variables) == 0 { + return nil, nil + } + + time, _ := time.ParseDuration(fmt.Sprintf("%dm", resp.Variables[0].Value.(int) / 60/ 100)) + return &time, nil +} + +func getBatteryStatus(s *gosnmp.GoSNMP) (* BatteryStatus, error) { + resp, err := s.Get("1.3.6.1.4.1.318.1.1.1.2.1.1.0") + if err != nil { + return nil, err + } + + if len(resp.Variables) == 0 { + return nil, nil + } + + status := BatteryStatus(resp.Variables[0].Value.(int)) + return &status, nil +} + +func getBatteryReplaceIndicator(s *gosnmp.GoSNMP) (* BatteryReplaceIndicator, error) { + resp, err := s.Get("1.3.6.1.4.1.318.1.1.1.2.2.4.0") + if err != nil { + return nil, err + } + + if len(resp.Variables) == 0 { + return nil, nil + } + + status := BatteryReplaceIndicator(resp.Variables[0].Value.(int)) + return &status, nil +} + +func getOutputStatus(s *gosnmp.GoSNMP) (* OutputStatus, error) { + resp, err := s.Get("1.3.6.1.4.1.318.1.1.1.4.1.1.0") + if err != nil { + return nil, err + } + + if len(resp.Variables) == 0 { + return nil, nil + } + + status := OutputStatus(resp.Variables[0].Value.(int)) + return &status, nil +} + +type UPS struct { + Name string + EstimatedRuntime *time.Duration + BatteryStatus *BatteryStatus + BatteryReplaceIndicator *BatteryReplaceIndicator + OutputStatus *OutputStatus +} + +func UPSStatus(name string, community string) *UPS { + ups := &UPS{ + Name: name, + EstimatedRuntime: nil, + BatteryStatus: nil, + BatteryReplaceIndicator: nil, + OutputStatus: nil, + } + + // Get UPS status + snmp, _ := gosnmp.NewGoSNMP(name, community, gosnmp.Version1, 2) + + ups.EstimatedRuntime, _ = getRuntime(snmp) + ups.BatteryStatus, _ = getBatteryStatus(snmp) + ups.BatteryReplaceIndicator, _ = getBatteryReplaceIndicator(snmp) + ups.OutputStatus, _ = getOutputStatus(snmp) + + return ups +} diff --git a/plugins/uptimerobot/uptimerobot.go b/plugins/uptimerobot/uptimerobot.go new file mode 100644 index 0000000..d53524d --- /dev/null +++ b/plugins/uptimerobot/uptimerobot.go @@ -0,0 +1,45 @@ +package uptimerobot + +import ( + "fmt" + "time" + + ur "git.uwaterloo.ca/csc/cscsysbot/utils/uptimerobot" + "git.uwaterloo.ca/csc/cscsysbot/plugins/background" +) + +func uptimeRobot() { + lastStatuses := make(map[int]ur.MonitorStatus) + + for { + monitors, err := ur.GetMonitors() + if err != nil { + time.Sleep(time.Minute * 1) + continue + } + + for _, mon := range monitors.Monitors { + lastStatus, ok := lastStatuses[mon.ID] + lastStatuses[mon.ID] = mon.Status + + if !ok { + continue + } + + if lastStatus != mon.Status { + if (mon.Type == ur.MonitorTypePort) { + background.Messages <- fmt.Sprintf("Uptime Robot: %s -> %s, %s:%d (%s)\n", lastStatus, mon.Status, mon.URL, mon.Type, int(mon.Port.(float64))) + } else { + background.Messages <- fmt.Sprintf("Uptime Robot: %s -> %s, %s (%s)\n", lastStatus, mon.Status, mon.URL, mon.Type) + } + } + + } + + time.Sleep(time.Minute * 1) + } +} + +func init() { + go uptimeRobot() +} diff --git a/plugins/url/url.go b/plugins/url/url.go new file mode 100644 index 0000000..2075b58 --- /dev/null +++ b/plugins/url/url.go @@ -0,0 +1,77 @@ +// Based on https://github.com/go-chat-bot/plugins/blob/master/url/url.go +// Copyright (c) 2014 Fábio Gomes +// The MIT License (MIT) + +package url + +import ( + "github.com/go-chat-bot/bot" + "github.com/go-chat-bot/plugins/web" + "html" + "net/url" + "regexp" + "strings" + "fmt" +) + +const ( + minDomainLength = 3 +) + +var ( + re = regexp.MustCompile("\\n*?(.*?)\\n*?<\\/title>") +) + +func canBeURLWithoutProtocol(text string) bool { + return len(text) > minDomainLength && + !strings.HasPrefix(text, "http") && + strings.Contains(text, ".") +} + +func extractURL(text string) string { + extractedURL := "" + for _, value := range strings.Split(text, " ") { + if canBeURLWithoutProtocol(value) { + value = "http://" + value + } + + parsedURL, err := url.Parse(value) + if err != nil { + continue + } + if strings.HasPrefix(parsedURL.Scheme, "http") { + extractedURL = parsedURL.String() + break + } + } + return extractedURL +} + +func urlTitle(cmd *bot.PassiveCmd) (string, error) { + URL := extractURL(cmd.Raw) + + if URL == "" { + return "", nil + } + + body, err := web.GetBody(URL) + if err != nil { + return "", err + } + + title := re.FindString(string(body)) + if title == "" { + return "", nil + } + + title = strings.Replace(title, "\n", "", -1) + title = title[strings.Index(title, ">")+1 : strings.LastIndex(title, "<")] + + return fmt.Sprintf("%q", html.UnescapeString(title)), nil +} + +func init() { + bot.RegisterPassiveCommand( + "url", + urlTitle) +} diff --git a/utils/lists.go b/utils/lists.go new file mode 100644 index 0000000..57b7aba --- /dev/null +++ b/utils/lists.go @@ -0,0 +1,11 @@ +package utils + +func InList(l []string, s string) bool { + for _, i := range l { + if i == s { + return true + } + } + + return false +} diff --git a/utils/syscom.go b/utils/syscom.go new file mode 100644 index 0000000..97ac9a2 --- /dev/null +++ b/utils/syscom.go @@ -0,0 +1,77 @@ +package utils + +import( + "crypto/tls" + "fmt" + "strings" + "sort" + + "gopkg.in/ldap.v2" +) + +type InsensitiveSlice []string +func (s InsensitiveSlice) Len() int { + return len(s) +} + +func (s InsensitiveSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s InsensitiveSlice) Less(i, j int) bool { + a := strings.ToLower(s[i]) + b := strings.ToLower(s[j]) + + return a < b +} + +const ( + ldapServer = "ldap-master.csclub.uwaterloo.ca" +) + +func SyscomNicks() ([]string, error) { + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, 636), &tls.Config{ + ServerName: ldapServer, + }) + if err != nil { + return make([]string, 0), err + } + defer l.Close() + + // Get members + req := ldap.NewSearchRequest("ou=Group,dc=csclub,dc=uwaterloo,dc=ca", ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + "(&(cn=syscom)(objectClass=posixGroup))", + []string{"*"}, nil) + res, err := l.Search(req) + if err != nil { + return make([]string, 0), err + } + + if len(res.Entries) > 0 { + entry := res.Entries[0] + + members := entry.GetAttributeValues("uniqueMember") + memberUids := make([]string, len(members)) + for i, member := range members { + memberUids[i] = strings.Replace(strings.Replace(member, "uid=", "", -1), + ",ou=People,dc=csclub,dc=uwaterloo,dc=ca", "", -1) + + // Exceptions: some use different nicks + switch memberUids[i] { + case "ztseguin": + memberUids[i] = "zseguin" + case "nablack": + memberUids[i] = "Na" + case "laden": + memberUids[i] = "Luqman" + } + } + + smemberUids := InsensitiveSlice(memberUids[0:]) + sort.Sort(smemberUids) + return []string(smemberUids), nil + } + + return make([]string, 0), nil +} diff --git a/utils/uptimerobot/uptimerobot.go b/utils/uptimerobot/uptimerobot.go new file mode 100644 index 0000000..5566b29 --- /dev/null +++ b/utils/uptimerobot/uptimerobot.go @@ -0,0 +1,120 @@ +package uptimerobot + +import ( + "fmt" + "net/http" + "strings" + "io/ioutil" + "encoding/json" + "time" + "net" + "os" +) + +// Really basic, no pagination support +type Monitors struct { + Monitors []Monitor `json:"monitors"` +} + +type Monitor struct { + ID int `json:"id"` + FriendlyName string `json:"friendly_name"` + URL string `json:"url"` + Type MonitorType `json:"type"` + Status MonitorStatus `json:"status"` + + // Type specifc properties + Port interface{} `json:"port"` +} + +type MonitorType int +const ( + _ MonitorType = iota + MonitorTypeHTTP + MonitorTypeKeyword + MonitorTypePing + MonitorTypePort +) + +func (t MonitorType) String() string { + switch t { + case MonitorTypeHTTP: + return "HTTP" + case MonitorTypeKeyword: + return "HTTP Keyword" + case MonitorTypePing: + return "Ping" + case MonitorTypePort: + return "Port" + } + + return "Invalid" +} + +type MonitorStatus int +const ( + MonitorStatusPaused MonitorStatus = 0 + MonitorStatusNotCheckedYet = 1 + MonitorStatusUp = 2 + MonitorStatusSeemsDown = 8 + MonitorStatusDown = 9 +) + +func (s MonitorStatus) String() string { + switch s { + case MonitorStatusPaused: + return "Paused" + case MonitorStatusNotCheckedYet: + return "Not Checked Yet" + case MonitorStatusUp: + return "Up" + case MonitorStatusSeemsDown: + return "Seems Down" + case MonitorStatusDown: + return "Down" + } + + return "Invalid" +} + +func GetMonitors() (Monitors, error) { + var monitors Monitors + + transport := &http.Transport{ + Dial: (&net.Dialer{ + Timeout: time.Second * 10, + }).Dial, + TLSHandshakeTimeout: time.Second * 10, + } + net := http.Client{ + Timeout: time.Second * 10, + Transport: transport, + } + url := "https://api.uptimerobot.com/v2/getMonitors" + payload := strings.NewReader(fmt.Sprintf("api_key=%s&format=json", os.Getenv("UPTIME_ROBOT_API_KEY"))) + req, err := http.NewRequest("POST", url, payload) + if err != nil { + return monitors, err + } + + req.Header.Add("content-type", "application/x-www-form-urlencoded") + req.Header.Add("cache-control", "no-cache") + + res, err := net.Do(req) + if err != nil { + return monitors, err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return monitors, err + } + + err = json.Unmarshal(body, &monitors) + if err != nil { + return monitors, err + } + + return monitors, nil +}