Browse Source

Merge pull request 'feature/split_main' (#1) from feature/split_main into master

Reviewed-on: #1
master
netmoose 4 years ago
parent
commit
60485ab440
  1. 104
      README.md
  2. 271
      main.go
  3. 139
      process_rss.go
  4. 145
      send.go

104
README.md

@ -1 +1,103 @@ @@ -1 +1,103 @@
# RSSCollector
# RSSCollector
## О проекте
Данный проект предназначен для сбора информации из RSS о публикациях на сайтах. Найденные новые публикации пересылаются в канал в Telegram.
## Какие технологии используются
Для сборки проекта необходимы:
1. Компилятор [GO](https://go.dev/dl/)
2. Дополнительный модуль обработки параметров командной строки https://github.com/jessevdk/go-flags
3. Дополнительный модуль для работы с Telegram Bot API https://github.com/go-telegram-bot-api/telegram-bot-api
4. Встраиваемая [БД BoltDB](https://github.com/boltdb/bolt)
## Конфигурация
По-умолчанию конфигурационный файл ищется в том же каталоге, где запускается исполняемый файл, с именем `config.yml`.
Формат данного файла - YAML
Выглядит конфиг так:
```yaml
dbpath: "/var/cache/rsscollector/rss.db"
telegram:
senddebug: true
chatid: <CHATID>
token: "<TOKEN>"
rsslist:
- name: "<RSS1>"
url: "https://<URL1>"
- name: "<RSS2>"
url: "https://<URL2>"
```
где
- `dbpath` - путь до файла БД
- `telegram` - блок описания парабетров для telegram-бота
- `senddebug` - выводить ли debug-информацию при отправке в телеграм
- \<CHATID\> - идентификатор канала, в который будет присылаться информация. Получить можно например из робота [Json Dump Bot](https://t.me/JsonDumpBot), переслав ему любое сообщение из требующегося канала и взяв идентификатор (вместе с -) из параметра `forward_from_chat`
- \<TOKEN\> - токен бота, от имени которого будут присылаться сообщения и которого необходимо добавить в канал администратором (достаточно оставить ему права отсылать сообщения в канал). Бота заводим, как обычно, через [BotFather](https://t.me/BotFather)
- `rsslist` - блок описания лент, с которых будет собираться информация
- \<RSS1\>, \<RSS2\> - наименования лент. Используются как идентификаторы в БД и в логах для вывода информации, откуда инфа, а также при отправке в канал в заголовке
- \<URL1\>, \<URL2\> - веб-адреса RSS-лент
## Сборка
`go build -o rsscollector` в каталоге проекта. Перед этим имеет смысл получить все зависимости, перечисленные в `go.mod`
## Запуск
При запуске проверяется один параметр командной строки:
`-c` или `--configpath` - в нем необходимо указать, где находится конфиг. Как было уже сказано выше - это необязательно и конфиг можно разместить прямо в каталоге запуска.
RSS - ленты парсятся по-очереди, новые сообщения отсылаются в канал и записываются в БД, чтобы при следующем запуске не отсылались. Соответственно, при первом запуске надо быть готовым, что в канал прилетит очень много сообщений.
Я использую для запуска данного бинарника systemd и его возможности для запуска сервисов по расписанию.
Для этого в /etc/systemd/system созданы два файла:
- rsscollector.service
- rsscollector.timer
rsscollector.service:
```ini
[Unit]
Description=Collect RSS from sites and send to telegram
Wants=rsscollector.timer
[Service]
Type=oneshot
ExecStart=/usr/local/bin/rsscollector -c /etc/rsscollector/config.yml
User=root
Group=root
[Install]
WantedBy=multi-user.target
```
rsscollector.timer
```ini
[Unit]
Description=Collect RSS from sites and send to telegram
Requires=rsscollector.service
[Timer]
Unit=rsscollector.service
OnCalendar=*:0
[Install]
WantedBy=timers.target
```
После создания файлов необходимо включить выполнение `rsscollector.timer` командой:
`systemctl enable rsscollector.timer`
В начале каждого часа будет вызываться на выполнение `rsscollector.service`

271
main.go

@ -1,25 +1,10 @@ @@ -1,25 +1,10 @@
package main
import (
"crypto/tls"
"encoding/json"
"encoding/xml"
"fmt"
"sort"
"html/template"
"io"
"log"
"net/http"
"os"
"reflect"
"strings"
"time"
"github.com/boltdb/bolt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/jessevdk/go-flags"
"golang.org/x/net/html"
"gopkg.in/yaml.v2"
)
@ -66,262 +51,6 @@ type Options struct { @@ -66,262 +51,6 @@ type Options struct {
var ConfigPath = "./config.yml"
type Rss2 struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
// Required
Title string `xml:"channel>title"`
Link string `xml:"channel>link"`
Description string `xml:"channel>description"`
// Optional
PubDate string `xml:"channel>pubDate"`
ItemList []Item `xml:"channel>item"`
}
type Item struct {
// Required
Title string `xml:"title"`
Link string `xml:"link"`
Description template.HTML `xml:"description"`
// Optional
Content template.HTML `xml:"encoded"`
PubDate string `xml:"pubDate"`
Comments string `xml:"comments"`
}
type SendItems struct {
ItemList []Item
}
type ByPubDate []Item
func (a ByPubDate) Len() int { return len(a) }
func (a ByPubDate) Less(i, j int) bool {
timeone, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", a[i].PubDate)
timetwo, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", a[j].PubDate)
return timeone.Unix() > timetwo.Unix()
}
func (a ByPubDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func GetRSS(name string, url string) (*Rss2, error) {
rss := &Rss2{}
var netClient = &http.Client{}
customTransport := &(*http.DefaultTransport.(*http.Transport)) // make shallow copy
timeout := time.Duration(240 * time.Second)
customTransport = &http.Transport{
IdleConnTimeout: timeout,
ResponseHeaderTimeout: timeout,
DisableKeepAlives: false,
DisableCompression: false,
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: timeout,
MaxIdleConns: 20,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
}
netClient = &http.Client{Transport: customTransport, Timeout: timeout}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0")
resp, err := netClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Start RSS decoding from file
if err := xml.Unmarshal(body, rss); err != nil {
return nil, err
}
return rss, nil
}
func ProcessRss(rss Rss2, dbpath string, rssname string) (*SendItems, error) {
var si SendItems
db, err := bolt.Open(dbpath, 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(rssname))
if err != nil {
return err
}
return nil
})
for _, v := range rss.ItemList {
db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(rssname))
c := b.Cursor()
flag := false
for key, _ := c.First(); key != nil; key, _ = c.Next() {
if v.Link == string(key) {
flag = true
break
}
}
if !flag {
si.ItemList = append(si.ItemList, v)
}
return nil
})
}
sort.Sort(ByPubDate(si.ItemList))
if len(si.ItemList) > 0 {
return &si, nil
} else {
return nil, nil
}
}
func in_array(val interface{}, array interface{}) (exists bool) {
exists = false
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
exists = true
return
}
}
}
return
}
func NormalizeHTMLforTelegram(s string) (out string) {
tags := []string{"br", "img", "b", "strong", "i", "em", "code", "s", "strike", "del", "u", "pre"}
domDocTest := html.NewTokenizer(strings.NewReader(s))
previousStartTokenTest := domDocTest.Token()
flagendtag := false
for {
tt := domDocTest.Next()
if len(out) > 2500 {
if e := in_array(previousStartTokenTest.Data, tags) && previousStartTokenTest.Data != "img" && previousStartTokenTest.Data != "br" && !flagendtag; e {
out += fmt.Sprintf("</%s> ...", previousStartTokenTest.Data)
} else {
out += " ..."
}
return
}
switch {
case tt == html.ErrorToken:
return
case tt == html.StartTagToken:
previousStartTokenTest = domDocTest.Token()
if e := in_array(previousStartTokenTest.Data, tags); e {
switch {
case previousStartTokenTest.Data == "br":
out += "\n"
case previousStartTokenTest.Data == "img" && previousStartTokenTest.Attr[0].Key == "src":
out += fmt.Sprintf("%s ", previousStartTokenTest.Attr[0].Val)
// case previousStartTokenTest.Data == "a" && previousStartTokenTest.Attr[0].Key == "href":
// out += fmt.Sprintf(" %s ", previousStartTokenTest.Attr[0].Val)
default:
out += fmt.Sprintf(" <%s>", previousStartTokenTest.Data)
}
flagendtag = false
}
case tt == html.EndTagToken:
t := domDocTest.Token()
if e := in_array(t.Data, tags); e {
// switch {
// case t.Data == "a":
// out += " "
// default:
out += fmt.Sprintf("</%s> ", t.Data)
// }
flagendtag = true
}
case tt == html.SelfClosingTagToken:
t := domDocTest.Token()
if e := in_array(t.Data, tags); e {
if t.Data == "br" {
out += "\n"
}
}
case tt == html.TextToken:
if previousStartTokenTest.Data == "script" {
continue
}
TxtContent := strings.TrimSpace(html.UnescapeString(string(domDocTest.Text())))
if len(TxtContent) > 0 {
out += TxtContent
}
}
}
}
func SendAndWriteToDB(send SendItems, dbpath string, rssname string, token string, chatid int64, debug bool) error {
db, err := bolt.Open(dbpath, 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
for i := len(send.ItemList) - 1; i >= 0; i-- {
v := send.ItemList[i]
log.Printf("Send to telegram post: %s", v.Title)
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
log.Panic(err)
}
bot.Debug = debug
s := "<i>" + rssname + "</i>\n\n" + "<b>" + string(v.Title) + "</b>\n\n" + NormalizeHTMLforTelegram(html.UnescapeString(string(v.Description))) +
"\n\n" + v.Link
msg := tgbotapi.NewMessage(chatid, s)
msg.ParseMode = "Html"
_, err = bot.Send(msg)
if err != nil {
log.Panic(err)
}
duration := time.Duration(10) * time.Second
time.Sleep(duration)
db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(rssname))
if err != nil {
return err
}
encoded, err := json.Marshal(v)
if err != nil {
return err
}
err = b.Put([]byte(v.Link), encoded)
if err != nil {
return err
}
return nil
})
}
return nil
}
func main() {
var options Options
var parser = flags.NewParser(&options, flags.Default)

139
process_rss.go

@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
package main
import (
"crypto/tls"
"encoding/xml"
"html/template"
"io"
"log"
"net/http"
"sort"
"time"
"github.com/boltdb/bolt"
)
type Rss2 struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
// Required
Title string `xml:"channel>title"`
Link string `xml:"channel>link"`
Description string `xml:"channel>description"`
// Optional
PubDate string `xml:"channel>pubDate"`
ItemList []Item `xml:"channel>item"`
}
type Item struct {
// Required
Title string `xml:"title"`
Link string `xml:"link"`
Description template.HTML `xml:"description"`
// Optional
Content template.HTML `xml:"encoded"`
PubDate string `xml:"pubDate"`
Comments string `xml:"comments"`
}
type SendItems struct {
ItemList []Item
}
type ByPubDate []Item
func (a ByPubDate) Len() int { return len(a) }
func (a ByPubDate) Less(i, j int) bool {
timeone, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", a[i].PubDate)
timetwo, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", a[j].PubDate)
return timeone.Unix() > timetwo.Unix()
}
func (a ByPubDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func GetRSS(name string, url string) (*Rss2, error) {
rss := &Rss2{}
var netClient = &http.Client{}
customTransport := &(*http.DefaultTransport.(*http.Transport)) // make shallow copy
timeout := time.Duration(240 * time.Second)
customTransport = &http.Transport{
IdleConnTimeout: timeout,
ResponseHeaderTimeout: timeout,
DisableKeepAlives: false,
DisableCompression: false,
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: timeout,
MaxIdleConns: 20,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
}
netClient = &http.Client{Transport: customTransport, Timeout: timeout}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0")
resp, err := netClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Start RSS decoding from file
if err := xml.Unmarshal(body, rss); err != nil {
return nil, err
}
return rss, nil
}
func ProcessRss(rss Rss2, dbpath string, rssname string) (*SendItems, error) {
var si SendItems
db, err := bolt.Open(dbpath, 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(rssname))
if err != nil {
return err
}
return nil
})
for _, v := range rss.ItemList {
db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(rssname))
c := b.Cursor()
flag := false
for key, _ := c.First(); key != nil; key, _ = c.Next() {
if v.Link == string(key) {
flag = true
break
}
}
if !flag {
si.ItemList = append(si.ItemList, v)
}
return nil
})
}
sort.Sort(ByPubDate(si.ItemList))
if len(si.ItemList) > 0 {
return &si, nil
} else {
return nil, nil
}
}

