multi_feed #6

Merged
B4D_US3R merged 4 commits from multi_feed into master 2025-05-12 07:21:31 +00:00
11 changed files with 144 additions and 48 deletions

View file

@ -2,15 +2,16 @@ FROM golang:1.24
WORKDIR /app/kiki
RUN mkdir /app/kiki/config /app/kiki/service /app/kiki/stacker /app/kiki/tooter
RUN mkdir /app/kiki/config /app/kiki/service /app/kiki/stacker /app/kiki/tooter /app/kiki/file_watcher
COPY config/* /app/kiki/config
COPY service/* /app/kiki/service
COPY stacker/* /app/kiki/stacker
COPY tooter/* /app/kiki/tooter
COPY file_watcher/* /app/kiki/file_watcher
RUN cd service && go mod tidy && cd ../
COPY Makefile /app/kiki/
RUN go build -C ./service -o ../kiki
RUN make
CMD [ "/app/kiki/kiki", "run" ]

View file

@ -10,33 +10,23 @@
* golang 1.24.2+
* redis
* gnu make
Далее надо пройтись по всем папкам и сделать ```go mod tidy```:
После необходимо произвести сборку утилиты с помощью команды ```make```:
```
cd config
go mod tidy
cd ../stacker
go mod tidy
cd ../tooter
go mod tidy
cd ../service
go mod tidy
cd ../
```
После уже собрать саму программу:
```
go build -C service -o ../kiki
make
```
В папке с проектом появится исполняемый файл kiki. Перед запуском необходимо заполнить конфигурационный файл config.yaml (пример заполнения представлен в файле config.example.yaml)
```
instance: https://pleroma.catgirls.asia #Инстанс, на котором находится аккаунт, в который будет происходить постинг
rss_url: https://4pda.to/feed #Новостная лента
sensitive: true #Ставить ли плашку NSFW
instance: https://pleroma.catgirls.asia #Адрес инстанса
rss_urls: #YAML массив лент
- url: https://habr.com/ru/rss/flows/admin/articles/?fl=ru #Адрес ленты
sensitive: false #Нужно ли ставить NSFW плашку
- url: https://4pda.to/feed
sensitive: false
redis:
address: localhost:6379 #Адрес Redis
address: localhost:6379 #Адрес Redis, в случае использования Docker Compose необходимо написать redis:6379
```
После чего необходимо провести инициализацию для получения секретов аккаунта. **СЕКРЕТЫ ХРАНЯТСЯ В ОТКРЫТОМ ВИДЕ, ТАК ЧТО БУДЬТЕ ОСТОРОЖНЫ!**
@ -47,6 +37,11 @@ redis:
После этого программу можно запустить командой ```./kiki run```. Перед запуском убедитесь что Redis запущен.
Если вам надо удалить утилиту и почистить все go.sum файлы, то можно выполнить следующую команду:
```
make clean
```
# Запуск в Docker
Для запуска программы в Docker необходимо создать два файла: secret.conf и config.yaml:
@ -78,9 +73,13 @@ docker compose up -d
* [x] Добавление картинок в пост
* [x] Добавление поддержки Redis
* [x] Упаковка в Docker образ
* [ ] Создание Makefile для более удобной сборки
* [ ] Поддержка обновления данных из конфига "на лету"
* [ ] Добавление поддержки нескольких лент
* [x] Создание Makefile для более удобной сборки
* [x] Поддержка обновления данных из конфига "на лету"
* [x] Добавление поддержки нескольких лент
* [ ] Добавить поддержку шаблонов
* [ ] Добавить сбор информации о инстансе, кол-ве поддерживаемых символов и кол-ве поддерживаемых медиа в одном посте. Сделать обработку этой информации и формирование постов с учетом инстансоспецифичных факторов
* [ ] Некоторые RSS-ленты содержат видео. Добавить поддержку видео в постах
* [ ] Дополнительные флаги для команды чтобы указывать где лежит конфиг
# Отказ от ответственности

View file

@ -1,5 +1,8 @@
instance: https://pleroma.catgirls.asia
rss_url: https://4pda.to/feed
sensitive: true
rss_urls:
- url: https://habr.com/ru/rss/flows/admin/articles/?fl=ru
sensitive: false
- url: https://4pda.to/feed
sensitive: false
redis:
address: localhost:6379

View file

@ -8,15 +8,19 @@ import (
"gopkg.in/yaml.v2"
)
// Сруктура config.yaml
type KikiSettings struct {
Instance string `yaml:"instance,omitempty"`
RSSUri string `yaml:"rss_url,omitempty"`
RSSURLs []struct {
Url string `yaml:"url,omitempty"`
Sensitive bool `yaml:"sensitive,omitempty"`
} `yaml:"rss_urls,omitempty"`
Redis struct {
Address string `yaml:"address"`
} `yaml:"redis"`
}
// Структура secret.conf
type MastodonClientData struct {
ClientID string `yaml:"clientID,omitempty"`
ClientSecret string `yaml:"clientSecret,omitempty"`
@ -24,6 +28,7 @@ type MastodonClientData struct {
Instance string `yaml:"instance,omitempty"`
}
// Получение данных из конфига config.yaml
func GetKikiConfig(path string) KikiSettings {
var kikiSettings KikiSettings
@ -40,6 +45,7 @@ func GetKikiConfig(path string) KikiSettings {
return kikiSettings
}
// Получение данных из конфига secret.conf
func GetSecrets(path string) *mastodon.Config {
var clientData MastodonClientData

View file

@ -10,4 +10,5 @@ services:
redis:
image: redis
restart: always
volumes:
- ./redis_data:/data

View file

@ -0,0 +1,32 @@
package file_watcher
import (
"os"
"time"
)
// Проверка на изменение в файле filename
func IsFileChange(lastMod *time.Time, filename string) bool {
fileStat, err := os.Stat(filename)
if err != nil {
return false
}
if *lastMod != fileStat.ModTime() {
*lastMod = fileStat.ModTime()
return true
} else {
return false
}
}
// Получение информации о том, когда файл изменился
func GetFileModTime(filename string) (time.Time, error) {
fileStat, err := os.Stat(filename)
if err != nil {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), err
}
return fileStat.ModTime(), nil
}

3
file_watcher/go.mod Normal file
View file

@ -0,0 +1,3 @@
module file_watcher
go 1.24.2

View file

@ -8,10 +8,13 @@ replace kiki/stacker => ../stacker
replace kiki/tooter => ../tooter
replace kiki/file_watcher => ../file_watcher/
require (
github.com/mattn/go-mastodon v0.0.9
github.com/urfave/cli/v3 v3.2.0
kiki/config v0.0.0-00010101000000-000000000000
kiki/file_watcher v0.0.0-00010101000000-000000000000
kiki/stacker v0.0.0-00010101000000-000000000000
kiki/tooter v0.0.0-00010101000000-000000000000
)

View file

@ -6,9 +6,11 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"time"
"kiki/config"
"kiki/file_watcher"
"kiki/stacker"
"kiki/tooter"
@ -23,7 +25,7 @@ func main() {
Commands: []*cli.Command{
{
Name: "init",
Usage: "Инициализировать клиента",
Usage: "Инициализировать клиента и создать secret.conf, в котором будут храниться данные для доступа к учетной записи",
Action: func(ctx context.Context, cmd *cli.Command) error {
confFile, err := filepath.Abs("config.yaml")
kikiConfig := config.GetKikiConfig(confFile)
@ -55,16 +57,31 @@ func main() {
}
kikiConfig := config.GetKikiConfig(confFile)
lastFileMod, err := file_watcher.GetFileModTime(confFile)
log.Println(lastFileMod)
if err != nil {
log.Println(err)
}
rdb := stacker.ConnectToRedis(kikiConfig.Redis.Address)
defer stacker.SaveRedis(rdb)
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
newPosts := tooter.NewsText(kikiConfig.RSSUri)
if file_watcher.IsFileChange(&lastFileMod, confFile) {
log.Println(lastFileMod)
log.Println("RSS ленты перечитаны")
kikiConfig.RSSURLs = config.GetKikiConfig(confFile).RSSURLs
}
for _, post := range newPosts {
for _, rssUrl := range kikiConfig.RSSURLs {
newPost := tooter.NewsText(rssUrl.Url)
for _, post := range slices.Backward(newPost) {
inStack, err := stacker.CheckInRedis(rdb, post.GUID)
if err != nil {
log.Println(err)
@ -73,7 +90,7 @@ func main() {
if !inStack {
log.Println(post.Description)
toot, err := tooter.CreateToot(*mastoClient, post, kikiConfig.Sensitive)
toot, err := tooter.CreateToot(*mastoClient, post, rssUrl.Sensitive)
if err != nil {
log.Println(err)
}
@ -84,6 +101,7 @@ func main() {
}
}
}
}
return nil
},
},

View file

@ -6,6 +6,7 @@ import (
"github.com/redis/go-redis/v9"
)
// Создание подключения к Redis
func ConnectToRedis(addr string) redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: addr,
@ -16,6 +17,7 @@ func ConnectToRedis(addr string) redis.Client {
return *rdb
}
// Создание записи в Redis о том, что этот пост отправлен
func SetToRedis(rdb redis.Client, key string, val interface{}) error {
err := rdb.Set(context.Background(), key, val, 0).Err()
if err != nil {
@ -24,6 +26,7 @@ func SetToRedis(rdb redis.Client, key string, val interface{}) error {
return nil
}
// Проверка есть ли пост в Redis или нет
func CheckInRedis(rdb redis.Client, key string) (bool, error) {
_, err := rdb.Get(context.Background(), key).Result()
if err == redis.Nil {
@ -34,6 +37,7 @@ func CheckInRedis(rdb redis.Client, key string) (bool, error) {
return true, nil
}
// Сохранение базы данных Redis
func SaveRedis(rdb redis.Client) error {
err := rdb.Save(context.Background()).Err()
if err != nil {

View file

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
@ -18,6 +19,7 @@ import (
"gopkg.in/yaml.v2"
)
// Функция создает файл secret.conf, в котором хранятся данные для доступа к аккаунту
func ClientConfiguration(Instance string) {
appConfig := &mastodon.AppConfig{
Server: Instance,
@ -37,7 +39,7 @@ func ClientConfiguration(Instance string) {
log.Println(err)
}
var userToken string
//fmt.Println(u)
fmt.Printf("Перейдите по ссылке\n%s\nИ введите user token ниже:\n", u)
fmt.Scanln(&userToken)
@ -67,24 +69,38 @@ func ClientConfiguration(Instance string) {
}
log.Println(string(marshaledYaml))
secretConfig, err := os.OpenFile("secret.conf", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
log.Println(err)
}
secretConfig.Write(marshaledYaml)
defer secretConfig.Close()
}
// Возвращает срез новостей, полученных из RSS ленты
func NewsText(url string) []*gofeed.Item {
fp := gofeed.NewParser()
feed, err := fp.ParseURL(url)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
log.Println("Timeout Error:", err)
return []*gofeed.Item{}
} else {
log.Println(err)
return []*gofeed.Item{}
}
}
log.Println("RSS лента получена")
return feed.Items
}
// Отправляет созданный пост
func CreatePost(mastoClient mastodon.Client, toot mastodon.Toot) {
_, err := mastoClient.PostStatus(context.Background(), &toot)
if err != nil {
@ -92,25 +108,34 @@ func CreatePost(mastoClient mastodon.Client, toot mastodon.Toot) {
}
}
// Обходит срез ссылок на изображения и возвращает срез, состоящий из представления картинок в виде срезов байтов
func PicBytesArray(picturesArray []string) [][]byte {
var picturesBytes [][]byte
for _, picture := range picturesArray {
resp, err := http.Get(picture)
if err != nil {
log.Println(err)
return picturesBytes
}
defer resp.Body.Close()
picBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return picturesBytes
}
picturesBytes = append(picturesBytes, picBytes)
}
return picturesBytes
}
// Загружает на инстанс изображения, после чего возвращает массив прикрепленных медиа в Mastodon. Необходимо для получения MediaID для каждого изображения
func UploadPictures(mastoClient mastodon.Client, filesBytes [][]byte) []*mastodon.Attachment {
var attachments []*mastodon.Attachment
@ -128,6 +153,7 @@ func UploadPictures(mastoClient mastodon.Client, filesBytes [][]byte) []*mastodo
return attachments
}
// Формирование тела статуса
func CreateToot(mastoClient mastodon.Client, newsDesc *gofeed.Item, sensitive bool) (mastodon.Toot, error) {
var imgArray []string
var attachments []*mastodon.Attachment