Initial commit

This commit is contained in:
Martin Dosch 2018-07-13 22:53:22 +02:00
commit 33e54a068b
6 changed files with 413 additions and 0 deletions

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
config.json
.vscode
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright 2018 Martin Dosch
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

47
README.md Normal file
View file

@ -0,0 +1,47 @@
## about
*feed-to-muc* is a XMPP which queries Atom or RSS newsfeeds and
posts the short summary to a XMPP MUC if there is a new article.
## disclaimer
I am no programmer and this bot is a result of a lot of *try and error*.
There are probably lots of quirks that are remains of some wrong paths
I went in between. Also a lot is probably solved in a *hacky way* due
to missing experience.
Anyway, it works (for me at least) and so I upload this here.
Recommendations about what can be done better improved and so on are
very welcome.
## requirements
* [go](https://golang.org/)
## installation
If you have *[GOPATH](https://github.com/golang/go/wiki/SettingGOPATH)*
set just run this commands:
```
$ go get salsa.debian.org/mdosch-guest/feed-to-muc
$ go install salsa.debian.org/mdosch-guest/feed-to-muc
```
You will find the binary in `$GOPATH/bin` or, if set, `$GOBIN`.
## configuration
The configuration is expected at `$HOME/.config/gowttr/config.json` with this format:
```
{
"ServerAddress": "example.com:5222",
"BotJid": "feedbot@example.com",
"Password": "ChangeThis!",
"Muc": "muc-to-feed@conference.example.com",
"MucNick": "feedbot",
"MaxArticles": 5,
"Feeds": [ "https://www.debian.org/News/news",
"https://www.debian.org/security/dsa-long" ]
}
```

10
config.json.example Normal file
View file

@ -0,0 +1,10 @@
{
"ServerAddress": "example.com:5222",
"BotJid": "feedbot@example.com",
"Password": "ChangeThis!",
"Muc": "muc-to-feed@conference.example.com",
"MucNick": "feedbot",
"MaxArticles": 5,
"Feeds": [ "https://www.debian.org/News/news",
"https://www.debian.org/security/dsa-long" ]
}

152
feed-to-muc.go Normal file
View file

@ -0,0 +1,152 @@
/* Copyright 2018 Martin Dosch
Licensed under the "MIT License" */
package main
import (
"encoding/json"
"log"
"os"
"os/user"
"strings"
"time"
"github.com/mattn/go-xmpp"
)
func main() {
var err error
var configPath string
type configuration struct {
ServerAddress string
BotJid string
Password string
Muc string
MucNick string
MaxArticles int
Feeds []string
}
// Get systems user config path.
osConfigDir := os.Getenv("$XDG_CONFIG_HOME")
if osConfigDir != "" {
// Create configPath if not yet existing.
configPath = osConfigDir + "/.config/feed-to-muc/"
if _, err := os.Stat(configPath); os.IsNotExist(err) {
err = os.MkdirAll(configPath, 0700)
if err != nil {
log.Fatal("Error: ", err)
}
}
} else { // Get the current user.
curUser, err := user.Current()
if err != nil {
log.Fatal("Error: ", err)
return
}
// Get home directory.
home := curUser.HomeDir
if home == "" {
log.Fatal("Error: No home directory available.")
return
}
// Create configPath if not yet existing.
configPath = home + "/.config/feed-to-muc/"
if _, err := os.Stat(configPath + "config.json"); os.IsNotExist(err) {
err = os.MkdirAll(configPath, 0700)
if err != nil {
log.Fatal("Error: ", err)
}
}
}
// Check that config file is existing.
if _, err := os.Stat(configPath + "config.json"); os.IsNotExist(err) {
log.Fatal("Error: ", err)
}
// Read configuration file into variable config.
file, _ := os.Open(configPath + "config.json")
defer file.Close()
decoder := json.NewDecoder(file)
config := configuration{}
if err := decoder.Decode(&config); err != nil {
log.Fatal("Error: ", err)
}
if _, err := os.Stat(configPath + "config.json"); os.IsNotExist(err) {
err = os.MkdirAll(configPath, 0700)
if err != nil {
log.Fatal("Error: ", err)
}
}
var client *xmpp.Client
options := xmpp.Options{
Host: config.ServerAddress,
User: config.BotJid,
Password: config.Password,
NoTLS: true,
StartTLS: true,
Debug: false,
}
client, err = options.NewClient()
if err != nil {
log.Fatal(err)
}
// Starting goroutine to ping the server every 30 seconds.
go ping(client, config.ServerAddress, config.BotJid)
// Join the MUC
mucStatus, err := client.JoinMUCNoHistory(config.Muc, config.MucNick)
if err != nil {
log.Fatal(err)
}
// Exit if Status is > 300, see https://xmpp.org/registrar/mucstatus.html
if mucStatus > 300 {
os.Exit(mucStatus)
}
for {
_, err := client.Recv()
if err != nil {
log.Fatal(err)
}
for i := 0; i < len(config.Feeds); i++ {
output, err := getArticles(config.Feeds[i], config.MaxArticles, configPath)
if err != nil {
log.Fatal(err)
}
if output != "" {
_, err = client.Send(xmpp.Chat{Remote: config.Muc, Type: "groupchat", Text: output})
}
if err != nil {
log.Fatal(err)
}
}
time.Sleep(30 * time.Second)
}
}
// Send a ping every 30 seconds to check if the server is still available.
func ping(client *xmpp.Client, server string, botJid string) {
for {
time.Sleep(30 * time.Second)
err := client.PingC2S(botJid, strings.Split(server, ":")[0])
if err != nil {
log.Fatal(err)
}
}
}

178
getarticles.go Normal file
View file

@ -0,0 +1,178 @@
/* Copyright 2018 Martin Dosch
Licensed under the "MIT License" */
package main
import (
"encoding/json"
"hash/fnv"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/mmcdole/gofeed"
"jaytaylor.com/html2text"
)
//func getArticles(max int) ([]string, error) {
func getArticles(feedURL string, max int, cachePath string) (string, error) {
type feedCache struct {
LastChange string
}
var output string
var last time.Time
var lastUpdate feedCache
var file *os.File
var updateTime time.Time
// Create a hash as identifier for the feed.
// The identifier will be used as filename for caching the update time.
// ToDo: cachePath should probably be moved to ~/.cache/feed-to-muc
h := fnv.New32a()
h.Write([]byte(feedURL))
cachePath = cachePath + "cache/"
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
err = os.MkdirAll(cachePath, 0700)
if err != nil {
log.Fatal("Error: ", err)
}
}
cacheFile := cachePath + strconv.Itoa(int(h.Sum32()))
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
file, err = os.Create(cacheFile)
if err != nil {
log.Fatal("Error: ", err)
}
defer file.Close()
last = time.Now()
lastUpdate.LastChange = last.Format(time.RFC3339)
lastUpdateJSON, _ := json.MarshalIndent(lastUpdate, "", " ")
_, err = file.Write(lastUpdateJSON)
if err != nil {
log.Fatal("Error: ", err)
}
} else {
file, err = os.OpenFile(cacheFile, os.O_RDWR, 0600)
if err != nil {
log.Fatal("Error: ", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
lastUpdate := feedCache{}
if err := decoder.Decode(&lastUpdate); err != nil {
log.Fatal("Error: ", err)
}
last, err = time.Parse(time.RFC3339, string(lastUpdate.LastChange))
if err != nil {
log.Fatal("Error: ", err)
}
}
fp := gofeed.NewParser()
feed, err := fp.ParseURL(feedURL)
if err != nil {
return "", err
}
// If no publish date is offered try update date.
// If both is not offered give up.
if feed.Items[0].PublishedParsed == nil {
if feed.Items[0].UpdatedParsed == nil {
return "", err
}
// If cached timestamp is newer than the one of
// the last article return.
if last.After(*feed.Items[0].UpdatedParsed) {
return "", err
}
} else {
// If cached timestamp is newer than the one of
// the last article return.
if last.After(*feed.Items[0].PublishedParsed) {
return "", err
}
}
// Check last n (defined in config) articles for new ones.
for i := max - 1; i >= 0; i-- {
// Stop processing for article i if there are not so
// many articles in the feed.
if len(feed.Items) < i+1 {
continue
}
article := *feed.Items[i]
if err != nil {
return "", err
}
if article.PublishedParsed == nil {
updateTime = *article.UpdatedParsed
} else {
updateTime = *article.PublishedParsed
}
// If cached timestamp is not older than the article stop processing.
// Note: Checking for cached timestamp being newer, instead of not older
// lead to duplicate messages for the same article. Probably a corner
// case when the time is identical.
if last.Before(updateTime) == false {
continue
}
if i == 0 {
last = updateTime
lastUpdate.LastChange = updateTime.Format(time.RFC3339)
// Remove file with cached timestamp and create it
// again with updated timestamp.
// ToDo: Replace timestamp without deleting.
err = os.Remove(cacheFile)
if err != nil {
log.Fatal("Error: ", err)
}
file, err = os.Create(cacheFile)
if err != nil {
log.Fatal("Error: ", err)
}
defer file.Close()
lastUpdateJSON, _ := json.MarshalIndent(lastUpdate, "", " ")
_, err = file.Write(lastUpdateJSON)
if err != nil {
log.Fatal("Error: ", err)
}
}
// Strip HTML as we want to get plain text.
description, err := html2text.FromString(article.Description)
if err != nil {
return "", err
}
if strings.Contains(description, article.Link) == true {
output = output + feed.Title + ": *" + article.Title + "*\n\n" + description
} else {
output = output + feed.Title + ": *" + article.Title + "*\n\n" + description + "\n" + article.Link
}
if i > 0 {
output = output + "\n\n---\n\n"
}
}
return output, err
}