145
send.go

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
package main
import (
"encoding/json"
"fmt"
"log"
"reflect"
"strings"
"time"
"github.com/boltdb/bolt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"golang.org/x/net/html"
)
func in_array(val interface{}, array interface{}) (exists bool) {
exists = false
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
exists = true
return
}
}
}
return
}
func NormalizeHTMLforTelegram(s string) (out string) {
tags := []string{"br", "img", "b", "strong", "i", "em", "code", "s", "strike", "del", "u", "pre"}
domDocTest := html.NewTokenizer(strings.NewReader(s))
previousStartTokenTest := domDocTest.Token()
flagendtag := false
for {
tt := domDocTest.Next()
if len(out) > 2500 {
if e := in_array(previousStartTokenTest.Data, tags) && previousStartTokenTest.Data != "img" && previousStartTokenTest.Data != "br" && !flagendtag; e {
out += fmt.Sprintf("</%s> ...", previousStartTokenTest.Data)
} else {
out += " ..."
}
return
}
switch {
case tt == html.ErrorToken:
return
case tt == html.StartTagToken:
previousStartTokenTest = domDocTest.Token()
if e := in_array(previousStartTokenTest.Data, tags); e {
switch {
case previousStartTokenTest.Data == "br":
out += "\n"
case previousStartTokenTest.Data == "img" && previousStartTokenTest.Attr[0].Key == "src":
out += fmt.Sprintf("%s ", previousStartTokenTest.Attr[0].Val)
// case previousStartTokenTest.Data == "a" && previousStartTokenTest.Attr[0].Key == "href":
// out += fmt.Sprintf(" %s ", previousStartTokenTest.Attr[0].Val)
default:
out += fmt.Sprintf(" <%s>", previousStartTokenTest.Data)
}
flagendtag = false
}
case tt == html.EndTagToken:
t := domDocTest.Token()
if e := in_array(t.Data, tags); e {
// switch {
// case t.Data == "a":
// out += " "
// default:
out += fmt.Sprintf("</%s> ", t.Data)
// }
flagendtag = true
}
case tt == html.SelfClosingTagToken:
t := domDocTest.Token()
if e := in_array(t.Data, tags); e {
if t.Data == "br" {
out += "\n"
}
}
case tt == html.TextToken:
if previousStartTokenTest.Data == "script" {
continue
}
TxtContent := strings.TrimSpace(html.UnescapeString(string(domDocTest.Text())))
if len(TxtContent) > 0 {
out += TxtContent
}
}
}
}
func SendAndWriteToDB(send SendItems, dbpath string, rssname string, token string, chatid int64, debug bool) error {
db, err := bolt.Open(dbpath, 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
for i := len(send.ItemList) - 1; i >= 0; i-- {
v := send.ItemList[i]
log.Printf("Send to telegram post: %s", v.Title)
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
log.Panic(err)
}
bot.Debug = debug
s := "<i>" + rssname + "</i>\n\n" + "<b>" + string(v.Title) + "</b>\n\n" + NormalizeHTMLforTelegram(html.UnescapeString(string(v.Description))) +
"\n\n" + v.Link
msg := tgbotapi.NewMessage(chatid, s)
msg.ParseMode = "Html"
_, err = bot.Send(msg)
if err != nil {
log.Panic(err)
}
duration := time.Duration(10) * time.Second
time.Sleep(duration)
db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(rssname))
if err != nil {
return err
}
encoded, err := json.Marshal(v)
if err != nil {
return err
}
err = b.Put([]byte(v.Link), encoded)
if err != nil {
return err
}
return nil
})
}
return nil
}
Loading…
Cancel
Save