Compare commits
18 commits
img_extrac
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9455220a0b | |||
| 4f998a2730 | |||
| 9fa91004bf | |||
| 0373a1addd | |||
| a79933b724 | |||
| 47242e5431 | |||
| 66afe8eac7 | |||
| 1de14cdae1 | |||
| 4b7b941650 | |||
| 13e06eea4b | |||
| c4fb075a83 | |||
| 10474bb165 | |||
| 4d38338506 | |||
| 664bc8df76 | |||
| fef2d2fdb6 | |||
| 1f1c964e91 | |||
| c853cfb592 | |||
| be76c08756 |
18 changed files with 711 additions and 301 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -18,7 +18,8 @@ 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/
|
||||||
|
|
||||||
|
|
|
||||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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" ]
|
||||||
13
LICENSE.md
Normal file
13
LICENSE.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
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.
|
||||||
15
Makefile
Normal file
15
Makefile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
88
README.md
Normal file
88
README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# 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-ленты содержат видео. Добавить поддержку видео в постах
|
||||||
|
* [ ] Дополнительные флаги для команды чтобы указывать где лежит конфиг
|
||||||
|
|
||||||
|
# Отказ от ответственности
|
||||||
|
|
||||||
|
Программное обеспечение поставляется "как есть" и автор не несет ответственности на нарушение работы ЭВМ
|
||||||
|
|
@ -1,2 +1,10 @@
|
||||||
instance: https://pleroma.catgirls.asia
|
instance: https://pleroma.catgirls.asia
|
||||||
rss_url: https://4pda.to/feed
|
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
|
||||||
71
config/config.go
Normal file
71
config/config.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
14
config/go.mod
Normal file
14
config/go.mod
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
||||||
32
file_watcher/file_watcher.go
Normal file
32
file_watcher/file_watcher.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
3
file_watcher/go.mod
Normal file
3
file_watcher/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module file_watcher
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
287
main.go
287
main.go
|
|
@ -1,287 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
service/go.mod
Normal file
38
service/go.mod
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
113
service/kiki.go
Normal file
113
service/kiki.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
stacker/go.mod
Normal file
10
stacker/go.mod
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
47
stacker/stacker.go
Normal file
47
stacker/stacker.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,25 @@
|
||||||
module kiki
|
module kiki/tooter
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24.2
|
||||||
|
|
||||||
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.10.2 // indirect
|
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.1 // 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 // 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/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
|
||||||
205
tooter/tooter.go
Normal file
205
tooter/tooter.go
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue