package main import ( "bufio" "crypto/md5" "errors" "flag" "fmt" "io" "log" "os" "sort" "strings" "time" irc "github.com/fluffle/goirc/client" "github.com/mmcdole/gofeed" ) var ( server = flag.String("server", "", "IRC server") channel = flag.String("channel", "", "IRC channel") nick = flag.String("nick", "", "IRC nickname") period = flag.String("period", "10m", "Update period") seenPath = flag.String("seen", "", "\"Seen\" entries store") postOld = flag.Bool("post-old", false, "Post all entries on the first fetch ever") ) func main() { flag.Parse() if !flag.Parsed() || *server == "" || *channel == "" || *nick == "" || *seenPath == "" || len(flag.Args()) == 0 { flag.Usage() if flag.Parsed() { fmt.Fprintln(os.Stderr, "Flags -server, -channel, -nick and -seen are not optional.") fmt.Fprintln(os.Stderr, "At least one url has to be provided.") } os.Exit(1) } updatePeriod, err := time.ParseDuration(*period) if err == nil && updatePeriod < 5*time.Minute { err = fmt.Errorf("too short: %s", updatePeriod) } if err != nil { fmt.Fprintf(os.Stderr, "update period: %s", err) os.Exit(1) } seenFile, err := os.OpenFile(*seenPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { fmt.Fprintf(os.Stderr, "seen file: %s", err) os.Exit(1) } defer seenFile.Close() seenReader := bufio.NewReader(seenFile) seen := make(map[[md5.Size]byte]struct{}) for { if url, err := seenReader.ReadString('\n'); err != nil { if errors.Is(err, io.EOF) { break } fmt.Fprintf(os.Stderr, "seen file: %s", err) os.Exit(1) } else { url := []byte(strings.TrimSpace(url)) seen[md5.Sum(url)] = struct{}{} } } urls := flag.Args() skipFirst := make(map[string]struct{}) if len(seen) == 0 && !*postOld { for _, url := range urls { skipFirst[url] = struct{}{} } } cfg := irc.NewConfig(*nick, "feed2ircbot", "feed2irc bot") c := irc.Client(cfg) c.EnableStateTracking() c.HandleFunc(irc.CONNECTED, func(*irc.Conn, *irc.Line) { log.Printf("irc: connected") c.Join(*channel) }) c.HandleFunc(irc.DISCONNECTED, func(*irc.Conn, *irc.Line) { log.Printf("irc: disconnected") os.Exit(1) }) joinedChan := make(chan bool, 1) c.HandleFunc(irc.JOIN, func(c *irc.Conn, line *irc.Line) { if c.Me().Nick == line.Nick { log.Printf("irc: joined") close(joinedChan) } }) if err := c.ConnectTo(*server); err != nil { fmt.Fprintf(os.Stderr, "irc: %s", err) os.Exit(1) } <-joinedChan for { for _, url := range urls { p := gofeed.NewParser() if feed, err := p.ParseURL(url); err != nil { log.Printf("%s: %s", url, err) } else { sort.Sort(feed) for _, item := range feed.Items { sum := md5.Sum([]byte(item.Link)) if _, have := seen[sum]; !have { if _, err := fmt.Fprintln(seenFile, item.Link); err != nil { log.Printf("seen file: %s", err) os.Exit(1) } seenFile.Sync() seen[sum] = struct{}{} if _, skip := skipFirst[url]; !skip { s := fmt.Sprintf("%s | %s\n", item.Title, item.Link) c.Privmsgln(*channel, s) } } } delete(skipFirst, url) } } time.Sleep(updatePeriod) } }