Compare commits

..

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

14 changed files with 29 additions and 256 deletions

1
.gitignore vendored
View file

@ -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/

View file

@ -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" ]

View file

@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
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.

View file

@ -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

View file

@ -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-ленты содержат видео. Добавить поддержку видео в постах
* [ ] Дополнительные флаги для команды чтобы указывать где лежит конфиг
# Отказ от ответственности
Программное обеспечение поставляется "как есть" и автор не несет ответственности на нарушение работы ЭВМ

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

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

View file

@ -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
)

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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,
}