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