diff --git a/.gitignore b/.gitignore index 688a10d..83d7ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,7 @@ 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 deleted file mode 100644 index 777570e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM golang:1.24-alpine AS build - -RUN apk add --no-cache make - -WORKDIR /app/kiki - -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 - -COPY Makefile /app/kiki/ - -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" ] \ 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..3b02854 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,10 +1,2 @@ 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" -redis: - address: localhost:6379 \ No newline at end of file +rss_url: https://4pda.to/feed \ No newline at end of file diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 4d5e72b..0000000 --- a/config/config.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "log" - "os" - - "github.com/mattn/go-mastodon" - "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 { - Address string `yaml:"address"` - } `yaml:"redis"` -} - -// Структура secret.conf -type MastodonClientData struct { - ClientID string `yaml:"clientID,omitempty"` - ClientSecret string `yaml:"clientSecret,omitempty"` - AccessToken string `yaml:"accessToken,omitempty"` - Instance string `yaml:"instance,omitempty"` -} - -// Получение данных из конфига config.yaml -func GetKikiConfig(path string) KikiSettings { - var kikiSettings KikiSettings - - kikiConfigFile, err := os.ReadFile(path) - if err != nil { - log.Println(err) - } - - err = yaml.Unmarshal(kikiConfigFile, &kikiSettings) - if err != nil { - log.Println(err) - } - - return kikiSettings -} - -// Получение данных из конфига secret.conf -func GetSecrets(path string) *mastodon.Config { - var clientData MastodonClientData - - secretConfig, err := os.ReadFile(path) - if err != nil { - log.Println(err) - } - - err = yaml.Unmarshal(secretConfig, &clientData) - if err != nil { - log.Println(err) - } - - config := &mastodon.Config{ - Server: clientData.Instance, - ClientID: clientData.ClientID, - ClientSecret: clientData.ClientSecret, - AccessToken: clientData.AccessToken, - } - - return config -} diff --git a/config/go.mod b/config/go.mod deleted file mode 100644 index 390a727..0000000 --- a/config/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module kiki/config - -go 1.24.2 - -require ( - github.com/mattn/go-mastodon v0.0.9 - gopkg.in/yaml.v2 v2.4.0 -) - -require ( - github.com/gorilla/websocket v1.5.1 // indirect - github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect - golang.org/x/net v0.25.0 // indirect -) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index f348f4a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - kiki: - image: git.catgirls.asia/b4d_us3r/kiki:latest - restart: always - volumes: - - ./config.yaml:/app/kiki/config.yaml - - ./secret.conf:/app/kiki/secret.conf - depends_on: - - redis - 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 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/tooter/go.mod b/go.mod similarity index 50% rename from tooter/go.mod rename to go.mod index 27e1817..4c8a8ee 100644 --- a/tooter/go.mod +++ b/go.mod @@ -1,25 +1,27 @@ -module kiki/tooter +module kiki -go 1.24.2 +go 1.23.0 + +toolchain go1.24.2 require ( + github.com/go-yaml/yaml v2.1.0+incompatible github.com/mattn/go-mastodon v0.0.9 github.com/mmcdole/gofeed v1.3.0 + github.com/urfave/cli/v3 v3.1.1 golang.org/x/net v0.39.0 - gopkg.in/yaml.v2 v2.4.0 - kiki/config v0.0.0-00010101000000-000000000000 ) require ( - github.com/PuerkitoBio/goquery v1.8.0 // indirect - github.com/andybalholm/cascadia v1.3.1 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/PuerkitoBio/goquery v1.10.2 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect + github.com/mmcdole/goxpp v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect golang.org/x/text v0.24.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) - -replace kiki/config => ../config diff --git a/main.go b/main.go new file mode 100644 index 0000000..6b00be9 --- /dev/null +++ b/main.go @@ -0,0 +1,287 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-yaml/yaml" + "github.com/mattn/go-mastodon" + "github.com/mmcdole/gofeed" + "github.com/urfave/cli/v3" + "golang.org/x/net/html" +) + +type MastodonClientData struct { + ClientID string `yaml:"clientID,omitempty"` + ClientSecret string `yaml:"clientSecret,omitempty"` + AccessToken string `yaml:"accessToken,omitempty"` + Instance string `yaml:"instance,omitempty"` +} + +type KikiSettings struct { + Instance string `yaml:"instance,omitempty"` + RSSUri string `yaml:"rss_url,omitempty"` +} + +func getSecrets(path string) *mastodon.Config { + var clientData MastodonClientData + + secretConfig, err := os.ReadFile(path) + if err != nil { + log.Println(err) + } + + err = yaml.Unmarshal(secretConfig, &clientData) + if err != nil { + log.Println(err) + } + + config := &mastodon.Config{ + Server: clientData.Instance, + ClientID: clientData.ClientID, + ClientSecret: clientData.ClientSecret, + AccessToken: clientData.AccessToken, + } + + return config +} + +func getKikiConfig(path string) KikiSettings { + var kikiSettings KikiSettings + + kikiConfigFile, err := os.ReadFile(path) + if err != nil { + log.Println(err) + } + + err = yaml.Unmarshal(kikiConfigFile, &kikiSettings) + if err != nil { + log.Println(err) + } + + return kikiSettings +} + +func clientConfiguration(Instance string) { + appConfig := &mastodon.AppConfig{ + Server: Instance, + ClientName: "Kiki", + Scopes: "read write follow", + Website: "catgirls.asia", + RedirectURIs: "urn:ietf:wg:oauth:2.0:oob", + } + + app, err := mastodon.RegisterApp(context.Background(), appConfig) + if err != nil { + log.Println(err) + } + + u, err := url.Parse(app.AuthURI) + if err != nil { + log.Println(err) + } + var userToken string + fmt.Println(u) + fmt.Scanln(&userToken) + + config := &mastodon.Config{ + Server: Instance, + ClientID: app.ClientID, + ClientSecret: app.ClientSecret, + AccessToken: userToken, + } + + mastoClient := mastodon.NewClient(config) + err = mastoClient.AuthenticateToken(context.Background(), userToken, "urn:ietf:wg:oauth:2.0:oob") + if err != nil { + log.Println(err) + } + + clientData := MastodonClientData{ + Instance: Instance, + ClientID: mastoClient.Config.ClientID, + ClientSecret: mastoClient.Config.ClientSecret, + AccessToken: mastoClient.Config.AccessToken, + } + + marshaledYaml, err := yaml.Marshal(clientData) + if err != nil { + log.Println(err) + } + + 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() +} + +func newsText(url string) []*gofeed.Item { + fp := gofeed.NewParser() + feed, err := fp.ParseURL(url) + if err != nil { + 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 { + log.Println(err) + } +} + +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 +} + +func uploadPictures(mastoClient mastodon.Client, filesBytes [][]byte) []*mastodon.Attachment { + var attachments []*mastodon.Attachment + + for _, file := range filesBytes { + att, err := mastoClient.UploadMediaFromBytes(context.Background(), file) + + if err != nil { + log.Println(err) + return attachments + } + + attachments = append(attachments, att) + } + + return attachments +} + +func createToot(mastoClient mastodon.Client, newsDesc string) (mastodon.Toot, error) { + var tootText string + var imgArray []string + var attachments []*mastodon.Attachment + + toot := mastodon.Toot{ + Visibility: "unlisted", + Sensitive: true, + } + + uString := html.UnescapeString(newsDesc) + pHtml, err := html.Parse(strings.NewReader(uString)) + if err != nil { + return mastodon.Toot{}, err + } + + for n := range pHtml.Descendants() { + if n.Type != html.ElementNode { + tootText += (n.Data + "\n") + } + if n.Type == html.ElementNode && n.Data == "img" { + for _, attr := range n.Attr { + if attr.Key == "src" { + imgArray = append(imgArray, attr.Val) + } + } + } + } + + if len(imgArray) != 0 { + attachments = uploadPictures(mastoClient, picBytesArray(imgArray)) + } + + toot.Status = tootText + for _, attach := range attachments { + toot.MediaIDs = append(toot.MediaIDs, attach.ID) + } + + return toot, nil +} + +func main() { + cmd := &cli.Command{ + Name: "kiki", + Usage: "Ретранслятор из RSS в Mastodon. Когда-нибудь...", + Commands: []*cli.Command{ + { + Name: "init", + Usage: "Инициализировать клиента", + Action: func(ctx context.Context, cmd *cli.Command) error { + confFile, err := filepath.Abs("config.yaml") + kikiConfig := getKikiConfig(confFile) + + if err != nil { + log.Println(err) + } + + instanceUrlParser, err := url.Parse(kikiConfig.Instance) + if err != nil { + log.Println(err) + } + instanceUrlParser.Scheme = "https" + + clientConfiguration(instanceUrlParser.String()) + + return nil + }, + }, + { + Name: "run", + Usage: "Запуск транслятора", + Action: func(ctx context.Context, cmd *cli.Command) error { + var lastGUID string + mastoClient := mastodon.NewClient(getSecrets("secret.conf")) + + confFile, err := filepath.Abs("config.yaml") + if err != nil { + log.Println(err) + } + kikiConfig := getKikiConfig(confFile) + + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for range ticker.C { + news := newsText(kikiConfig.RSSUri) + if news[0].GUID != lastGUID { + log.Println(news[0].Description) + + toot, err := createToot(*mastoClient, news[0].Description) + if err != nil { + log.Println(err) + } + + createPost(*mastoClient, toot) + lastGUID = news[0].GUID + } + } + return nil + }, + }, + }, + } + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/service/go.mod b/service/go.mod deleted file mode 100644 index 9475995..0000000 --- a/service/go.mod +++ /dev/null @@ -1,38 +0,0 @@ -module kiki/kiki - -go 1.24.2 - -replace kiki/config => ../config - -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 -) - -require ( - github.com/PuerkitoBio/goquery v1.8.0 // indirect - github.com/andybalholm/cascadia v1.3.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gorilla/websocket v1.5.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/mmcdole/gofeed v1.3.0 // indirect - github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/redis/go-redis/v9 v9.7.3 // indirect - github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/text v0.24.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/service/kiki.go b/service/kiki.go deleted file mode 100644 index e1d4ed1..0000000 --- a/service/kiki.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "context" - "log" - "net/url" - "os" - "path/filepath" - "slices" - "time" - - "kiki/config" - "kiki/file_watcher" - "kiki/stacker" - "kiki/tooter" - - "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v3" -) - -func main() { - cmd := &cli.Command{ - Name: "kiki", - Usage: "Ретранслятор из RSS в Mastodon. Когда-нибудь...", - Commands: []*cli.Command{ - { - Name: "init", - Usage: "Инициализировать клиента и создать secret.conf, в котором будут храниться данные для доступа к учетной записи", - Action: func(ctx context.Context, cmd *cli.Command) error { - confFile, err := filepath.Abs("config.yaml") - kikiConfig := config.GetKikiConfig(confFile) - - if err != nil { - log.Println(err) - } - - instanceUrlParser, err := url.Parse(kikiConfig.Instance) - if err != nil { - log.Println(err) - } - instanceUrlParser.Scheme = "https" - - tooter.ClientConfiguration(instanceUrlParser.String()) - - return nil - }, - }, - { - Name: "run", - Usage: "Запуск транслятора", - Action: func(ctx context.Context, cmd *cli.Command) error { - mastoClient := mastodon.NewClient(config.GetSecrets("secret.conf")) - - confFile, err := filepath.Abs("config.yaml") - if err != nil { - log.Println(err) - } - 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 - } - - 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) - } - - if !inStack { - log.Println(post.Description) - - 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) - } - } - } - } - return nil - }, - }, - }, - } - if err := cmd.Run(context.Background(), os.Args); err != nil { - log.Fatal(err) - } -} diff --git a/stacker/go.mod b/stacker/go.mod deleted file mode 100644 index 4b1afb2..0000000 --- a/stacker/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module kiki/stacker - -go 1.24.2 - -require github.com/redis/go-redis/v9 v9.7.3 - -require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect -) diff --git a/stacker/stacker.go b/stacker/stacker.go deleted file mode 100644 index 28d2933..0000000 --- a/stacker/stacker.go +++ /dev/null @@ -1,47 +0,0 @@ -package stacker - -import ( - "context" - - "github.com/redis/go-redis/v9" -) - -// Создание подключения к Redis -func ConnectToRedis(addr string) redis.Client { - rdb := redis.NewClient(&redis.Options{ - Addr: addr, - Password: "", - DB: 0, - }) - - 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 { - return err - } - return nil -} - -// Проверка есть ли пост в Redis или нет -func CheckInRedis(rdb redis.Client, key string) (bool, error) { - _, err := rdb.Get(context.Background(), key).Result() - if err == redis.Nil { - return false, nil - } else if err != nil { - return false, err - } - return true, nil -} - -// Сохранение базы данных Redis -func SaveRedis(rdb redis.Client) error { - err := rdb.Save(context.Background()).Err() - if err != nil { - return err - } - return nil -} diff --git a/tooter/tooter.go b/tooter/tooter.go deleted file mode 100644 index 3ae99f3..0000000 --- a/tooter/tooter.go +++ /dev/null @@ -1,205 +0,0 @@ -package tooter - -import ( - "context" - "fmt" - "io" - "log" - "net" - "net/http" - "net/url" - "os" - "strings" - - "kiki/config" - - "github.com/mattn/go-mastodon" - "github.com/mmcdole/gofeed" - "golang.org/x/net/html" - "gopkg.in/yaml.v2" -) - -// Функция создает файл secret.conf, в котором хранятся данные для доступа к аккаунту -func ClientConfiguration(Instance string) { - appConfig := &mastodon.AppConfig{ - Server: Instance, - ClientName: "Kiki", - Scopes: "read write follow", - Website: "catgirls.asia", - RedirectURIs: "urn:ietf:wg:oauth:2.0:oob", - } - - app, err := mastodon.RegisterApp(context.Background(), appConfig) - if err != nil { - log.Println(err) - } - - u, err := url.Parse(app.AuthURI) - if err != nil { - log.Println(err) - } - var userToken string - - fmt.Printf("Перейдите по ссылке\n%s\nИ введите user token ниже:\n", u) - fmt.Scanln(&userToken) - - conf := &mastodon.Config{ - Server: Instance, - ClientID: app.ClientID, - ClientSecret: app.ClientSecret, - AccessToken: userToken, - } - - mastoClient := mastodon.NewClient(conf) - err = mastoClient.AuthenticateToken(context.Background(), userToken, "urn:ietf:wg:oauth:2.0:oob") - if err != nil { - log.Println(err) - } - - clientData := config.MastodonClientData{ - Instance: Instance, - ClientID: mastoClient.Config.ClientID, - ClientSecret: mastoClient.Config.ClientSecret, - AccessToken: mastoClient.Config.AccessToken, - } - - marshaledYaml, err := yaml.Marshal(clientData) - if err != nil { - log.Println(err) - } - - 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 { - log.Println(err) - } -} - -// Обходит срез ссылок на изображения и возвращает срез, состоящий из представления картинок в виде срезов байтов -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 - - for _, file := range filesBytes { - att, err := mastoClient.UploadMediaFromBytes(context.Background(), file) - - if err != nil { - log.Println(err) - return attachments - } - - attachments = append(attachments, att) - } - - return attachments -} - -// Формирование тела статуса -func CreateToot(mastoClient mastodon.Client, newsDesc *gofeed.Item, sensitive bool, visibility string) (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, - Sensitive: sensitive, - } - - uString := html.UnescapeString(newsDesc.Description) - pHtml, err := html.Parse(strings.NewReader(uString)) - if err != nil { - return mastodon.Toot{}, err - } - - for n := range pHtml.Descendants() { - if n.Type != html.ElementNode { - tootText += (n.Data) - } - - if n.Type == html.ElementNode { - switch n.Data { - case "img": - for _, attr := range n.Attr { - if attr.Key == "src" { - imgArray = append(imgArray, attr.Val) - } - } - - case "br", "p": - tootText += "\n" - } - } - - } - - if len(imgArray) != 0 { - attachments = UploadPictures(mastoClient, PicBytesArray(imgArray)) - } - - toot.Status = tootText - for _, attach := range attachments { - toot.MediaIDs = append(toot.MediaIDs, attach.ID) - } - - return toot, nil -}