Compare commits

..

No commits in common. "master" and "watch_file_change" have entirely different histories.

10 changed files with 51 additions and 111 deletions

1
.gitignore vendored
View file

@ -19,7 +19,6 @@ config.yaml
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
kiki kiki
redis_data
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/

View file

@ -1,6 +1,4 @@
FROM golang:1.24-alpine AS build FROM golang:1.24
RUN apk add --no-cache make
WORKDIR /app/kiki WORKDIR /app/kiki
@ -16,12 +14,4 @@ COPY Makefile /app/kiki/
RUN make RUN make
FROM alpine:latest
WORKDIR /app/kiki/
COPY --from=build /app/kiki/kiki /app/kiki/kiki
RUN chmod +x /app/kiki/kiki
CMD [ "/app/kiki/kiki", "run" ] CMD [ "/app/kiki/kiki", "run" ]

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
services: services:
kiki: kiki:
image: git.catgirls.asia/b4d_us3r/kiki:latest build: ./
restart: always restart: always
volumes: volumes:
- ./config.yaml:/app/kiki/config.yaml - ./config.yaml:/app/kiki/config.yaml
@ -10,5 +10,4 @@ services:
redis: redis:
image: redis image: redis
restart: always restart: always
volumes:
- ./redis_data:/data

View file

@ -5,7 +5,6 @@ import (
"time" "time"
) )
// Проверка на изменение в файле filename
func IsFileChange(lastMod *time.Time, filename string) bool { func IsFileChange(lastMod *time.Time, filename string) bool {
fileStat, err := os.Stat(filename) fileStat, err := os.Stat(filename)
@ -21,7 +20,6 @@ func IsFileChange(lastMod *time.Time, filename string) bool {
} }
} }
// Получение информации о том, когда файл изменился
func GetFileModTime(filename string) (time.Time, error) { func GetFileModTime(filename string) (time.Time, error) {
fileStat, err := os.Stat(filename) fileStat, err := os.Stat(filename)

View file

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"time" "time"
"kiki/config" "kiki/config"
@ -25,7 +24,7 @@ func main() {
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "init", Name: "init",
Usage: "Инициализировать клиента и создать secret.conf, в котором будут храниться данные для доступа к учетной записи", Usage: "Инициализировать клиента",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
confFile, err := filepath.Abs("config.yaml") confFile, err := filepath.Abs("config.yaml")
kikiConfig := config.GetKikiConfig(confFile) kikiConfig := config.GetKikiConfig(confFile)
@ -75,30 +74,28 @@ func main() {
if file_watcher.IsFileChange(&lastFileMod, confFile) { if file_watcher.IsFileChange(&lastFileMod, confFile) {
log.Println(lastFileMod) log.Println(lastFileMod)
log.Println("RSS ленты перечитаны") log.Println("RSS ленты перечитаны")
kikiConfig.RSSURLs = config.GetKikiConfig(confFile).RSSURLs kikiConfig.RSSUri = config.GetKikiConfig(confFile).RSSUri
} }
for _, rssUrl := range kikiConfig.RSSURLs { newPosts := tooter.NewsText(kikiConfig.RSSUri)
newPost := tooter.NewsText(rssUrl.Url)
for _, post := range slices.Backward(newPost) { for _, post := range newPosts {
inStack, err := stacker.CheckInRedis(rdb, post.GUID) 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)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
if !inStack { tooter.CreatePost(*mastoClient, toot)
log.Println(post.Description)
toot, err := tooter.CreateToot(*mastoClient, post, rssUrl.Sensitive, rssUrl.Visibility) stacker.SetToRedis(rdb, post.GUID, post.Description)
if err != nil {
log.Println(err)
}
tooter.CreatePost(*mastoClient, toot)
stacker.SetToRedis(rdb, post.GUID, post.Description)
}
} }
} }
} }

View file

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

View file

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