diff --git a/.gitignore b/.gitignore index 688a10d..867ccf5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ config.yaml # Output of the go coverage tool, specifically when used with LiteIDE *.out kiki -redis_data # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/Dockerfile b/Dockerfile index 777570e..a6020e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,16 @@ -FROM golang:1.24-alpine AS build - -RUN apk add --no-cache make +FROM golang:1.24 WORKDIR /app/kiki -RUN mkdir /app/kiki/config /app/kiki/service /app/kiki/stacker /app/kiki/tooter /app/kiki/file_watcher +RUN mkdir /app/kiki/config /app/kiki/service /app/kiki/stacker /app/kiki/tooter 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 -COPY Makefile /app/kiki/ +RUN cd service && go mod tidy && cd ../ -RUN make - -FROM alpine:latest - -WORKDIR /app/kiki/ - -COPY --from=build /app/kiki/kiki /app/kiki/kiki - -RUN chmod +x /app/kiki/kiki +RUN go build -C ./service -o ../kiki CMD [ "/app/kiki/kiki", "run" ] \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 9b14b6d..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - -Copyright (C) 2004 Sam Hocevar - -Everyone is permitted to copy and distribute verbatim or modified -copies of this license document, and changing it is allowed as long -as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/Makefile b/Makefile deleted file mode 100644 index df22fcc..0000000 --- a/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -kiki : tooter/go.sum config/go.sum stacker/go.sum service/go.sum service/kiki - mv service/kiki kiki - -tooter/go.sum : tooter/go.mod - cd tooter && go mod tidy && cd ../ -config/go.sum : config/go.mod - cd config && go mod tidy && cd ../ -stacker/go.sum : stacker/go.mod - cd stacker && go mod tidy && cd ../ -service/go.sum : service/go.mod - cd service && go mod tidy && cd ../ -service/kiki : service/kiki.go - go build -C service -clean : - rm kiki tooter/go.sum config/go.sum stacker/go.sum service/go.sum \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index daabdb8..0000000 --- a/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Kiki - RSS -> MastodonAPI crossposter - -Данная программа представляет собой кросспостер из лент RSS в Mastodon-совместимые сервера. Увы, программа пока тестировалась только на Pleroma - -### ⚠ ПРОВЕРКИ НА КОЛИЧЕСТВО ПОДДЕРЖИВАЕМЫХ СЕРВЕРОМ СИМВОЛОВ НЕТ, НЕИЗВЕСТНО КАК ПРОГРАММА СЕБЯ ПОВЕДЕТ ЕСЛИ В НОВОСТИ 500+ СИМВОЛОВ - -# Руководство по сборке и использованию - -Для начала проверьте зависимости: - -* golang 1.24.2+ -* redis -* gnu make - -После необходимо произвести сборку утилиты с помощью команды ```make```: -``` -make -``` - -В папке с проектом появится исполняемый файл kiki. Перед запуском необходимо заполнить конфигурационный файл config.yaml (пример заполнения представлен в файле config.example.yaml) -``` -instance: https://pleroma.catgirls.asia #Адрес инстанса -rss_urls: #YAML массив лент - - url: https://habr.com/ru/rss/flows/admin/articles/?fl=ru #Адрес ленты - sensitive: false #Нужно ли ставить NSFW плашку - visibility: "unlisted" #Режим отображения постов. Есть public, unlisted, private, direct - - url: https://4pda.to/feed - sensitive: false - visibility: "unlisted" -redis: - address: localhost:6379 #Адрес Redis, в случае использования Docker Compose необходимо написать redis:6379 -``` - -После чего необходимо провести инициализацию для получения секретов аккаунта. **СЕКРЕТЫ ХРАНЯТСЯ В ОТКРЫТОМ ВИДЕ, ТАК ЧТО БУДЬТЕ ОСТОРОЖНЫ!** -``` -./kiki init -``` -Программа в коммандной строке выдаст ссылку на получение прав. Необходимо по ней перейти, дать разрешения, скопировать OAuth код, вставить в коммандную строку и нажать Enter. После этого должен создаться файл secret.conf. - -После этого программу можно запустить командой ```./kiki run```. Перед запуском убедитесь что Redis запущен. - -Если вам надо удалить утилиту и почистить все go.sum файлы, то можно выполнить следующую команду: -``` -make clean -``` - -# Запуск в Docker - -Для запуска программы в Docker необходимо создать два файла: secret.conf и config.yaml: -``` -touch secret.conf -touch config.yaml -``` - -Далее необходимо собрать Docker образ с утилитой: -``` -docker compose build -``` - -После этого необходимо провести инициализацию: -``` -docker compose run --rm kiki /app/kiki/kiki init -``` - -Программа в коммандной строке выдаст ссылку на получение прав. Необходимо по ней перейти, дать разрешения, скопировать OAuth код, вставить в коммандную строку и нажать Enter. В файле secret.conf должны появиться данные для использования аккаунта. Проверьте их наличие с помощью команды ```cat secret.conf``` - -После чего можно запустить контейнер с программой: -``` -docker compose up -d -``` - -# TODO: - -* [x] Первая реализация -* [x] Добавление картинок в пост -* [x] Добавление поддержки Redis -* [x] Упаковка в Docker образ -* [x] Создание Makefile для более удобной сборки -* [x] Поддержка обновления данных из конфига "на лету" -* [x] Добавление поддержки нескольких лент -* [ ] Добавить поддержку шаблонов -* [ ] Добавить сбор информации о инстансе, кол-ве поддерживаемых символов и кол-ве поддерживаемых медиа в одном посте. Сделать обработку этой информации и формирование постов с учетом инстансоспецифичных факторов -* [ ] Некоторые RSS-ленты содержат видео. Добавить поддержку видео в постах -* [ ] Дополнительные флаги для команды чтобы указывать где лежит конфиг - -# Отказ от ответственности - -Программное обеспечение поставляется "как есть" и автор не несет ответственности на нарушение работы ЭВМ \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml index ce83de1..10cff5e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,10 +1,5 @@ instance: https://pleroma.catgirls.asia -rss_urls: - - url: https://habr.com/ru/rss/flows/admin/articles/?fl=ru - sensitive: false - visibility: "unlisted" - - url: https://4pda.to/feed - sensitive: false - visibility: "unlisted" +rss_url: https://4pda.to/feed +sensitive: true redis: address: localhost:6379 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 4d5e72b..b84cd21 100644 --- a/config/config.go +++ b/config/config.go @@ -8,20 +8,15 @@ import ( "gopkg.in/yaml.v2" ) -// Сруктура config.yaml type KikiSettings struct { - Instance string `yaml:"instance,omitempty"` - RSSURLs []struct { - Url string `yaml:"url,omitempty"` - Sensitive bool `yaml:"sensitive,omitempty"` - Visibility string `yaml:"visibility"` - } `yaml:"rss_urls,omitempty"` - Redis struct { + Instance string `yaml:"instance,omitempty"` + RSSUri string `yaml:"rss_url,omitempty"` + Sensitive bool `yaml:"sensitive,omitempty"` + Redis struct { Address string `yaml:"address"` } `yaml:"redis"` } -// Структура secret.conf type MastodonClientData struct { ClientID string `yaml:"clientID,omitempty"` ClientSecret string `yaml:"clientSecret,omitempty"` @@ -29,7 +24,6 @@ type MastodonClientData struct { Instance string `yaml:"instance,omitempty"` } -// Получение данных из конфига config.yaml func GetKikiConfig(path string) KikiSettings { var kikiSettings KikiSettings @@ -46,7 +40,6 @@ 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 f348f4a..3e8a0af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: kiki: - image: git.catgirls.asia/b4d_us3r/kiki:latest + build: ./ restart: always volumes: - ./config.yaml:/app/kiki/config.yaml @@ -10,5 +10,4 @@ services: redis: image: redis restart: always - volumes: - - ./redis_data:/data \ No newline at end of file + \ No newline at end of file diff --git a/file_watcher/file_watcher.go b/file_watcher/file_watcher.go deleted file mode 100644 index 35d2311..0000000 --- a/file_watcher/file_watcher.go +++ /dev/null @@ -1,32 +0,0 @@ -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 -} diff --git a/file_watcher/go.mod b/file_watcher/go.mod deleted file mode 100644 index 760830b..0000000 --- a/file_watcher/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module file_watcher - -go 1.24.2 diff --git a/service/go.mod b/service/go.mod index 9475995..7f97ff1 100644 --- a/service/go.mod +++ b/service/go.mod @@ -8,13 +8,10 @@ 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 e1d4ed1..420c0dd 100644 --- a/service/kiki.go +++ b/service/kiki.go @@ -6,11 +6,9 @@ import ( "net/url" "os" "path/filepath" - "slices" "time" "kiki/config" - "kiki/file_watcher" "kiki/stacker" "kiki/tooter" @@ -25,7 +23,7 @@ func main() { Commands: []*cli.Command{ { Name: "init", - Usage: "Инициализировать клиента и создать secret.conf, в котором будут храниться данные для доступа к учетной записи", + Usage: "Инициализировать клиента", Action: func(ctx context.Context, cmd *cli.Command) error { confFile, err := filepath.Abs("config.yaml") kikiConfig := config.GetKikiConfig(confFile) @@ -57,48 +55,32 @@ 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.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) + } - for _, post := range slices.Backward(newPost) { - inStack, err := stacker.CheckInRedis(rdb, post.GUID) + if !inStack { + log.Println(post.Description) + + toot, err := tooter.CreateToot(*mastoClient, post, kikiConfig.Sensitive) if err != nil { log.Println(err) } - if !inStack { - log.Println(post.Description) + tooter.CreatePost(*mastoClient, toot) - toot, err := tooter.CreateToot(*mastoClient, post, rssUrl.Sensitive, rssUrl.Visibility) - if err != nil { - log.Println(err) - } - - tooter.CreatePost(*mastoClient, toot) - - stacker.SetToRedis(rdb, post.GUID, post.Description) - } + stacker.SetToRedis(rdb, post.GUID, post.Description) } } } diff --git a/stacker/stacker.go b/stacker/stacker.go index 28d2933..8f3de73 100644 --- a/stacker/stacker.go +++ b/stacker/stacker.go @@ -6,7 +6,6 @@ import ( "github.com/redis/go-redis/v9" ) -// Создание подключения к Redis func ConnectToRedis(addr string) redis.Client { rdb := redis.NewClient(&redis.Options{ Addr: addr, @@ -17,7 +16,6 @@ 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 { @@ -26,7 +24,6 @@ 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 { @@ -37,7 +34,6 @@ 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 3ae99f3..69fbb60 100644 --- a/tooter/tooter.go +++ b/tooter/tooter.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "log" - "net" "net/http" "net/url" "os" @@ -19,7 +18,6 @@ import ( "gopkg.in/yaml.v2" ) -// Функция создает файл secret.conf, в котором хранятся данные для доступа к аккаунту func ClientConfiguration(Instance string) { appConfig := &mastodon.AppConfig{ Server: Instance, @@ -39,7 +37,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) @@ -69,38 +67,24 @@ 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(err) } - log.Println("RSS лента получена") - return feed.Items } -// Отправляет созданный пост func CreatePost(mastoClient mastodon.Client, toot mastodon.Toot) { _, err := mastoClient.PostStatus(context.Background(), &toot) if err != nil { @@ -108,34 +92,25 @@ 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 @@ -153,15 +128,14 @@ func UploadPictures(mastoClient mastodon.Client, filesBytes [][]byte) []*mastodo return attachments } -// Формирование тела статуса -func CreateToot(mastoClient mastodon.Client, newsDesc *gofeed.Item, sensitive bool, visibility string) (mastodon.Toot, error) { +func CreateToot(mastoClient mastodon.Client, newsDesc *gofeed.Item, sensitive bool) (mastodon.Toot, error) { var imgArray []string var attachments []*mastodon.Attachment var tootText string = fmt.Sprintf("src: %s\n\n", newsDesc.Link) toot := mastodon.Toot{ - Visibility: visibility, + Visibility: "unlisted", Sensitive: sensitive, }