From 66afe8eac72a996b1d65dfff5573f2d32d22cccf Mon Sep 17 00:00:00 2001 From: B4D_US3R Date: Wed, 30 Apr 2025 10:19:33 +0500 Subject: [PATCH 1/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D0=B5.=20=D0=9F=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D0=BC=20=D0=BA=D0=B0=D0=BA=20=D0=B1=D1=83=D0=B4=D0=B5?= =?UTF-8?q?=D1=82=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 ++++--- file_watcher/file_watcher.go | 30 ++++++++++++++++++++++++++++++ file_watcher/go.mod | 3 +++ service/go.mod | 3 +++ service/kiki.go | 15 +++++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 file_watcher/file_watcher.go create mode 100644 file_watcher/go.mod diff --git a/Dockerfile b/Dockerfile index a6020e3..0ade8a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] \ No newline at end of file diff --git a/file_watcher/file_watcher.go b/file_watcher/file_watcher.go new file mode 100644 index 0000000..52b8a98 --- /dev/null +++ b/file_watcher/file_watcher.go @@ -0,0 +1,30 @@ +package file_watcher + +import ( + "os" + "time" +) + +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 +} diff --git a/file_watcher/go.mod b/file_watcher/go.mod new file mode 100644 index 0000000..760830b --- /dev/null +++ b/file_watcher/go.mod @@ -0,0 +1,3 @@ +module file_watcher + +go 1.24.2 diff --git a/service/go.mod b/service/go.mod index 7f97ff1..9475995 100644 --- a/service/go.mod +++ b/service/go.mod @@ -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 ) diff --git a/service/kiki.go b/service/kiki.go index 420c0dd..bb462ce 100644 --- a/service/kiki.go +++ b/service/kiki.go @@ -9,6 +9,7 @@ import ( "time" "kiki/config" + "kiki/file_watcher" "kiki/stacker" "kiki/tooter" @@ -55,13 +56,27 @@ 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 { + if file_watcher.IsFileChange(&lastFileMod, confFile) { + log.Println(lastFileMod) + log.Println("RSS ленты перечитаны") + kikiConfig.RSSUri = config.GetKikiConfig(confFile).RSSUri + } + newPosts := tooter.NewsText(kikiConfig.RSSUri) for _, post := range newPosts { From 47242e54319fcf59b8c2489691836b78c8bcd541 Mon Sep 17 00:00:00 2001 From: B4D_US3R Date: Wed, 30 Apr 2025 15:59:44 +0500 Subject: [PATCH 2/4] =?UTF-8?q?=D0=9C=D1=83=D0=BB=D1=8C=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0,=20=D1=82=D0=B5=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F.=20=D0=9F=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 10 ++++++---- service/kiki.go | 30 ++++++++++++++++-------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/config/config.go b/config/config.go index b84cd21..dd8b353 100644 --- a/config/config.go +++ b/config/config.go @@ -9,10 +9,12 @@ import ( ) type KikiSettings struct { - Instance string `yaml:"instance,omitempty"` - RSSUri string `yaml:"rss_url,omitempty"` - Sensitive bool `yaml:"sensitive,omitempty"` - Redis struct { + Instance string `yaml:"instance,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"` } diff --git a/service/kiki.go b/service/kiki.go index bb462ce..52901a9 100644 --- a/service/kiki.go +++ b/service/kiki.go @@ -74,28 +74,30 @@ func main() { if file_watcher.IsFileChange(&lastFileMod, confFile) { log.Println(lastFileMod) log.Println("RSS ленты перечитаны") - kikiConfig.RSSUri = config.GetKikiConfig(confFile).RSSUri + kikiConfig.RSSURLs = config.GetKikiConfig(confFile).RSSURLs } - newPosts := tooter.NewsText(kikiConfig.RSSUri) + for _, rssUrl := range kikiConfig.RSSURLs { + newPost := tooter.NewsText(rssUrl.Url) - for _, post := range newPosts { - inStack, err := stacker.CheckInRedis(rdb, post.GUID) - if err != nil { - log.Println(err) - } - - if !inStack { - log.Println(post.Description) - - toot, err := tooter.CreateToot(*mastoClient, post, kikiConfig.Sensitive) + for _, post := range newPost { + inStack, err := stacker.CheckInRedis(rdb, post.GUID) if err != nil { log.Println(err) } - tooter.CreatePost(*mastoClient, toot) + if !inStack { + log.Println(post.Description) - stacker.SetToRedis(rdb, post.GUID, post.Description) + toot, err := tooter.CreateToot(*mastoClient, post, rssUrl.Sensitive) + if err != nil { + log.Println(err) + } + + tooter.CreatePost(*mastoClient, toot) + + stacker.SetToRedis(rdb, post.GUID, post.Description) + } } } } From a79933b724d328719d95a2be8cf82b4ccaadcd79 Mon Sep 17 00:00:00 2001 From: B4D_US3R Date: Sun, 4 May 2025 16:14:34 +0500 Subject: [PATCH 3/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=82=D0=B0=D0=B9=D0=BC?= =?UTF-8?q?=D0=B0=D1=83=D1=82=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20NewsText=20=D0=BD=D0=B8=D1=87=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0?= =?UTF-8?q?=D0=BB=D0=B0=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=BC=D0=B0=20=D0=BF=D0=B0=D0=B4=D0=B0=D0=BB=D0=B0.=20?= =?UTF-8?q?=D0=A2=D0=B0=D0=BA=20=D0=B6=D0=B5=20=D1=82=D0=B5=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D1=8C=20=D0=BB=D0=B5=D0=BD=D1=82=D0=B0=20=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=D1=87=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=82=20=D1=81=D0=B0=D0=BC=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=81=D1=82=D0=B0=D1=80=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D1=82=D0=B0,=20=D0=B7=D0=B0=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D1=87=D0=B8=D0=B2=D0=B0=D1=8F=20=D1=81=D0=B0=D0=BC=D1=8B?= =?UTF-8?q?=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 45 ++++++++++++++++++------------------ config.example.yaml | 7 ++++-- config/config.go | 4 ++++ docker-compose.yml | 2 ++ file_watcher/file_watcher.go | 2 ++ service/kiki.go | 5 ++-- stacker/stacker.go | 4 ++++ tooter/tooter.go | 30 ++++++++++++++++++++++-- 8 files changed, 70 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3edeb29..5067564 100644 --- a/README.md +++ b/README.md @@ -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-ленты содержат видео. Добавить поддержку видео в постах +* [ ] Дополнительные флаги для команды чтобы указывать где лежит конфиг # Отказ от ответственности diff --git a/config.example.yaml b/config.example.yaml index 10cff5e..3450bc5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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 \ No newline at end of file diff --git a/config/config.go b/config/config.go index dd8b353..014aca8 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,7 @@ import ( "gopkg.in/yaml.v2" ) +// Сруктура config.yaml type KikiSettings struct { Instance string `yaml:"instance,omitempty"` RSSURLs []struct { @@ -19,6 +20,7 @@ type KikiSettings struct { } `yaml:"redis"` } +// Структура secret.conf type MastodonClientData struct { ClientID string `yaml:"clientID,omitempty"` ClientSecret string `yaml:"clientSecret,omitempty"` @@ -26,6 +28,7 @@ type MastodonClientData struct { Instance string `yaml:"instance,omitempty"` } +// Получение данных из конфига config.yaml func GetKikiConfig(path string) KikiSettings { var kikiSettings KikiSettings @@ -42,6 +45,7 @@ func GetKikiConfig(path string) KikiSettings { return kikiSettings } +// Получение данных из конфига secret.conf func GetSecrets(path string) *mastodon.Config { var clientData MastodonClientData diff --git a/docker-compose.yml b/docker-compose.yml index 3e8a0af..491b637 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,4 +10,6 @@ services: redis: image: redis restart: always + volumes: + - ./redis_data:/data \ No newline at end of file diff --git a/file_watcher/file_watcher.go b/file_watcher/file_watcher.go index 52b8a98..35d2311 100644 --- a/file_watcher/file_watcher.go +++ b/file_watcher/file_watcher.go @@ -5,6 +5,7 @@ import ( "time" ) +// Проверка на изменение в файле filename func IsFileChange(lastMod *time.Time, filename string) bool { fileStat, err := os.Stat(filename) @@ -20,6 +21,7 @@ func IsFileChange(lastMod *time.Time, filename string) bool { } } +// Получение информации о том, когда файл изменился func GetFileModTime(filename string) (time.Time, error) { fileStat, err := os.Stat(filename) diff --git a/service/kiki.go b/service/kiki.go index 52901a9..bc2cc75 100644 --- a/service/kiki.go +++ b/service/kiki.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "time" "kiki/config" @@ -24,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) @@ -80,7 +81,7 @@ func main() { for _, rssUrl := range kikiConfig.RSSURLs { newPost := tooter.NewsText(rssUrl.Url) - for _, post := range newPost { + for _, post := range slices.Backward(newPost) { inStack, err := stacker.CheckInRedis(rdb, post.GUID) if err != nil { log.Println(err) diff --git a/stacker/stacker.go b/stacker/stacker.go index 8f3de73..28d2933 100644 --- a/stacker/stacker.go +++ b/stacker/stacker.go @@ -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 { diff --git a/tooter/tooter.go b/tooter/tooter.go index 69fbb60..42229ba 100644 --- a/tooter/tooter.go +++ b/tooter/tooter.go @@ -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 { - log.Println(err) + 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 From 0373a1adddcf83d1606bb09563f668bd78a7988c Mon Sep 17 00:00:00 2001 From: B4D_US3R Date: Sun, 4 May 2025 16:23:30 +0500 Subject: [PATCH 4/4] =?UTF-8?q?=D0=9B=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B1=D0=B5=D0=BB=D1=8B=20=D0=B2=20docker=20?= =?UTF-8?q?compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 491b637..4b7dbc1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,5 +11,4 @@ services: image: redis restart: always volumes: - - ./redis_data:/data - \ No newline at end of file + - ./redis_data:/data \ No newline at end of file