Browse Source

Adds basic notification and configuration support

Adds REST call to Pushover API to send notifications when matched phrase
is received from the network.

Configuration via POSIX getopt-style flags and an optional key-value
configuration file. If specified, the settings in the file are read
first, then overwritten with the commandline flags. This means that the
commandline flags take precedence over the configuration file.
master
Noah Pederson 1 year ago
parent
commit
6e450184a3
  1. 130
      config.go
  2. 5
      go.mod
  3. 9
      go.sum
  4. 83
      main.go

130
config.go

@ -0,0 +1,130 @@
package main
import (
"bufio"
"errors"
"fmt"
"git.sr.ht/~sircmpwn/getopt"
"log"
"os"
"strings"
)
type Config struct {
Host string `toml:"irc-server"`
Pass string `toml:"pass"`
User string `toml:"user"`
Nick string `toml:"nick"`
Match string `toml:"match"`
AppToken string `toml:"app-token"`
UserToken string `toml:"user-token"`
ExcludeChannels string `toml:"exclude-channels"`
ExcludeUsers string `toml:"exclude-users"`
}
//Partially adapted from:
//https://stackoverflow.com/questions/40022861/parsing-values-from-property-file-in-golang/46860900
func LoadConfig() (*Config, error) {
opts, optind, err := getopt.Getopts(os.Args, "h:u:p:m:n:a:t:")
if err != nil {
log.Fatalf("Unable to get options %s", err)
}
var config *Config
if optind < len(os.Args) {
filePath := os.Args[optind]
config, err = loadConfigFromFile(filePath)
if err != nil {
return nil, fmt.Errorf("Unable to read config file: %w", err)
}
} else {
config = &Config{User: "-bell"}
}
for _, opt := range opts {
switch opt.Option {
case 'h':
config.Host = opt.Value
case 'u':
config.User = opt.Value
case 'p':
config.Pass = opt.Value
case 'm':
config.Match = opt.Value
case 'n':
config.Nick = opt.Value
case 'a':
config.AppToken = opt.Value
case 't':
config.UserToken = opt.Value
}
}
if config.Host == "" {
return nil, errors.New("Most specify host either in config file or with -h")
}
if config.Nick == "" {
config.Nick = config.User
}
if config.Match == "" {
config.Match = config.Nick
}
if config.AppToken == "" || config.UserToken == "" {
return nil, errors.New("Must specify app-token and user-token for pushover")
}
return config, nil
}
func loadConfigFromFile(path string) (*Config, error) {
if len(path) == 0 {
return nil, errors.New("Cannot read empty path")
}
config := &Config{
User: "-bell",
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Unable to open config file: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
//handle comments
if line[0] == '#' {
continue
}
// This is big and ugly
if equal := strings.Index(line, "="); equal >= 0 {
if key := strings.TrimSpace(line[:equal]); len(key) > 0 {
value := ""
if len(line) > equal {
value = strings.TrimSpace(line[equal+1:])
}
//TODO (noah): parse into config
switch key {
case "irc-server":
config.Host = value
case "pass":
config.Pass = value
case "user":
config.User = value
case "nick":
config.Nick = value
case "app-token":
config.AppToken = value
case "user-token":
config.UserToken = value
case "exclude-channels":
config.ExcludeChannels = value
case "exclude-users":
config.ExcludeUsers = value
default:
log.Printf("Unknown property in config file: %s=%s", key, value)
break
}
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("Unable to scan through config file: %w", err)
}
return config, nil
}

5
go.mod

@ -2,4 +2,7 @@ module git.packetlostandfound.us/chiefnoah/bell
go 1.14
require gopkg.in/sorcix/irc.v2 v2.0.0-20190306112350-8d7a73540b90
require (
git.sr.ht/~sircmpwn/getopt v0.0.0-20190808004552-daaf1274538b
gopkg.in/sorcix/irc.v2 v2.0.0-20190306112350-8d7a73540b90
)

9
go.sum

@ -1,2 +1,11 @@
git.sr.ht/~sircmpwn/getopt v0.0.0-20190808004552-daaf1274538b h1:da5JBQ6dcW14aWnEf/pFRIMV2PsqTQEWmR+V2sw5oxU=
git.sr.ht/~sircmpwn/getopt v0.0.0-20190808004552-daaf1274538b/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
gopkg.in/sorcix/irc.v2 v2.0.0-20190306112350-8d7a73540b90 h1:ItuFAq9SlPhZvdIvsdgoE38i9aLLdDpBbFV9vTJhlp8=
gopkg.in/sorcix/irc.v2 v2.0.0-20190306112350-8d7a73540b90/go.mod h1:PmJkUcwbuPi1FiZ9Rarr6wzVMvzkO7uWqH1jwrMkgW0=

83
main.go

@ -1,17 +1,30 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"gopkg.in/sorcix/irc.v2"
)
const PushoverAPI string = "https://api.pushover.net/1/messages.json"
func main() {
conn, err := irc.DialTLS("<irc server>", &tls.Config{})
config, err := LoadConfig()
if err != nil {
log.Fatalf("Unable to configure: %s", err)
}
//TODO (noah): probably should check for protocol + port or something
conn, err := irc.DialTLS(config.Host, &tls.Config{})
if err != nil {
log.Fatalf("Unable to connect to IRC server: %s", err)
}
@ -29,27 +42,79 @@ func main() {
conn.Close()
os.Exit(0)
}()
_, err = conn.Write([]byte("PASS <password>"))
if err != nil {
log.Fatalf("Unable to authenticate to server: %s", err)
if config.Pass != "" {
_, err = conn.Write([]byte("PASS " + config.Pass))
if err != nil {
log.Fatalf("Unable to authenticate to server: %s", err)
}
}
_, err = conn.Write([]byte("NICK -testbot"))
_, err = conn.Write([]byte("USER " + config.User))
if err != nil {
log.Fatalf("Unable to send message to server: %s", err)
}
_, err = conn.Write([]byte("USER -testbot"))
_, err = conn.Write([]byte("NICK " + config.Nick))
if err != nil {
log.Fatalf("Unable to send message to server: %s", err)
}
_, err = conn.Write([]byte("JOIN #channel"))
_, err = conn.Write([]byte("WHOIS " + config.Nick))
if err != nil {
log.Fatalf("Unable to send message to server: %s", err)
}
away := false
for running {
raw_message, err := conn.Decode()
message, err := conn.Decode()
if err != nil {
log.Printf("Unable to decode: %s", err)
time.Sleep(1 * time.Second)
}
if message == nil {
continue
}
log.Printf("Message Prefix: %+v", message.Prefix)
log.Printf("Message Command: %+v", message.Command)
log.Printf("Message Params: %v", message.Params)
if away && message.Command == irc.PRIVMSG {
log.Printf("Away and received message: %s, checking if contains search term: %s", message.Params[1], config.Match)
if strings.Contains(message.Params[1], config.Match) {
log.Printf("Message contains search word: %s", message.Params)
err = sendNotification(config.UserToken, config.AppToken, message)
if err != nil {
log.Printf("Unable to send notification: %s", err)
}
}
} else if message.Command == irc.RPL_NOWAWAY {
log.Printf("Marking as away, will notify")
away = true
} else if message.Command == irc.RPL_UNAWAY {
log.Printf("No longer away, won't notify")
away = false
} else if message.Command == irc.RPL_NAMREPLY {
//This is used to query for away status on startup
//TODO (noah): is this valid?
config.Nick = message.Params[0]
log.Printf("Set nick reference to: %s", config.Nick)
} else if message.Command == irc.NICK {
config.Nick = message.Params[0]
log.Printf("Setting nick to: %s", config.Nick)
}
log.Printf("%s", raw_message)
}
}
func sendNotification(user_token, app_token string, message *irc.Message) error {
body := map[string]string{
"token": app_token,
"user": user_token,
"title": fmt.Sprintf("%s on %s mentioned you", message.Prefix.User, message.Params[0]),
"message": message.Params[1],
}
json_body, err := json.Marshal(body)
if err != nil {
return err
}
resp, err := http.Post(PushoverAPI, "application/json", bytes.NewBuffer(json_body))
if err != nil {
return err
}
log.Printf("Pushover API notify resp code: %d %s", resp.StatusCode, resp.Status)
return nil
}
Loading…
Cancel
Save