From a1b10758cd75a4b97273e39cfa79ceedeaea1cd3 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 13 Mar 2022 02:19:57 +0100 Subject: [PATCH] =?UTF-8?q?Decouple=20layers=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cfg/environ.go | 52 ++++++++++++++++++++++ cfg/environ_test.go | 30 +++++++++++++ cmd/run.go | 104 ++++++++++---------------------------------- cmd/run_test.go | 23 ---------- go.mod | 6 +++ go.sum | 10 +++++ mastodon/post.go | 59 +++++++++++++++++++++++++ 7 files changed, 180 insertions(+), 104 deletions(-) create mode 100644 cfg/environ.go create mode 100644 cfg/environ_test.go create mode 100644 mastodon/post.go diff --git a/cfg/environ.go b/cfg/environ.go new file mode 100644 index 0000000..edf0eb9 --- /dev/null +++ b/cfg/environ.go @@ -0,0 +1,52 @@ +package cfg + +import ( + "os" + "sort" + "strconv" + "strings" +) + +const ( + MASTODON_ACCESS_TOKEN = "MASTODON_ACCESS_TOKEN" + MASTODON_SERVER_ADDRESS = "MASTODON_SERVER_ADDRESS" + MASTODON_TOOT_FOOTER = "MASTODON_TOOT_FOOTER" + MASTODON_TOOT_MAX_CHARACTERS = "MASTODON_TOOT_MAX_CHARACTERS" + MASTODON_TOOT_VISIBILITY = "MASTODON_TOOT_VISIBILITY" + TELEGRAM_BOT_TOKEN = "TELEGRAM_BOT_TOKEN" + TELEGRAM_CHAT_ID = "TELEGRAM_CHAT_ID" + TELEGRAM_DEBUG = "TELEGRAM_DEBUG" +) + +// Check the specified Mastodon visibility and return it if valid or return +// unlisted if it's not valid. +// The specified string will be cheched case unsensitive. +func parseMastodonVisibility(s string) string { + s = strings.ToLower(s) + // Keep sorted since we search inside. + visibilities := []string{"direct", "private", "public", "unlisted"} + r := sort.SearchStrings(visibilities, s) + if r < len(visibilities) && visibilities[r] == s { + return s + } + + return "unlisted" +} + +// Return configured Mastodon visibility for toot. +func GetMastodonVisibility() string { + return parseMastodonVisibility(os.Getenv(MASTODON_TOOT_VISIBILITY)) +} + +// Parse Mastodon max characters and return 500 as default in case of errors. +func parseMastodonMaxCharacters(s string) int { + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return int(n) + } + + return 500 +} + +func GetMastodonMaxCharacters() int { + return parseMastodonMaxCharacters(os.Getenv(MASTODON_TOOT_MAX_CHARACTERS)) +} diff --git a/cfg/environ_test.go b/cfg/environ_test.go new file mode 100644 index 0000000..52c7f20 --- /dev/null +++ b/cfg/environ_test.go @@ -0,0 +1,30 @@ +package cfg + +import ( + "testing" + + "github.com/alecthomas/assert" +) + +func TestParseMastodonVisibility(t *testing.T) { + assert.Equal(t, parseMastodonVisibility("public"), "public") + assert.Equal(t, parseMastodonVisibility("direct"), "direct") + assert.Equal(t, parseMastodonVisibility("unlisted"), "unlisted") + assert.Equal(t, parseMastodonVisibility("private"), "private") + + assert.Equal(t, parseMastodonVisibility("Public"), "public") + assert.Equal(t, parseMastodonVisibility("diRect"), "direct") + assert.Equal(t, parseMastodonVisibility("unlisTED"), "unlisted") + assert.Equal(t, parseMastodonVisibility("PRIVATE"), "private") + + assert.Equal(t, parseMastodonVisibility("True"), "unlisted") + assert.Equal(t, parseMastodonVisibility("eriol"), "unlisted") + assert.Equal(t, parseMastodonVisibility(""), "unlisted") + assert.Equal(t, parseMastodonVisibility(" "), "unlisted") +} + +func TestParseMastodonMaxCharacters(t *testing.T) { + assert.Equal(t, parseMastodonMaxCharacters("42"), 42) + assert.Equal(t, parseMastodonMaxCharacters("-42"), 500) + assert.Equal(t, parseMastodonMaxCharacters("hello"), 500) +} diff --git a/cmd/run.go b/cmd/run.go index 190bd49..26af649 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,20 +1,19 @@ package cmd import ( - "context" "fmt" "io" "log" "net/http" "os" - "sort" "strconv" - "strings" - "github.com/cking/go-mastodon" + mastodonapi "github.com/cking/go-mastodon" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/spf13/cobra" + "noa.mornie.org/eriol/telegram-group2mastodon/cfg" + "noa.mornie.org/eriol/telegram-group2mastodon/mastodon" "noa.mornie.org/eriol/telegram-group2mastodon/utils" ) @@ -23,7 +22,6 @@ const ( MASTODON_SERVER_ADDRESS = "MASTODON_SERVER_ADDRESS" MASTODON_TOOT_FOOTER = "MASTODON_TOOT_FOOTER" MASTODON_TOOT_MAX_CHARACTERS = "MASTODON_TOOT_MAX_CHARACTERS" - MASTODON_TOOT_VISIBILITY = "MASTODON_TOOT_VISIBILITY" TELEGRAM_BOT_TOKEN = "TELEGRAM_BOT_TOKEN" TELEGRAM_CHAT_ID = "TELEGRAM_CHAT_ID" TELEGRAM_DEBUG = "TELEGRAM_DEBUG" @@ -38,15 +36,14 @@ var runCmd = &cobra.Command{ Every messages posted in the Telegram groups the bot is in will be posted into the specified Mastodon account.`, Run: func(cmd *cobra.Command, args []string) { - mastodon_instance := os.Getenv(MASTODON_SERVER_ADDRESS) - c := mastodon.NewClient(&mastodon.Config{ - Server: mastodon_instance, + mastodonInstance := os.Getenv(MASTODON_SERVER_ADDRESS) + c := mastodonapi.NewClient(&mastodonapi.Config{ + Server: mastodonInstance, AccessToken: os.Getenv(MASTODON_ACCESS_TOKEN), }) - log.Println("Crating a new client for mastondon istance:", mastodon_instance) - max_characters := parseMastodonMaxCharacters(os.Getenv(MASTODON_TOOT_MAX_CHARACTERS)) - allowed_telegram_chat := parseTelegramChatID(os.Getenv(TELEGRAM_CHAT_ID)) - log.Println("Allowed telegram chat id:", allowed_telegram_chat) + log.Println("Crating a new client for mastondon istance:", mastodonInstance) + allowedTelegramChat := parseTelegramChatID(os.Getenv(TELEGRAM_CHAT_ID)) + log.Println("Allowed telegram chat id:", allowedTelegramChat) bot, err := tgbotapi.NewBotAPI(os.Getenv(TELEGRAM_BOT_TOKEN)) if err != nil { @@ -62,39 +59,26 @@ the specified Mastodon account.`, for update := range updates { chatID := update.Message.Chat.ID - if chatID != allowed_telegram_chat { + if chatID != allowedTelegramChat { log.Printf("Error: telegram chat %d is not the allowed one: %d\n", chatID, - allowed_telegram_chat, + allowedTelegramChat, ) continue } if update.Message != nil { messageID := update.Message.MessageID + maxChars := cfg.GetMastodonMaxCharacters() + tootVisibility := cfg.GetMastodonVisibility() + tootFooter := os.Getenv(MASTODON_TOOT_FOOTER) if update.Message.Text != "" { log.Printf("Text message received. Message id: %d\n", messageID) text := update.Message.Text - in_reply_to := "" - - toot_footer := os.Getenv(MASTODON_TOOT_FOOTER) - - messages := utils.SplitTextAtChunk(text, max_characters, toot_footer) - for _, message := range messages { - status, err := c.PostStatus(context.Background(), &mastodon.Toot{ - Status: message, - Visibility: parseMastodonVisibility(os.Getenv(MASTODON_TOOT_VISIBILITY)), - InReplyToID: mastodon.ID(in_reply_to), - }) - if err != nil { - log.Printf("Could not post status: %v", err) - continue - } - log.Printf("Posted status %s", status.URL) - in_reply_to = string(status.ID) - } + messages := utils.SplitTextAtChunk(text, maxChars, tootFooter) + mastodon.PostToots(c, messages, tootVisibility) } else if update.Message.Photo != nil { log.Printf("Photo received. Message id: %d\n", messageID) @@ -116,32 +100,14 @@ the specified Mastodon account.`, log.Printf("Could not download file: %v", err) continue } - attachment, err := c.UploadMediaFromReader( - context.Background(), file) - if err != nil { - log.Printf("Could not upload media: %v", err) - continue - } - file.Close() - log.Printf("Posted attachment %s", attachment.TextURL) - mediaIds := [...]mastodon.ID{attachment.ID} - caption := update.Message.Caption - if len(caption) > max_characters { - caption = caption[:max_characters] - } - status, err := c.PostStatus(context.Background(), &mastodon.Toot{ - // Write the caption in the toot because it almost probably - // doesn't describe the image. - Status: caption, - MediaIDs: mediaIds[:], - Visibility: parseMastodonVisibility(os.Getenv(MASTODON_TOOT_VISIBILITY)), - }) - if err != nil { - log.Printf("Could not post status: %v", err) - continue - } - log.Printf("Posted status %s", status.URL) + mastodon.PostPhoto( + c, + file, + update.Message.Caption, + maxChars, + tootVisibility, + ) } } } @@ -161,21 +127,6 @@ func parseBoolOrFalse(s string) bool { return r } -// Check the specified Mastodon visibility and return it if valid or return -// unlisted if it's not valid. -// The specified string will be cheched case unsensitive. -func parseMastodonVisibility(s string) string { - s = strings.ToLower(s) - // Keep sorted since we search inside. - visibilities := []string{"direct", "private", "public", "unlisted"} - r := sort.SearchStrings(visibilities, s) - if r < len(visibilities) && visibilities[r] == s { - return s - } - - return "unlisted" -} - func downloadFile(url string) (io.ReadCloser, error) { response, err := http.Get(url) if err != nil { @@ -189,15 +140,6 @@ func downloadFile(url string) (io.ReadCloser, error) { return response.Body, nil } -// Parse Mastodon max characters and return 500 as default in case of errors. -func parseMastodonMaxCharacters(s string) int { - if n, err := strconv.ParseUint(s, 10, 32); err == nil { - return int(n) - } - - return 500 -} - func parseTelegramChatID(s string) int64 { r, err := strconv.ParseInt(s, 10, 64) if err != nil { diff --git a/cmd/run_test.go b/cmd/run_test.go index ff96949..b7b9dc2 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -15,26 +15,3 @@ func TestParseBoolOrFalse(t *testing.T) { assert.Equal(t, parseBoolOrFalse("FALSE"), false) assert.Equal(t, parseBoolOrFalse("false"), false) } - -func TestParseMastodonVisibility(t *testing.T) { - assert.Equal(t, parseMastodonVisibility("public"), "public") - assert.Equal(t, parseMastodonVisibility("direct"), "direct") - assert.Equal(t, parseMastodonVisibility("unlisted"), "unlisted") - assert.Equal(t, parseMastodonVisibility("private"), "private") - - assert.Equal(t, parseMastodonVisibility("Public"), "public") - assert.Equal(t, parseMastodonVisibility("diRect"), "direct") - assert.Equal(t, parseMastodonVisibility("unlisTED"), "unlisted") - assert.Equal(t, parseMastodonVisibility("PRIVATE"), "private") - - assert.Equal(t, parseMastodonVisibility("True"), "unlisted") - assert.Equal(t, parseMastodonVisibility("eriol"), "unlisted") - assert.Equal(t, parseMastodonVisibility(""), "unlisted") - assert.Equal(t, parseMastodonVisibility(" "), "unlisted") -} - -func TestParseMastodonMaxCharacters(t *testing.T) { - assert.Equal(t, parseMastodonMaxCharacters("42"), 42) - assert.Equal(t, parseMastodonMaxCharacters("-42"), 500) - assert.Equal(t, parseMastodonMaxCharacters("hello"), 500) -} diff --git a/go.mod b/go.mod index ad61cd7..7365eab 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module noa.mornie.org/eriol/telegram-group2mastodon go 1.17 require ( + github.com/alecthomas/assert v1.0.0 github.com/cking/go-mastodon v0.0.6 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/spf13/cobra v1.3.0 @@ -10,11 +11,16 @@ require ( ) require ( + github.com/alecthomas/colour v0.1.0 // indirect + github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gorilla/websocket v1.4.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect + golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 3d097ff..8a566cc 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= +github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= +github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= +github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= +github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -263,6 +269,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= @@ -310,6 +317,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -536,6 +545,7 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/mastodon/post.go b/mastodon/post.go new file mode 100644 index 0000000..ca33034 --- /dev/null +++ b/mastodon/post.go @@ -0,0 +1,59 @@ +package mastodon + +import ( + "context" + "io" + "log" + + mastodonapi "github.com/cking/go-mastodon" +) + +// Post one or more toots. +func PostToots(client *mastodonapi.Client, messages []string, visibility string) { + in_reply_to := "" + for _, message := range messages { + status, err := client.PostStatus(context.Background(), &mastodonapi.Toot{ + Status: message, + Visibility: visibility, + InReplyToID: mastodonapi.ID(in_reply_to), + }) + if err != nil { + log.Printf("Could not post status: %v", err) + continue + } + log.Printf("Posted status %s", status.URL) + in_reply_to = string(status.ID) + } +} + +// Post a photo on mastodon with caption. +func PostPhoto( + client *mastodonapi.Client, + file io.ReadCloser, + caption string, + maxCharacters int, + visibility string) { + attachment, err := client.UploadMediaFromReader( + context.Background(), file) + if err != nil { + log.Printf("Could not upload media: %v", err) + } + file.Close() + log.Printf("Posted attachment %s", attachment.TextURL) + + mediaIds := [...]mastodonapi.ID{attachment.ID} + if len(caption) > maxCharacters { + caption = caption[:maxCharacters] + } + status, err := client.PostStatus(context.Background(), &mastodonapi.Toot{ + // Write the caption in the toot because it almost probably + // doesn't describe the image. + Status: caption, + MediaIDs: mediaIds[:], + Visibility: visibility, + }) + if err != nil { + log.Printf("Could not post status: %v", err) + } + log.Printf("Posted status %s", status.URL) +}