Compare commits

..

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

18 changed files with 301 additions and 711 deletions

3
.gitignore vendored
View file

@ -18,8 +18,7 @@ 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
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,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" ]

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,2 @@
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: false
visibility: "unlisted"
- url: https://4pda.to/feed
sensitive: false
visibility: "unlisted"
redis:
address: localhost:6379

View file

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

View file

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

View file

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

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

@ -1,25 +1,27 @@
module kiki/tooter module kiki
go 1.24.2 go 1.23.0
toolchain go1.24.2
require ( require (
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/mattn/go-mastodon v0.0.9 github.com/mattn/go-mastodon v0.0.9
github.com/mmcdole/gofeed v1.3.0 github.com/mmcdole/gofeed v1.3.0
github.com/urfave/cli/v3 v3.1.1
golang.org/x/net v0.39.0 golang.org/x/net v0.39.0
gopkg.in/yaml.v2 v2.4.0
kiki/config v0.0.0-00010101000000-000000000000
) )
require ( require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/PuerkitoBio/goquery v1.10.2 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/json-iterator/go v1.1.12 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
golang.org/x/text v0.24.0 // 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

287
main.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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