From 58a2922e5aa0c66964e90b65a3fdc097dceca3de Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:17:28 +0000 Subject: [PATCH 01/25] Create `cobra/` --- cobra/cmd/root.go | 41 +++++++++++++++++++++++++++++++++++++++++ cobra/main.go | 11 +++++++++++ go.mod | 3 +++ go.sum | 8 ++++++++ 4 files changed, 63 insertions(+) create mode 100644 cobra/cmd/root.go create mode 100644 cobra/main.go diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go new file mode 100644 index 0000000..e16c299 --- /dev/null +++ b/cobra/cmd/root.go @@ -0,0 +1,41 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cross-blogger", + Short: "A utility to cross-publish content between different platforms", + Long: `cross-blogger is a utility to cross-publish content between different platforms.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cobra/main.go b/cobra/main.go new file mode 100644 index 0000000..5839a4a --- /dev/null +++ b/cobra/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package main + +import "github.com/slashtechno/cross-blogger/cobra/cmd" + +func main() { + cmd.Execute() +} diff --git a/go.mod b/go.mod index 03b6318..af2387f 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,9 @@ require ( require ( github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 90a1478..e908a72 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4Pnl github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -15,6 +16,8 @@ github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -23,6 +26,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -32,6 +36,10 @@ github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= From 9c4f397bd47c736f87d54dfcff2a8238cbb9b01c Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:58:57 +0000 Subject: [PATCH 02/25] Add publish command to cross-blogger Doesn't do anything yet --- cobra/cmd/publish.go | 45 ++++++++++++++++++++++++++++++++++++++++++++ cobra/cmd/root.go | 6 ++---- 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 cobra/cmd/publish.go diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go new file mode 100644 index 0000000..786d524 --- /dev/null +++ b/cobra/cmd/publish.go @@ -0,0 +1,45 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// publishCmd represents the publish command +var publishCmd = &cobra.Command{ + Use: "publish", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Args: cobra.OnlyValidArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("publish called") + }, +} + +func init() { + rootCmd.AddCommand(publishCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // publishCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + + // Google OAuth flags + publishCmd.Flags().String("client-id", "", "Google OAuth client ID") + publishCmd.Flags().String("client-secret", "", "Google OAuth client secret") + publishCmd.Flags().String("refresh-token", "", "Google OAuth refresh token") + +} diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go index e16c299..2915739 100644 --- a/cobra/cmd/root.go +++ b/cobra/cmd/root.go @@ -1,6 +1,3 @@ -/* -Copyright © 2024 NAME HERE -*/ package cmd import ( @@ -14,6 +11,7 @@ var rootCmd = &cobra.Command{ Use: "cross-blogger", Short: "A utility to cross-publish content between different platforms", Long: `cross-blogger is a utility to cross-publish content between different platforms.`, + // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -37,5 +35,5 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } From 64355d47be599b98bc00c29b5f01595f3863ff7a Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 00:55:57 +0000 Subject: [PATCH 03/25] feat: Allow for multiple destinations and attributes Might just be better to use a file via viper --- cobra/cmd/publish.go | 70 ++++++++++++++++++++++++++++---------------- cobra/main.go | 7 +++-- go.mod | 13 +++++++- go.sum | 28 +++++++++++++++++- 4 files changed, 89 insertions(+), 29 deletions(-) diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 786d524..ab1fdaa 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -1,45 +1,65 @@ -/* -Copyright © 2024 NAME HERE -*/ package cmd import ( - "fmt" + "strings" + "github.com/charmbracelet/log" "github.com/spf13/cobra" ) -// publishCmd represents the publish command var publishCmd = &cobra.Command{ Use: "publish", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Args: cobra.OnlyValidArgs, + Short: "Publish to a destination", + Long: `Publish to a destination. Available destinations: blogger, markdown, html. Make sure to specify with ,=,=,...`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("publish called") + destinations, _ := cmd.Flags().GetStringSlice("destination") + // Make a map of platforms and their attributes + platformMap := make(map[string]map[string]string) + + // Iterate over the destinations + for _, destination := range destinations { + // Split the destination into platform and attributes + // This splits the destination into two parts: the platform and the attributes + // Anything after the first semicolon is considered attributes + destinationParts := strings.SplitN(destination, ";", 2) + platform := destinationParts[0] + // Create a new map for the platform if it doesn't exist + if _, ok := platformMap[platform]; !ok { + platformMap[platform] = make(map[string]string) + } + // Check if there are any attributes + if len(destinationParts) > 1 { + // Split the attributes part on semicolon + attributes := strings.Split(destinationParts[1], ";") + // Populate the map with the attributes + for _, attribute := range attributes { + keyValue := strings.Split(attribute, "=") + if len(keyValue) == 2 { + platformMap[platform][keyValue[0]] = keyValue[1] + } + } + } + } + + // Debug log the destination map + log.Debugf("Platform map: %v", platformMap) + // Iterate over the platforms + for platform, attributes := range platformMap { + // Debug log the platform and attributes + log.Debugf("Platform: %s, Attributes: %v", platform, attributes) + } + }, } func init() { + // example command: go run . publish --destination "blogger;blogAddress=example.com;postAddress=example-post" --destination "markdown;filepath=example.md" --title "Example Title" --dry-run rootCmd.AddCommand(publishCmd) - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // publishCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - - // Google OAuth flags + publishCmd.Flags().StringSliceP("destination", "d", nil, "Destination(s) to publish to\nAvailable destinations: blogger, markdown, html\nMake sure to specify with ,=,=,...") + publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") + publishCmd.Flags().BoolP("dry-run", "r", false, "Don't actually publish") publishCmd.Flags().String("client-id", "", "Google OAuth client ID") publishCmd.Flags().String("client-secret", "", "Google OAuth client secret") publishCmd.Flags().String("refresh-token", "", "Google OAuth refresh token") - } diff --git a/cobra/main.go b/cobra/main.go index 5839a4a..6215dd9 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -1,11 +1,14 @@ /* Copyright © 2024 NAME HERE - */ package main -import "github.com/slashtechno/cross-blogger/cobra/cmd" +import ( + "github.com/charmbracelet/log" + "github.com/slashtechno/cross-blogger/cobra/cmd" +) func main() { + log.SetLevel(log.DebugLevel) cmd.Execute() } diff --git a/go.mod b/go.mod index af2387f..e1f5a13 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,30 @@ go 1.19 require ( github.com/JohannesKaufmann/html-to-markdown v1.4.0 github.com/alexflint/go-arg v1.4.3 + github.com/charmbracelet/log v0.4.0 github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c github.com/imdario/mergo v0.3.16 github.com/sirupsen/logrus v1.9.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 + github.com/spf13/cobra v1.8.1 github.com/tidwall/gjson v1.16.0 ) require ( github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index e908a72..1f9cd4b 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,18 @@ github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4Pnl github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -23,9 +31,24 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= @@ -44,8 +67,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -57,6 +80,8 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -79,6 +104,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From d08777f084dd62330e4a4648d2a06fb862235452 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:16:03 +0000 Subject: [PATCH 04/25] feat: Add `--config` --- cobra/cmd/publish.go | 2 +- cobra/cmd/root.go | 4 ++-- cobra/main.go | 25 +++++++++++++++++++ go.mod | 21 ++++++++++++++-- go.sum | 57 +++++++++++++++++++++++++++++++++++++++----- 5 files changed, 98 insertions(+), 11 deletions(-) diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index ab1fdaa..b43d925 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -54,7 +54,7 @@ var publishCmd = &cobra.Command{ func init() { // example command: go run . publish --destination "blogger;blogAddress=example.com;postAddress=example-post" --destination "markdown;filepath=example.md" --title "Example Title" --dry-run - rootCmd.AddCommand(publishCmd) + RootCmd.AddCommand(publishCmd) publishCmd.Flags().StringSliceP("destination", "d", nil, "Destination(s) to publish to\nAvailable destinations: blogger, markdown, html\nMake sure to specify with ,=,=,...") publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go index 2915739..2a7d10f 100644 --- a/cobra/cmd/root.go +++ b/cobra/cmd/root.go @@ -7,7 +7,7 @@ import ( ) // rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +var RootCmd = &cobra.Command{ Use: "cross-blogger", Short: "A utility to cross-publish content between different platforms", Long: `cross-blogger is a utility to cross-publish content between different platforms.`, @@ -20,7 +20,7 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - err := rootCmd.Execute() + err := RootCmd.Execute() if err != nil { os.Exit(1) } diff --git a/cobra/main.go b/cobra/main.go index 6215dd9..f220b4e 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -6,8 +6,33 @@ package main import ( "github.com/charmbracelet/log" "github.com/slashtechno/cross-blogger/cobra/cmd" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) +var cfgFile string + +func init() { + cobra.OnInitialize(initConfig) + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") +} + +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Use config.yaml in the current working directory. + viper.SetConfigFile("./config.yaml") + } + + if err := viper.ReadInConfig(); err == nil { + log.Debug("Using config file:", viper.ConfigFileUsed()) + } else { + log.Fatal("Failed to read config file:", err) + } +} + func main() { log.SetLevel(log.DebugLevel) cmd.Execute() diff --git a/go.mod b/go.mod index e1f5a13..3145d68 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/sirupsen/logrus v1.9.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 github.com/tidwall/gjson v1.16.0 ) @@ -19,18 +20,34 @@ require ( github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( diff --git a/go.sum b/go.sum index 1f9cd4b..980af6c 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,18 @@ github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8 github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -29,27 +35,40 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -59,16 +78,33 @@ github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -78,6 +114,10 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= @@ -91,8 +131,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -106,8 +146,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -118,13 +158,18 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= From ba5478f6485238073153a0f2573679b974dd1ffe Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:21:10 +0000 Subject: [PATCH 05/25] Add debugging configuration --- .vscode/launch.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..341a0ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "--help for publish", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": ["publish", "--help"] + } + ] +} \ No newline at end of file From 768df2b37e0615fc2665010e6c3f07a76f64e1d2 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:43:45 +0000 Subject: [PATCH 06/25] Work on creating a config file with default values Currently it seems it isn't asserting the error correctly --- .vscode/launch.json | 7 +++++++ cobra/cmd/publish.go | 6 +++++- cobra/config.yaml.example | 0 cobra/main.go | 14 ++++++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 cobra/config.yaml.example diff --git a/.vscode/launch.json b/.vscode/launch.json index 341a0ec..1e56cf5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,13 @@ "mode": "auto", "program": "${workspaceFolder}", "args": ["publish", "--help"] + }, + { + "name": "Run publish without args", + "type": "go", + "request": "launch", + "program": "${workspaceFolder}", + "args": ["publish"] } ] } \ No newline at end of file diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index b43d925..5210c3a 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -10,7 +10,11 @@ import ( var publishCmd = &cobra.Command{ Use: "publish", Short: "Publish to a destination", - Long: `Publish to a destination. Available destinations: blogger, markdown, html. Make sure to specify with ,=,=,...`, + Long: `Publish to a destination from a source. + Specify the source as a positional argument and the destination as a flag in the format ,=,=. + Destination attributes + TODO + `, Run: func(cmd *cobra.Command, args []string) { destinations, _ := cmd.Flags().GetStringSlice("destination") // Make a map of platforms and their attributes diff --git a/cobra/config.yaml.example b/cobra/config.yaml.example new file mode 100644 index 0000000..e69de29 diff --git a/cobra/main.go b/cobra/main.go index f220b4e..814c282 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -14,7 +14,7 @@ var cfgFile string func init() { cobra.OnInitialize(initConfig) - cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.yaml", "config file (default is $HOME/.cobra.yaml)") } func initConfig() { @@ -29,7 +29,17 @@ func initConfig() { if err := viper.ReadInConfig(); err == nil { log.Debug("Using config file:", viper.ConfigFileUsed()) } else { - log.Fatal("Failed to read config file:", err) + // If the config file is not found, create a file, write the default values and exit + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Debug("Config file not found, creating a new one") + viper.Set("destination", []string{"blogger;blogAddress=example.com;postAddress=example-post", "markdown;filepath=example.md"}) + viper.Set("title", "Example Title") + viper.Set("dry-run", false) + viper.WriteConfigAs(cfgFile) + log.Debug("Config file created at:", cfgFile) + } else { + log.Fatal("Failed to read config file:", err) + } } } From 3fe45b2489a3ad477ebcc0c46847eb025d69c8da Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:21:01 +0000 Subject: [PATCH 07/25] feat: Create default toml config --- .gitignore | 3 ++- .vscode/launch.json | 4 ++-- cobra/config.toml | 10 ++++++++++ cobra/config.yaml.example | 0 cobra/main.go | 29 ++++++++++++++++++++++------- 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 cobra/config.toml delete mode 100644 cobra/config.yaml.example diff --git a/.gitignore b/.gitignore index 1826666..d99d469 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ test* output.* -.env* \ No newline at end of file +.env* +config.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1e56cf5..6abb6cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,14 +6,14 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}", + "program": "${workspaceFolder}/cobra", "args": ["publish", "--help"] }, { "name": "Run publish without args", "type": "go", "request": "launch", - "program": "${workspaceFolder}", + "program": "${workspaceFolder}/cobra", "args": ["publish"] } ] diff --git a/cobra/config.toml b/cobra/config.toml new file mode 100644 index 0000000..30cf11d --- /dev/null +++ b/cobra/config.toml @@ -0,0 +1,10 @@ +[[destinations]] +blogId = '1234567890' +blogUrl = 'https://example.com' +name = 'blogger' +type = 'blogger' + +[[destinations]] +contentDir = 'content' +name = 'markdown1' +type = 'markdown' diff --git a/cobra/config.yaml.example b/cobra/config.yaml.example deleted file mode 100644 index e69de29..0000000 diff --git a/cobra/main.go b/cobra/main.go index 814c282..76ab23d 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -4,6 +4,8 @@ Copyright © 2024 NAME HERE package main import ( + "io/fs" + "github.com/charmbracelet/log" "github.com/slashtechno/cross-blogger/cobra/cmd" "github.com/spf13/cobra" @@ -14,7 +16,7 @@ var cfgFile string func init() { cobra.OnInitialize(initConfig) - cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.yaml", "config file (default is $HOME/.cobra.yaml)") + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.toml", "config file path") } func initConfig() { @@ -23,20 +25,33 @@ func initConfig() { viper.SetConfigFile(cfgFile) } else { // Use config.yaml in the current working directory. - viper.SetConfigFile("./config.yaml") + viper.SetConfigFile("./config.toml") } if err := viper.ReadInConfig(); err == nil { log.Debug("Using config file:", viper.ConfigFileUsed()) } else { // If the config file is not found, create a file, write the default values and exit - if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError + if _, ok := err.(*fs.PathError); ok { log.Debug("Config file not found, creating a new one") - viper.Set("destination", []string{"blogger;blogAddress=example.com;postAddress=example-post", "markdown;filepath=example.md"}) - viper.Set("title", "Example Title") - viper.Set("dry-run", false) - viper.WriteConfigAs(cfgFile) + viper.SetDefault("destinations", []map[string]interface{}{ + { + "name": "blogger", + "type": "blogger", + "blogUrl": "https://example.com", + "blogId": "1234567890", + }, + { + "name": "markdown1", + "type": "markdown", + "contentDir": "content", + }, + }) log.Debug("Config file created at:", cfgFile) + if err := viper.WriteConfigAs(cfgFile); err != nil { + log.Fatal("Failed to write config file:", err) + } } else { log.Fatal("Failed to read config file:", err) } From 907048d16c907a4925aca71992858d660b47e9d4 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:05:18 +0000 Subject: [PATCH 08/25] Work on adding structs for destinations --- .gitignore | 3 +- cobra/cmd/publish.go | 93 ++++++++++++++++++++++++-------------------- cobra/main.go | 5 ++- 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index d99d469..b602352 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ test* output.* .env* -config.json \ No newline at end of file +config.json +_debug* \ No newline at end of file diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 5210c3a..2c05ded 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -1,66 +1,73 @@ package cmd import ( - "strings" - "github.com/charmbracelet/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) +type Destination struct { + Name string + Type string +} + +type Blogger struct { + Destination + BlogUrl string + BlogId string +} +type Markdown struct { + Destination + ContentDir string +} + var publishCmd = &cobra.Command{ Use: "publish", Short: "Publish to a destination", Long: `Publish to a destination from a source. - Specify the source as a positional argument and the destination as a flag in the format ,=,=. - Destination attributes - TODO - `, + Specify the source with the first positional argument. All arguments after the first are treated as destinations. + Destinations should be the name of the destinations specified in the config file`, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - destinations, _ := cmd.Flags().GetStringSlice("destination") - // Make a map of platforms and their attributes - platformMap := make(map[string]map[string]string) - - // Iterate over the destinations - for _, destination := range destinations { - // Split the destination into platform and attributes - // This splits the destination into two parts: the platform and the attributes - // Anything after the first semicolon is considered attributes - destinationParts := strings.SplitN(destination, ";", 2) - platform := destinationParts[0] - // Create a new map for the platform if it doesn't exist - if _, ok := platformMap[platform]; !ok { - platformMap[platform] = make(map[string]string) - } - // Check if there are any attributes - if len(destinationParts) > 1 { - // Split the attributes part on semicolon - attributes := strings.Split(destinationParts[1], ";") - // Populate the map with the attributes - for _, attribute := range attributes { - keyValue := strings.Split(attribute, "=") - if len(keyValue) == 2 { - platformMap[platform][keyValue[0]] = keyValue[1] - } - } - } + // Get the list of objects `destinations` from Viper and make a list of Destination structs + log.Debug(viper.AllSettings()) + destinations, ok := viper.Get("destinations").([]map[string]interface{}) + if !ok { + log.Fatal("Failed to get destinations from config file") } - - // Debug log the destination map - log.Debugf("Platform map: %v", platformMap) - // Iterate over the platforms - for platform, attributes := range platformMap { - // Debug log the platform and attributes - log.Debugf("Platform: %s, Attributes: %v", platform, attributes) + // Make a list of the respective Destination structs + var destinationSlice []interface{} + // _ ignores the index. `dest` is the map + for _, dest := range destinations { + switch dest["type"] { + case "blogger": + destinationSlice = append(destinationSlice, Blogger{ + Destination: Destination{ + Name: dest["name"].(string), + Type: dest["type"].(string), + }, + BlogUrl: dest["blogUrl"].(string), + BlogId: dest["blogId"].(string), + }) + case "markdown": + destinationSlice = append(destinationSlice, Markdown{ + Destination: Destination{ + Name: dest["name"].(string), + Type: dest["type"].(string), + }, + ContentDir: dest["contentDir"].(string), + }) + default: + log.Fatal("Unknown destination type:", dest["type"]) + } } - }, } func init() { - // example command: go run . publish --destination "blogger;blogAddress=example.com;postAddress=example-post" --destination "markdown;filepath=example.md" --title "Example Title" --dry-run RootCmd.AddCommand(publishCmd) - publishCmd.Flags().StringSliceP("destination", "d", nil, "Destination(s) to publish to\nAvailable destinations: blogger, markdown, html\nMake sure to specify with ,=,=,...") publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") publishCmd.Flags().BoolP("dry-run", "r", false, "Don't actually publish") publishCmd.Flags().String("client-id", "", "Google OAuth client ID") diff --git a/cobra/main.go b/cobra/main.go index 76ab23d..bcb739e 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -16,10 +16,11 @@ var cfgFile string func init() { cobra.OnInitialize(initConfig) - cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.toml", "config file path") + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.toml", "config file path") } func initConfig() { + log.Debug(cfgFile) if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) @@ -29,7 +30,7 @@ func initConfig() { } if err := viper.ReadInConfig(); err == nil { - log.Debug("Using config file:", viper.ConfigFileUsed()) + log.Debug("", "config file:", viper.ConfigFileUsed()) } else { // If the config file is not found, create a file, write the default values and exit // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError From 58da0d7f72e5f5c965d83a34f13b573966619609 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:57:46 +0000 Subject: [PATCH 09/25] fix: make config keys lowercase --- .vscode/launch.json | 4 +-- cobra/cmd/publish.go | 66 +++++++++++++++++++++++++++----------------- cobra/config.toml | 6 ++-- cobra/main.go | 18 ++++++------ 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6abb6cc..7be8a32 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,11 @@ "args": ["publish", "--help"] }, { - "name": "Run publish without args", + "name": "Run publish (dry run)", "type": "go", "request": "launch", "program": "${workspaceFolder}/cobra", - "args": ["publish"] + "args": ["publish", "markdown"] } ] } \ No newline at end of file diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 2c05ded..7c28c93 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -1,8 +1,9 @@ package cmd import ( - "github.com/charmbracelet/log" + "fmt" + "github.com/charmbracelet/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -31,40 +32,53 @@ var publishCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Get the list of objects `destinations` from Viper and make a list of Destination structs - log.Debug(viper.AllSettings()) - destinations, ok := viper.Get("destinations").([]map[string]interface{}) - if !ok { - log.Fatal("Failed to get destinations from config file") + destinations := viper.Get("destinations") + if destinations == nil { + log.Fatal("Failed to get destinations from config") } + // Make a list of the respective Destination structs var destinationSlice []interface{} // _ ignores the index. `dest` is the map - for _, dest := range destinations { - switch dest["type"] { - case "blogger": - destinationSlice = append(destinationSlice, Blogger{ - Destination: Destination{ - Name: dest["name"].(string), - Type: dest["type"].(string), - }, - BlogUrl: dest["blogUrl"].(string), - BlogId: dest["blogId"].(string), - }) - case "markdown": - destinationSlice = append(destinationSlice, Markdown{ - Destination: Destination{ - Name: dest["name"].(string), - Type: dest["type"].(string), - }, - ContentDir: dest["contentDir"].(string), - }) - default: - log.Fatal("Unknown destination type:", dest["type"]) + for _, dest := range destinations.([]interface{}) { + destMap, ok := dest.(map[string]interface{}) + if !ok { + log.Fatal("Failed to convert destination to map") } + destination, err := createDestination(destMap) + if err != nil { + log.Fatal(err) + } + destinationSlice = append(destinationSlice, destination) } + // Use destinationSlice here }, } +func createDestination(destMap map[string]interface{}) (interface{}, error) { + switch destMap["type"] { + case "blogger": + return Blogger{ + Destination: Destination{ + Name: destMap["name"].(string), + Type: destMap["type"].(string), + }, + BlogUrl: destMap["blog_url"].(string), + BlogId: destMap["blog_id"].(string), + }, nil + case "markdown": + return Markdown{ + Destination: Destination{ + Name: destMap["name"].(string), + Type: destMap["type"].(string), + }, + ContentDir: destMap["content_dir"].(string), + }, nil + default: + return nil, fmt.Errorf("Unsupported destination type") + } +} + func init() { RootCmd.AddCommand(publishCmd) diff --git a/cobra/config.toml b/cobra/config.toml index 30cf11d..da06cd6 100644 --- a/cobra/config.toml +++ b/cobra/config.toml @@ -1,10 +1,10 @@ [[destinations]] -blogId = '1234567890' -blogUrl = 'https://example.com' +blog_id = '1234567890' +blog_url = 'https://example.com' name = 'blogger' type = 'blogger' [[destinations]] -contentDir = 'content' +content_dir = 'content' name = 'markdown1' type = 'markdown' diff --git a/cobra/main.go b/cobra/main.go index bcb739e..64fd8d1 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -20,7 +20,7 @@ func init() { } func initConfig() { - log.Debug(cfgFile) + // log.Debug(cfgFile) if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) @@ -38,18 +38,18 @@ func initConfig() { log.Debug("Config file not found, creating a new one") viper.SetDefault("destinations", []map[string]interface{}{ { - "name": "blogger", - "type": "blogger", - "blogUrl": "https://example.com", - "blogId": "1234567890", + "name": "blogger", + "type": "blogger", + "blog_url": "https://example.com", + "blog_id": "1234567890", }, { - "name": "markdown1", - "type": "markdown", - "contentDir": "content", + "name": "markdown1", + "type": "markdown", + "content_dir": "content", }, }) - log.Debug("Config file created at:", cfgFile) + log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) if err := viper.WriteConfigAs(cfgFile); err != nil { log.Fatal("Failed to write config file:", err) } From 7066d7f30cf7921c461baf0873af7e7b3b601abc Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:02:29 +0000 Subject: [PATCH 10/25] refactor!: Move destinations to `cmd/destinations.go` --- cobra/cmd/destinations.go | 42 ++++++++++++++++++++++++++++++++++++ cobra/cmd/publish.go | 45 ++------------------------------------- 2 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 cobra/cmd/destinations.go diff --git a/cobra/cmd/destinations.go b/cobra/cmd/destinations.go new file mode 100644 index 0000000..40c5f1d --- /dev/null +++ b/cobra/cmd/destinations.go @@ -0,0 +1,42 @@ +package cmd + +import "fmt" + +type Destination struct { + Name string + Type string +} + +type Blogger struct { + Destination + BlogUrl string + BlogId string +} +type Markdown struct { + Destination + ContentDir string +} + +func CreateDestination(destMap map[string]interface{}) (interface{}, error) { + switch destMap["type"] { + case "blogger": + return Blogger{ + Destination: Destination{ + Name: destMap["name"].(string), + Type: destMap["type"].(string), + }, + BlogUrl: destMap["blog_url"].(string), + BlogId: destMap["blog_id"].(string), + }, nil + case "markdown": + return Markdown{ + Destination: Destination{ + Name: destMap["name"].(string), + Type: destMap["type"].(string), + }, + ContentDir: destMap["content_dir"].(string), + }, nil + default: + return nil, fmt.Errorf("unknown destination type: %s", destMap["type"]) + } +} diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 7c28c93..83fe5a1 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -1,28 +1,11 @@ package cmd import ( - "fmt" - "github.com/charmbracelet/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) -type Destination struct { - Name string - Type string -} - -type Blogger struct { - Destination - BlogUrl string - BlogId string -} -type Markdown struct { - Destination - ContentDir string -} - var publishCmd = &cobra.Command{ Use: "publish", Short: "Publish to a destination", @@ -45,40 +28,16 @@ var publishCmd = &cobra.Command{ if !ok { log.Fatal("Failed to convert destination to map") } - destination, err := createDestination(destMap) + destination, err := CreateDestination(destMap) if err != nil { log.Fatal(err) } destinationSlice = append(destinationSlice, destination) } - // Use destinationSlice here + log.Info("Destination slice", "destinations", destinationSlice) }, } -func createDestination(destMap map[string]interface{}) (interface{}, error) { - switch destMap["type"] { - case "blogger": - return Blogger{ - Destination: Destination{ - Name: destMap["name"].(string), - Type: destMap["type"].(string), - }, - BlogUrl: destMap["blog_url"].(string), - BlogId: destMap["blog_id"].(string), - }, nil - case "markdown": - return Markdown{ - Destination: Destination{ - Name: destMap["name"].(string), - Type: destMap["type"].(string), - }, - ContentDir: destMap["content_dir"].(string), - }, nil - default: - return nil, fmt.Errorf("Unsupported destination type") - } -} - func init() { RootCmd.AddCommand(publishCmd) From e20089e14a16e95cd5194e5ddbcbf70ac66b51a7 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:09:11 +0000 Subject: [PATCH 11/25] feat: change destinations to platforms and prepare to use OAuth2.0 --- cobra/cmd/{destinations.go => platforms.go} | 33 ++++---- cobra/cmd/publish.go | 5 ++ cobra/pkg/oauth/google.go | 86 +++++++++++++++++++++ go.mod | 6 ++ go.sum | 21 ++++- 5 files changed, 136 insertions(+), 15 deletions(-) rename cobra/cmd/{destinations.go => platforms.go} (61%) create mode 100644 cobra/pkg/oauth/google.go diff --git a/cobra/cmd/destinations.go b/cobra/cmd/platforms.go similarity index 61% rename from cobra/cmd/destinations.go rename to cobra/cmd/platforms.go index 40c5f1d..ea857f8 100644 --- a/cobra/cmd/destinations.go +++ b/cobra/cmd/platforms.go @@ -1,19 +1,30 @@ package cmd -import "fmt" +import ( + "fmt" +) -type Destination struct { - Name string - Type string +type Platform interface { + Push() + Pull() } +// type PlatformParent struct { +// Name string +// } + +// func (p PlatformParent) Push() { +// log.Error("child class must implement this method") +// } + type Blogger struct { - Destination + Name string BlogUrl string BlogId string } + type Markdown struct { - Destination + Name string ContentDir string } @@ -21,19 +32,13 @@ func CreateDestination(destMap map[string]interface{}) (interface{}, error) { switch destMap["type"] { case "blogger": return Blogger{ - Destination: Destination{ - Name: destMap["name"].(string), - Type: destMap["type"].(string), - }, + Name: destMap["name"].(string), BlogUrl: destMap["blog_url"].(string), BlogId: destMap["blog_id"].(string), }, nil case "markdown": return Markdown{ - Destination: Destination{ - Name: destMap["name"].(string), - Type: destMap["type"].(string), - }, + Name: destMap["name"].(string), ContentDir: destMap["content_dir"].(string), }, nil default: diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 83fe5a1..76c1535 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -35,6 +35,7 @@ var publishCmd = &cobra.Command{ destinationSlice = append(destinationSlice, destination) } log.Info("Destination slice", "destinations", destinationSlice) + }, } @@ -46,4 +47,8 @@ func init() { publishCmd.Flags().String("client-id", "", "Google OAuth client ID") publishCmd.Flags().String("client-secret", "", "Google OAuth client secret") publishCmd.Flags().String("refresh-token", "", "Google OAuth refresh token") + // Allow the OAuth stuff to be set via viper + viper.BindPFlag("client-id", publishCmd.Flags().Lookup("client-id")) + viper.BindPFlag("client-secret", publishCmd.Flags().Lookup("client-secret")) + viper.BindPFlag("refresh-token", publishCmd.Flags().Lookup("refresh-token")) } diff --git a/cobra/pkg/oauth/google.go b/cobra/pkg/oauth/google.go new file mode 100644 index 0000000..c34cebb --- /dev/null +++ b/cobra/pkg/oauth/google.go @@ -0,0 +1,86 @@ +package googleauth + +import ( + "context" + "fmt" + "net/http" + + "github.com/charmbracelet/log" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +// Config holds the configuration for the Google OAuth2 client. +type Config struct { + ClientID string + ClientSecret string + Port string +} + +// GetToken starts the OAuth2 flow, opens a temporary web server to handle the callback, +// and returns the refresh token as a string. +func GetToken(cfg Config) (string, error) { + // Configure the Google OAuth2 client. + googleOauthConfig := &oauth2.Config{ + RedirectURL: "http://localhost:8080/callback", + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + Scopes: []string{"https://www.googleapis.com/auth/blogger"}, + Endpoint: google.Endpoint, + } + + // This channel will receive the refresh token when the auth flow is done. + tokenCh := make(chan string) + + // This is the handler for the login route. + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + url := googleOauthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) + }) + + // This is the handler for the callback route. + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + code := r.FormValue("code") + token, err := googleOauthConfig.Exchange(context.Background(), code) + if err != nil { + log.Fatal(err) + } + + // Send the refresh token to the channel. + tokenCh <- token.RefreshToken + }) + + // Start the server in a separate goroutine. + go http.ListenAndServe(":"+cfg.Port, nil) + fmt.Println("Go to http://localhost:" + cfg.Port + "/login to get the refresh token.") + + // Wait for the refresh token and return it. + return <-tokenCh, nil +} + +// Return an access token from a refresh token +func GetAccessToken(cfg Config, refreshToken string) (string, error) { + // Configure the Google OAuth2 client. + googleOauthConfig := &oauth2.Config{ + RedirectURL: "http://localhost:" + cfg.Port + "/callback", + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + Scopes: []string{"https://www.googleapis.com/auth/blogger"}, + Endpoint: google.Endpoint, + } + + // Create a new token from the refresh token + token := &oauth2.Token{ + RefreshToken: refreshToken, + } + + // Get a new access token + tokenSource := googleOauthConfig.TokenSource(context.Background(), token) + newToken, err := tokenSource.Token() + if err != nil { + log.Fatal(err) + } + + return newToken.AccessToken, nil +} diff --git a/go.mod b/go.mod index 3145d68..6fae23a 100644 --- a/go.mod +++ b/go.mod @@ -13,15 +13,19 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/tidwall/gjson v1.16.0 + golang.org/x/oauth2 v0.18.0 ) require ( + cloud.google.com/go/compute v1.24.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -46,6 +50,8 @@ require ( golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 980af6c..7d64c1f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/JohannesKaufmann/html-to-markdown v1.4.0 h1:uaIPDub6VrBsQP0r5xKjpPo9lxMcuQF1L1pT6BiBdmw= github.com/JohannesKaufmann/html-to-markdown v1.4.0/go.mod h1:3p+lDUqSw+cxolZl7OINYzJ70JHXogXjyCl9UnMQ5gU= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= @@ -23,9 +27,14 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8= github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -133,6 +142,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -156,6 +167,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -165,6 +177,13 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 72db56ff57f230a1b27b8236a3a48b1575025dca Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:39:47 +0000 Subject: [PATCH 12/25] continue to work on OAuth --- cobra/cmd/platforms.go | 23 +++++++++++++++++++++++ cobra/pkg/oauth/google.go | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index ea857f8..87d65df 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -2,6 +2,10 @@ package cmd import ( "fmt" + + "github.com/charmbracelet/log" + "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" + "github.com/spf13/viper" ) type Platform interface { @@ -23,6 +27,25 @@ type Blogger struct { BlogId string } +func (b Blogger) authorize() (string, error) { + oauthConfig := oauth.Config{ + ClientID: viper.GetString("client-id"), + ClientSecret: viper.GetString("client-secret"), + Port: "8080", + } + refreshToken, err := oauth.GetToken(oauthConfig) + if err != nil { + return "", err + } + + accessToken, err := oauth.GetAccessToken(oauthConfig, refreshToken) + if err != nil { + return "", err + } + log.Info("", "access token", accessToken) + return accessToken, nil +} + type Markdown struct { Name string ContentDir string diff --git a/cobra/pkg/oauth/google.go b/cobra/pkg/oauth/google.go index c34cebb..f26e6c3 100644 --- a/cobra/pkg/oauth/google.go +++ b/cobra/pkg/oauth/google.go @@ -1,4 +1,4 @@ -package googleauth +package oauth import ( "context" From 7e270a084c955085688c54aa24fb6761bcfe626b Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:12:36 +0000 Subject: [PATCH 13/25] feat: Got OAuth working! --- .vscode/launch.json | 6 ++++-- cobra/cmd/platforms.go | 19 +++++++++++++------ cobra/cmd/publish.go | 23 +++++++++++++++-------- cobra/main.go | 5 +++++ go.mod | 2 +- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7be8a32..764219a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,14 +7,16 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cobra", - "args": ["publish", "--help"] + "args": ["publish", "--help"], + "console": "integratedTerminal" }, { "name": "Run publish (dry run)", "type": "go", "request": "launch", "program": "${workspaceFolder}/cobra", - "args": ["publish", "markdown"] + "args": ["publish", "markdown"], + "console": "integratedTerminal" } ] } \ No newline at end of file diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 87d65df..b5151e8 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -5,12 +5,11 @@ import ( "github.com/charmbracelet/log" "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" - "github.com/spf13/viper" ) type Platform interface { Push() - Pull() + // Pull() } // type PlatformParent struct { @@ -27,10 +26,10 @@ type Blogger struct { BlogId string } -func (b Blogger) authorize() (string, error) { +func (b Blogger) authorize(clientId string, clientSecret string) (string, error) { oauthConfig := oauth.Config{ - ClientID: viper.GetString("client-id"), - ClientSecret: viper.GetString("client-secret"), + ClientID: clientId, + ClientSecret: clientSecret, Port: "8080", } refreshToken, err := oauth.GetToken(oauthConfig) @@ -46,12 +45,20 @@ func (b Blogger) authorize() (string, error) { return accessToken, nil } +func (b Blogger) Push() { + log.Error("not implemented") +} + type Markdown struct { Name string ContentDir string } -func CreateDestination(destMap map[string]interface{}) (interface{}, error) { +func (m Markdown) Push() { + log.Error("not implemented") +} + +func CreateDestination(destMap map[string]interface{}) (Platform, error) { switch destMap["type"] { case "blogger": return Blogger{ diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 76c1535..367cd0f 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -21,7 +21,7 @@ var publishCmd = &cobra.Command{ } // Make a list of the respective Destination structs - var destinationSlice []interface{} + var destinationSlice []Platform // _ ignores the index. `dest` is the map for _, dest := range destinations.([]interface{}) { destMap, ok := dest.(map[string]interface{}) @@ -35,7 +35,10 @@ var publishCmd = &cobra.Command{ destinationSlice = append(destinationSlice, destination) } log.Info("Destination slice", "destinations", destinationSlice) - + // Check if OAuth works (remove this later!) + if blogger, ok := destinationSlice[0].(Blogger); ok { + blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret")) + } }, } @@ -44,11 +47,15 @@ func init() { publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") publishCmd.Flags().BoolP("dry-run", "r", false, "Don't actually publish") - publishCmd.Flags().String("client-id", "", "Google OAuth client ID") - publishCmd.Flags().String("client-secret", "", "Google OAuth client secret") - publishCmd.Flags().String("refresh-token", "", "Google OAuth refresh token") + publishCmd.Flags().String("google-client-id", "", "Google OAuth client ID") + publishCmd.Flags().String("google-client-secret", "", "Google OAuth client secret") + publishCmd.Flags().String("google-refresh-token", "", "Google OAuth refresh token") // Allow the OAuth stuff to be set via viper - viper.BindPFlag("client-id", publishCmd.Flags().Lookup("client-id")) - viper.BindPFlag("client-secret", publishCmd.Flags().Lookup("client-secret")) - viper.BindPFlag("refresh-token", publishCmd.Flags().Lookup("refresh-token")) + viper.BindPFlag("google-client-id", publishCmd.Flags().Lookup("google-client-id")) + viper.BindPFlag("google-client-secret", publishCmd.Flags().Lookup("google-client-secret")) + // viper.BindPFlag("google-refresh-token", publishCmd.Flags().Lookup("google-refresh-token")) + // Keep in mind that these should be prefixed with CROSS_BLOGGER + viper.BindEnv("google-client-id", "CROSS_BLOGGER_GOOGLE_CLIENT_ID") + viper.BindEnv("google-client-secret", "CROSS_BLOGGER_GOOGLE_CLIENT_SECRET") + // viper.BindEnv("google-refresh-token", "GOOGLE_REFRESH_TOKEN") } diff --git a/cobra/main.go b/cobra/main.go index 64fd8d1..0cd4d5a 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -10,6 +10,7 @@ import ( "github.com/slashtechno/cross-blogger/cobra/cmd" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/subosito/gotenv" ) var cfgFile string @@ -20,6 +21,10 @@ func init() { } func initConfig() { + // Load a .env file if it exists + gotenv.Load() + // Tell Viper to use the prefix "CROSS_BLOGGER" for environment variables + viper.SetEnvPrefix("CROSS_BLOGGER") // log.Debug(cfgFile) if cfgFile != "" { // Use config file from the flag. diff --git a/go.mod b/go.mod index 6fae23a..926acef 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + github.com/subosito/gotenv v1.6.0 github.com/tidwall/gjson v1.16.0 golang.org/x/oauth2 v0.18.0 ) @@ -43,7 +44,6 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect From aec7f584fa1871c5ecb5b19dcaa4d355a0572f44 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:00:10 +0000 Subject: [PATCH 14/25] feat: get the Blogger blog ID --- cobra/.gitignore | 1 + cobra/cmd/platforms.go | 32 +++++++++++++++++++++++++++----- cobra/cmd/publish.go | 19 ++++++++++++++++--- cobra/config.toml | 10 ---------- cobra/config.toml.example | 5 +++++ cobra/pkg/oauth/google.go | 2 +- go.mod | 7 ++++--- go.sum | 24 +++++++++++++++++++----- 8 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 cobra/.gitignore delete mode 100644 cobra/config.toml create mode 100644 cobra/config.toml.example diff --git a/cobra/.gitignore b/cobra/.gitignore new file mode 100644 index 0000000..ab8b69c --- /dev/null +++ b/cobra/.gitignore @@ -0,0 +1 @@ +config.toml \ No newline at end of file diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index b5151e8..bc728b8 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -4,12 +4,16 @@ import ( "fmt" "github.com/charmbracelet/log" + "github.com/go-resty/resty/v2" "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" ) -type Platform interface { +type Destination interface { Push() - // Pull() +} + +type Source interface { + Pull() } // type PlatformParent struct { @@ -23,7 +27,6 @@ type Platform interface { type Blogger struct { Name string BlogUrl string - BlogId string } func (b Blogger) authorize(clientId string, clientSecret string) (string, error) { @@ -44,10 +47,30 @@ func (b Blogger) authorize(clientId string, clientSecret string) (string, error) log.Info("", "access token", accessToken) return accessToken, nil } +func (b Blogger) getBlogId(accessToken string) (string, error) { + client := resty.New() + resp, err := client.R().SetHeader("Authorization", fmt.Sprintf("Bearer %s", accessToken)).SetResult(&map[string]interface{}{}).Get("https://www.googleapis.com/blogger/v3/blogs/byurl?url=" + b.BlogUrl) + if err != nil { + return "", err + } + if resp.StatusCode() != 200 { + return "", fmt.Errorf("failed to get blog id: %s", resp.String()) + } + // Get the key "id" from the response + result := (*resp.Result().(*map[string]interface{})) + id, ok := result["id"] + if !ok { + return "", fmt.Errorf("id not found in response") + } + return id.(string), nil +} func (b Blogger) Push() { log.Error("not implemented") } +func (b Blogger) Pull(postAddress string, accessToken string) { + +} type Markdown struct { Name string @@ -58,13 +81,12 @@ func (m Markdown) Push() { log.Error("not implemented") } -func CreateDestination(destMap map[string]interface{}) (Platform, error) { +func CreateDestination(destMap map[string]interface{}) (Destination, error) { switch destMap["type"] { case "blogger": return Blogger{ Name: destMap["name"].(string), BlogUrl: destMap["blog_url"].(string), - BlogId: destMap["blog_id"].(string), }, nil case "markdown": return Markdown{ diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 367cd0f..3715da8 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -10,7 +10,9 @@ var publishCmd = &cobra.Command{ Use: "publish", Short: "Publish to a destination", Long: `Publish to a destination from a source. - Specify the source with the first positional argument. All arguments after the first are treated as destinations. + Specify the source with the first positional argument. + The second positional argument is the specifier, such as a Blogger post URL or a file path. + All arguments after the first are treated as destinations. Destinations should be the name of the destinations specified in the config file`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -21,7 +23,7 @@ var publishCmd = &cobra.Command{ } // Make a list of the respective Destination structs - var destinationSlice []Platform + var destinationSlice []Destination // _ ignores the index. `dest` is the map for _, dest := range destinations.([]interface{}) { destMap, ok := dest.(map[string]interface{}) @@ -37,8 +39,19 @@ var publishCmd = &cobra.Command{ log.Info("Destination slice", "destinations", destinationSlice) // Check if OAuth works (remove this later!) if blogger, ok := destinationSlice[0].(Blogger); ok { - blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret")) + token, err := blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret")) + if err != nil { + log.Fatal(err) + } + log.Info("", "token", token) + blogId, err := blogger.getBlogId(token) + if err != nil { + log.Fatal(err) + } + log.Info("", "blog id", blogId) } + // Get the source from the first argument + }, } diff --git a/cobra/config.toml b/cobra/config.toml deleted file mode 100644 index da06cd6..0000000 --- a/cobra/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -[[destinations]] -blog_id = '1234567890' -blog_url = 'https://example.com' -name = 'blogger' -type = 'blogger' - -[[destinations]] -content_dir = 'content' -name = 'markdown1' -type = 'markdown' diff --git a/cobra/config.toml.example b/cobra/config.toml.example new file mode 100644 index 0000000..87244fc --- /dev/null +++ b/cobra/config.toml.example @@ -0,0 +1,5 @@ +[[destinations]] +name = 'blogger' +blog_url = 'https://example.com' +type = 'blogger' + diff --git a/cobra/pkg/oauth/google.go b/cobra/pkg/oauth/google.go index f26e6c3..eefcdb1 100644 --- a/cobra/pkg/oauth/google.go +++ b/cobra/pkg/oauth/google.go @@ -46,7 +46,7 @@ func GetToken(cfg Config) (string, error) { if err != nil { log.Fatal(err) } - + // Send the refresh token to the channel. tokenCh <- token.RefreshToken }) diff --git a/go.mod b/go.mod index 926acef..7f48dfe 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/JohannesKaufmann/html-to-markdown v1.4.0 github.com/alexflint/go-arg v1.4.3 github.com/charmbracelet/log v0.4.0 + github.com/go-resty/resty/v2 v2.13.1 github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c github.com/imdario/mergo v0.3.16 github.com/sirupsen/logrus v1.9.2 @@ -47,9 +48,9 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 7d64c1f..0e7a507 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= +github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -129,6 +131,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -140,8 +144,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -157,12 +163,17 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -170,8 +181,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From f222c2b3415ce31c27da5f89727f6ea0b4d5a058 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:02:00 +0000 Subject: [PATCH 15/25] feat: Add JS to close OAuth tab after token retrieval --- cobra/pkg/oauth/google.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cobra/pkg/oauth/google.go b/cobra/pkg/oauth/google.go index eefcdb1..2fb6caf 100644 --- a/cobra/pkg/oauth/google.go +++ b/cobra/pkg/oauth/google.go @@ -46,9 +46,13 @@ func GetToken(cfg Config) (string, error) { if err != nil { log.Fatal(err) } - + // Send the refresh token to the channel. tokenCh <- token.RefreshToken + + // Send a response to close the window + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("")) }) // Start the server in a separate goroutine. From 37f39117a936ac4e0577149f018e21816c580e11 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:49:35 +0000 Subject: [PATCH 16/25] feat: Continued to add logic for setting sources and destinations --- .vscode/launch.json | 2 +- cobra/cmd/platforms.go | 63 +++++++++++++++++++++-------- cobra/cmd/publish.go | 84 ++++++++++++++++++++++++++++++++++----- cobra/pkg/oauth/google.go | 4 +- 4 files changed, 123 insertions(+), 30 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 764219a..58827e1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "type": "go", "request": "launch", "program": "${workspaceFolder}/cobra", - "args": ["publish", "markdown"], + "args": ["publish", "blogger", "", "blogger"], "console": "integratedTerminal" } ] diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index bc728b8..a28eef1 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -10,10 +10,19 @@ import ( type Destination interface { Push() + GetName() string } type Source interface { - Pull() + Pull(SourceOptions) + GetName() string + GetType() string +} + +type SourceOptions struct { + AccessToken string + BlogId string + Filepath string } // type PlatformParent struct { @@ -29,18 +38,23 @@ type Blogger struct { BlogUrl string } -func (b Blogger) authorize(clientId string, clientSecret string) (string, error) { +func (b Blogger) authorize(clientId string, clientSecret string, providedRefreshToken string) (string, error) { oauthConfig := oauth.Config{ ClientID: clientId, ClientSecret: clientSecret, Port: "8080", } - refreshToken, err := oauth.GetToken(oauthConfig) - if err != nil { - return "", err + var refreshToken string + var err error + if providedRefreshToken == "" { + refreshToken, err = oauth.GetGoogleRefreshToken(oauthConfig) + if err != nil { + return "", err + } + } else { + refreshToken = providedRefreshToken } - - accessToken, err := oauth.GetAccessToken(oauthConfig, refreshToken) + accessToken, err := oauth.GetGoogleAccessToken(oauthConfig, refreshToken) if err != nil { return "", err } @@ -62,24 +76,24 @@ func (b Blogger) getBlogId(accessToken string) (string, error) { if !ok { return "", fmt.Errorf("id not found in response") } - return id.(string), nil } -func (b Blogger) Push() { - log.Error("not implemented") -} -func (b Blogger) Pull(postAddress string, accessToken string) { - +func (b Blogger) Pull(options SourceOptions) { + log.Info("Blogger pull called", "options", options) } +func (b Blogger) Push() { log.Error("not implemented") } +func (b Blogger) GetName() string { return b.Name } +func (b Blogger) GetType() string { return "blogger" } type Markdown struct { Name string ContentDir string } -func (m Markdown) Push() { - log.Error("not implemented") -} +func (m Markdown) Push() { log.Error("not implemented") } +func (m Markdown) Pull(options SourceOptions) { log.Error("not implemented") } +func (m Markdown) GetName() string { return m.Name } +func (m Markdown) GetType() string { return "markdown" } func CreateDestination(destMap map[string]interface{}) (Destination, error) { switch destMap["type"] { @@ -97,3 +111,20 @@ func CreateDestination(destMap map[string]interface{}) (Destination, error) { return nil, fmt.Errorf("unknown destination type: %s", destMap["type"]) } } + +func CreateSource(sourceMap map[string]interface{}) (Source, error) { + switch sourceMap["type"] { + case "blogger": + return Blogger{ + Name: sourceMap["name"].(string), + BlogUrl: sourceMap["blog_url"].(string), + }, nil + case "file": + return Markdown{ + Name: sourceMap["name"].(string), + ContentDir: sourceMap["content_dir"].(string), + }, nil + default: + return nil, fmt.Errorf("unknown source type: %s", sourceMap["type"]) + } +} diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 3715da8..212fada 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -16,13 +16,13 @@ var publishCmd = &cobra.Command{ Destinations should be the name of the destinations specified in the config file`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - // Get the list of objects `destinations` from Viper and make a list of Destination structs destinations := viper.Get("destinations") + sources := viper.Get("sources") if destinations == nil { log.Fatal("Failed to get destinations from config") } - // Make a list of the respective Destination structs + // Make a list of the Destination structs if the destination name is in the args var destinationSlice []Destination // _ ignores the index. `dest` is the map for _, dest := range destinations.([]interface{}) { @@ -34,24 +34,86 @@ var publishCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - destinationSlice = append(destinationSlice, destination) + // Check if the destination name is in the third argument or onwards + for _, arg := range args[2:] { + if destination.GetName() == arg { + log.Info("Adding destination", "destination", destination.GetName()) + destinationSlice = append(destinationSlice, destination) + } else { + log.Info("Not adding destination as it isn't specified in args", "destination", destination.GetName()) + } + } + } + log.Debug("Destination slice", "destinations", destinationSlice) + // Create a list of all the sources. If the source name is the first arg, use that source + var source Source + var found bool = false + for _, src := range sources.([]interface{}) { + sourceMap, ok := src.(map[string]interface{}) + if !ok { + log.Fatal("Failed to convert source to map") + } + src, err := CreateSource(sourceMap) + if err != nil { + log.Fatal(err) + } + if src.GetName() == args[0] { + source = src + found = true + log.Info("Found source", "source", source.GetName()) + } } - log.Info("Destination slice", "destinations", destinationSlice) + if !found { + log.Fatal("Failed to find source in config") + } + // Check if OAuth works (remove this later!) - if blogger, ok := destinationSlice[0].(Blogger); ok { - token, err := blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret")) + // if blogger, ok := destinationSlice[0].(Blogger); ok { + // token, err := blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret")) + // if err != nil { + // log.Fatal(err) + // } + // log.Info("", "token", token) + // blogId, err := blogger.getBlogId(token) + // if err != nil { + // log.Fatal(err) + // } + // log.Info("", "blog id", blogId) + // } + + // Pull the data from the source + var options SourceOptions + switch source.GetType() { + case "blogger": + // Convert source to Blogger + var blogger Blogger + if tmpBlogger, ok := source.(Blogger); ok { + log.Debug("Asserted that source is Blogger successfully") + blogger = tmpBlogger + } else { + log.Fatal("This shoud never happen but Blogger source assertion failed") + } + // If the refresh token exists in Viper, pass that to Blogger.Authorize. Otherwise, pass an empty string + refreshToken := viper.GetString("google-refresh-token") + if refreshToken == "" { + log.Warn("No refresh token found in Viper") + } else { + log.Info("Found refresh token in Viper") + } + accessToken, err := blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) if err != nil { log.Fatal(err) } - log.Info("", "token", token) - blogId, err := blogger.getBlogId(token) + blogId, err := blogger.getBlogId(accessToken) if err != nil { log.Fatal(err) } - log.Info("", "blog id", blogId) + options = SourceOptions{ + AccessToken: accessToken, + BlogId: blogId, + } + source.Pull(options) } - // Get the source from the first argument - }, } diff --git a/cobra/pkg/oauth/google.go b/cobra/pkg/oauth/google.go index 2fb6caf..865485f 100644 --- a/cobra/pkg/oauth/google.go +++ b/cobra/pkg/oauth/google.go @@ -20,7 +20,7 @@ type Config struct { // GetToken starts the OAuth2 flow, opens a temporary web server to handle the callback, // and returns the refresh token as a string. -func GetToken(cfg Config) (string, error) { +func GetGoogleRefreshToken(cfg Config) (string, error) { // Configure the Google OAuth2 client. googleOauthConfig := &oauth2.Config{ RedirectURL: "http://localhost:8080/callback", @@ -64,7 +64,7 @@ func GetToken(cfg Config) (string, error) { } // Return an access token from a refresh token -func GetAccessToken(cfg Config, refreshToken string) (string, error) { +func GetGoogleAccessToken(cfg Config, refreshToken string) (string, error) { // Configure the Google OAuth2 client. googleOauthConfig := &oauth2.Config{ RedirectURL: "http://localhost:" + cfg.Port + "/callback", From 3a24eda83e9f8eff67c962d7afd23a717fa5de03 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:15:42 +0000 Subject: [PATCH 17/25] feat: store the refresh token so the OAuth flow isn't required --- cobra/cmd/platforms.go | 24 +++++++++++++++++------- cobra/cmd/publish.go | 22 +++++++++++++++++++--- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index a28eef1..6c62eea 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -38,7 +38,10 @@ type Blogger struct { BlogUrl string } -func (b Blogger) authorize(clientId string, clientSecret string, providedRefreshToken string) (string, error) { +// Return the access token, refresh token (if one was not provided), and an error (if one occurred). +// The access and refresh tokens are only returned if an error did not occur. +// In Google Cloud, create OAuth client credentials for a desktop app and enable the Blogger API. +func (b Blogger) authorize(clientId string, clientSecret string, providedRefreshToken string) (string, string, error) { oauthConfig := oauth.Config{ ClientID: clientId, ClientSecret: clientSecret, @@ -46,20 +49,27 @@ func (b Blogger) authorize(clientId string, clientSecret string, providedRefresh } var refreshToken string var err error - if providedRefreshToken == "" { + if providedRefreshToken != "" { + log.Info("Using provided refresh token") + refreshToken = providedRefreshToken + } else { + log.Info("No refresh token provided, starting OAuth flow") refreshToken, err = oauth.GetGoogleRefreshToken(oauthConfig) if err != nil { - return "", err + return "", "", err } - } else { - refreshToken = providedRefreshToken } accessToken, err := oauth.GetGoogleAccessToken(oauthConfig, refreshToken) if err != nil { - return "", err + // Not returning the refresh token because it may have been invalid + return "", "", err } log.Info("", "access token", accessToken) - return accessToken, nil + if providedRefreshToken != "" { + return accessToken, providedRefreshToken, nil + } + return accessToken, refreshToken, nil + } func (b Blogger) getBlogId(accessToken string) (string, error) { client := resty.New() diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 212fada..ee9880c 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -95,12 +95,25 @@ var publishCmd = &cobra.Command{ } // If the refresh token exists in Viper, pass that to Blogger.Authorize. Otherwise, pass an empty string refreshToken := viper.GetString("google-refresh-token") + var accessToken string + var err error if refreshToken == "" { log.Warn("No refresh token found in Viper") + accessToken, refreshToken, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), "") + if err != nil { + log.Fatal(err) + } + // Write the refresh token to the config file + log.Info("Writing refresh token to Viper") + viper.Set("google-refresh-token", refreshToken) + err = viper.WriteConfig() + if err != nil { + log.Fatal(err) + } } else { log.Info("Found refresh token in Viper") + accessToken, _, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) } - accessToken, err := blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) if err != nil { log.Fatal(err) } @@ -125,12 +138,15 @@ func init() { publishCmd.Flags().String("google-client-id", "", "Google OAuth client ID") publishCmd.Flags().String("google-client-secret", "", "Google OAuth client secret") publishCmd.Flags().String("google-refresh-token", "", "Google OAuth refresh token") + // Keep in mind that if the refresh token is not set in the config file, the program will request one + // It will then write the refresh token to the config file, along with any flags or env vars that have been set. + // You could always go back and remove those lines and continue using environment variables or flags as it won't write to the config file as long as the refresh token is set // Allow the OAuth stuff to be set via viper viper.BindPFlag("google-client-id", publishCmd.Flags().Lookup("google-client-id")) viper.BindPFlag("google-client-secret", publishCmd.Flags().Lookup("google-client-secret")) - // viper.BindPFlag("google-refresh-token", publishCmd.Flags().Lookup("google-refresh-token")) + viper.BindPFlag("google-refresh-token", publishCmd.Flags().Lookup("google-refresh-token")) // Keep in mind that these should be prefixed with CROSS_BLOGGER viper.BindEnv("google-client-id", "CROSS_BLOGGER_GOOGLE_CLIENT_ID") viper.BindEnv("google-client-secret", "CROSS_BLOGGER_GOOGLE_CLIENT_SECRET") - // viper.BindEnv("google-refresh-token", "GOOGLE_REFRESH_TOKEN") + viper.BindEnv("google-refresh-token", "GOOGLE_REFRESH_TOKEN") } From 66ded0e42e95ec661e9cd518dc98f02a89f34c41 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:07:30 +0000 Subject: [PATCH 18/25] feat: Add support for pulling Blogger post data Just pulls the title and content for now. The content is also converted into markdown for later use --- .vscode/launch.json | 12 +++++++- cobra/cmd/platforms.go | 66 +++++++++++++++++++++++++++++++++++++----- cobra/cmd/publish.go | 10 ++++++- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 58827e1..0c1f443 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,8 +15,18 @@ "type": "go", "request": "launch", "program": "${workspaceFolder}/cobra", - "args": ["publish", "blogger", "", "blogger"], + // https://www.blogger.com/edit-profile.g + "args": ["publish", "blogger", "${input:PostURL}", "blogger"], + // "envFile": "${workspaceFolder}/cobra/.env", "console": "integratedTerminal" } + ], + "inputs": [ + { + "id": "PostURL", + "type": "promptString", + "description": "Enter the URL of the Blogger post to publish", + "default": "https://itsfrommars.blogspot.com/2022/08/hello-world.html" + } ] } \ No newline at end of file diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 6c62eea..3bbda1e 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -2,19 +2,21 @@ package cmd import ( "fmt" + "strings" + md "github.com/JohannesKaufmann/html-to-markdown" "github.com/charmbracelet/log" "github.com/go-resty/resty/v2" "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" ) type Destination interface { - Push() + Push() error GetName() string } type Source interface { - Pull(SourceOptions) + Pull(SourceOptions) (PostData, error) GetName() string GetType() string } @@ -23,6 +25,14 @@ type SourceOptions struct { AccessToken string BlogId string Filepath string + PostUrl string +} + +type PostData struct { + Title string + html string + markdown string + // Other fields that are probably needed are canonical URL, publish date, and description } // type PlatformParent struct { @@ -88,10 +98,44 @@ func (b Blogger) getBlogId(accessToken string) (string, error) { } return id.(string), nil } -func (b Blogger) Pull(options SourceOptions) { +func (b Blogger) Pull(options SourceOptions) (PostData, error) { log.Info("Blogger pull called", "options", options) + postPath := strings.Replace(options.PostUrl, b.BlogUrl, "", 1) + + client := resty.New() + resp, err := client.R().SetHeader("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken)).SetResult(&map[string]interface{}{}).Get("https://www.googleapis.com/blogger/v3/blogs/" + options.BlogId + "/posts/bypath?path=" + postPath) + if err != nil { + return PostData{}, err + } + if resp.StatusCode() != 200 { + return PostData{}, fmt.Errorf("failed to get post: %s", resp.String()) + } + // Get the keys "title" and "content" from the response + result := (*resp.Result().(*map[string]interface{})) + title, ok := result["title"].(string) + if !ok { + return PostData{}, fmt.Errorf("title not found in response or is not a string") + } + html, ok := result["content"].(string) + if !ok { + return PostData{}, fmt.Errorf("content not found in response or is not a string") + } + // Convert the HTML to Markdown + markdown, err := md.NewConverter("", true, nil).ConvertString(html) + if err != nil { + return PostData{}, err + } + return PostData{ + Title: title, + html: html, + markdown: markdown, + }, nil + +} +func (b Blogger) Push() error { + log.Error("not implemented") + return nil } -func (b Blogger) Push() { log.Error("not implemented") } func (b Blogger) GetName() string { return b.Name } func (b Blogger) GetType() string { return "blogger" } @@ -100,10 +144,16 @@ type Markdown struct { ContentDir string } -func (m Markdown) Push() { log.Error("not implemented") } -func (m Markdown) Pull(options SourceOptions) { log.Error("not implemented") } -func (m Markdown) GetName() string { return m.Name } -func (m Markdown) GetType() string { return "markdown" } +func (m Markdown) GetName() string { return m.Name } +func (m Markdown) GetType() string { return "markdown" } +func (m Markdown) Push() error { + log.Error("not implemented") + return nil +} +func (m Markdown) Pull(options SourceOptions) (PostData, error) { + log.Info("Markdown pull called", "options", options) + return PostData{}, nil +} func CreateDestination(destMap map[string]interface{}) (Destination, error) { switch destMap["type"] { diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index ee9880c..6098c69 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -124,9 +124,17 @@ var publishCmd = &cobra.Command{ options = SourceOptions{ AccessToken: accessToken, BlogId: blogId, + PostUrl: args[1], } - source.Pull(options) + case "markdown": + log.Fatal("Markdown source not implemented") } + // Pull the data from the source + postData, err := source.Pull(options) + if err != nil { + log.Fatal(err) + } + log.Info("Post data", "data", postData) }, } From 0792fb1b5e2a9935cd910bcb9025a472594fef68 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:42:53 +0000 Subject: [PATCH 19/25] feat: Implement posting to Markdown Frontmatter seems to not be written correctly, as can be seen by `cobra/output_markdown/hello-world.md` --- .vscode/launch.json | 2 +- cobra/cmd/platforms.go | 85 +++++++++++++++++++++++----- cobra/cmd/publish.go | 17 +++++- cobra/output_markdown/hello-world.md | 4 ++ go.mod | 4 +- go.sum | 4 ++ 6 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 cobra/output_markdown/hello-world.md diff --git a/.vscode/launch.json b/.vscode/launch.json index 0c1f443..3b48105 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,7 @@ "request": "launch", "program": "${workspaceFolder}/cobra", // https://www.blogger.com/edit-profile.g - "args": ["publish", "blogger", "${input:PostURL}", "blogger"], + "args": ["publish", "blogger", "${input:PostURL}", "markdown"], // "envFile": "${workspaceFolder}/cobra/.env", "console": "integratedTerminal" } diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 3bbda1e..5fff617 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -2,37 +2,43 @@ package cmd import ( "fmt" + "os" + "path/filepath" "strings" + "github.com/gosimple/slug" + md "github.com/JohannesKaufmann/html-to-markdown" "github.com/charmbracelet/log" "github.com/go-resty/resty/v2" "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" + "github.com/spf13/afero" ) type Destination interface { - Push() error + Push(PostData, PlatformOptions) error GetName() string + GetType() string } type Source interface { - Pull(SourceOptions) (PostData, error) + Pull(PlatformOptions) (PostData, error) GetName() string GetType() string } -type SourceOptions struct { +type PlatformOptions struct { AccessToken string BlogId string - Filepath string PostUrl string } type PostData struct { Title string - html string - markdown string + Html string + Markdown string // Other fields that are probably needed are canonical URL, publish date, and description + CanonicalUrl string } // type PlatformParent struct { @@ -98,10 +104,9 @@ func (b Blogger) getBlogId(accessToken string) (string, error) { } return id.(string), nil } -func (b Blogger) Pull(options SourceOptions) (PostData, error) { +func (b Blogger) Pull(options PlatformOptions) (PostData, error) { log.Info("Blogger pull called", "options", options) postPath := strings.Replace(options.PostUrl, b.BlogUrl, "", 1) - client := resty.New() resp, err := client.R().SetHeader("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken)).SetResult(&map[string]interface{}{}).Get("https://www.googleapis.com/blogger/v3/blogs/" + options.BlogId + "/posts/bypath?path=" + postPath) if err != nil { @@ -120,19 +125,24 @@ func (b Blogger) Pull(options SourceOptions) (PostData, error) { if !ok { return PostData{}, fmt.Errorf("content not found in response or is not a string") } + canonicalUrl, ok := result["url"].(string) + if !ok { + return PostData{}, fmt.Errorf("url not found in response or is not a string") + } // Convert the HTML to Markdown markdown, err := md.NewConverter("", true, nil).ConvertString(html) if err != nil { return PostData{}, err } return PostData{ - Title: title, - html: html, - markdown: markdown, + Title: title, + Html: html, + Markdown: markdown, + CanonicalUrl: canonicalUrl, }, nil } -func (b Blogger) Push() error { +func (b Blogger) Push(data PostData, options PlatformOptions) error { log.Error("not implemented") return nil } @@ -146,11 +156,56 @@ type Markdown struct { func (m Markdown) GetName() string { return m.Name } func (m Markdown) GetType() string { return "markdown" } -func (m Markdown) Push() error { - log.Error("not implemented") + +// Push the data to the contentdir with the title as the filename using gosimple/slug. +// The markdown file should have YAML frontmatter compatible with Hugo. +func (m Markdown) Push(data PostData, options PlatformOptions) error { + // Create the file, if it exists, log an error and return + fs := afero.NewOsFs() + slug := slug.Make(data.Title) + // Clean the slug to remove any characters that may cause issues with the filesystem + slug = filepath.Clean(slug) + filePath := filepath.Join(m.ContentDir, slug+".md") + // Create parent directories if they don't exist + dirPath := filepath.Dir(filePath) + if _, err := fs.Stat(dirPath); os.IsNotExist(err) { + errDir := fs.MkdirAll(dirPath, 0755) + if errDir != nil { + log.Error("failed to create directory", "directory", dirPath) + return errDir + } + } + // Check if the file already exists + if _, err := fs.Stat(filePath); err == nil { + log.Error("file already exists", "file", filePath) + return nil + } + // Create the file + file, err := fs.Create(filePath) + if err != nil { + return err + } + // After the function returns, close the file + defer file.Close() + // Create the frontmatter + frontmatter := struct { + Title string `yaml:"title"` + CanonicalUrl string `yaml:"canonicalUrl"` + }{ + Title: data.Title, + CanonicalUrl: data.CanonicalUrl, + } + // Convert the frontmatter to YAML + content := fmt.Sprintf("---\n%s---\n\n%s", frontmatter, data.Markdown) + log.Debug("Writing content", "content", content, "file", filePath) + _, err = file.WriteString(content) + if err != nil { + return err + } return nil + } -func (m Markdown) Pull(options SourceOptions) (PostData, error) { +func (m Markdown) Pull(options PlatformOptions) (PostData, error) { log.Info("Markdown pull called", "options", options) return PostData{}, nil } diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 6098c69..93151d1 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -82,7 +82,7 @@ var publishCmd = &cobra.Command{ // } // Pull the data from the source - var options SourceOptions + var options PlatformOptions switch source.GetType() { case "blogger": // Convert source to Blogger @@ -121,7 +121,7 @@ var publishCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - options = SourceOptions{ + options = PlatformOptions{ AccessToken: accessToken, BlogId: blogId, PostUrl: args[1], @@ -135,6 +135,19 @@ var publishCmd = &cobra.Command{ log.Fatal(err) } log.Info("Post data", "data", postData) + + // For each destination, push the data + for _, destination := range destinationSlice { + switch destination.GetType() { + case "markdown": + options := PlatformOptions{} + err := destination.Push(postData, options) + if err != nil { + log.Fatal(err) + } + + } + } }, } diff --git a/cobra/output_markdown/hello-world.md b/cobra/output_markdown/hello-world.md new file mode 100644 index 0000000..09cd64d --- /dev/null +++ b/cobra/output_markdown/hello-world.md @@ -0,0 +1,4 @@ +--- +{Hello, world! http://itsfrommars.blogspot.com/2022/08/hello-world.html}--- + +I created this post from the Google API. Hello, world! \ No newline at end of file diff --git a/go.mod b/go.mod index 7f48dfe..e231a88 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,11 @@ require ( github.com/charmbracelet/log v0.4.0 github.com/go-resty/resty/v2 v2.13.1 github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c + github.com/gosimple/slug v1.14.0 github.com/imdario/mergo v0.3.16 github.com/sirupsen/logrus v1.9.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 + github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/subosito/gotenv v1.6.0 @@ -28,6 +30,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -42,7 +45,6 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/go.sum b/go.sum index 0e7a507..1936e75 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,10 @@ github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= +github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= From e8fe17afe7f8337c187969375b944eca2596c4f9 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:52:13 +0000 Subject: [PATCH 20/25] fix: Marshal YAML before writing it I forgot to marshal the YAML before writing it --- cobra/cmd/platforms.go | 7 ++++++- cobra/cmd/publish.go | 4 ++-- cobra/output_markdown/hello-world.md | 4 +++- go.mod | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 5fff617..115993c 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gosimple/slug" + "gopkg.in/yaml.v2" md "github.com/JohannesKaufmann/html-to-markdown" "github.com/charmbracelet/log" @@ -196,7 +197,11 @@ func (m Markdown) Push(data PostData, options PlatformOptions) error { CanonicalUrl: data.CanonicalUrl, } // Convert the frontmatter to YAML - content := fmt.Sprintf("---\n%s---\n\n%s", frontmatter, data.Markdown) + frontmatterYaml, err := yaml.Marshal(frontmatter) + if err != nil { + return err + } + content := fmt.Sprintf("---\n%s---\n\n%s", frontmatterYaml, data.Markdown) log.Debug("Writing content", "content", content, "file", filePath) _, err = file.WriteString(content) if err != nil { diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 93151d1..5c0e530 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -134,7 +134,7 @@ var publishCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - log.Info("Post data", "data", postData) + log.Info("Successfully pulled data", "title", postData.Title, "url", postData.CanonicalUrl, "markdown", postData.Markdown) // For each destination, push the data for _, destination := range destinationSlice { @@ -153,7 +153,7 @@ var publishCmd = &cobra.Command{ func init() { RootCmd.AddCommand(publishCmd) - + // Perhaps add a -f flag to force overwrite posts/files if they already exist publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") publishCmd.Flags().BoolP("dry-run", "r", false, "Don't actually publish") publishCmd.Flags().String("google-client-id", "", "Google OAuth client ID") diff --git a/cobra/output_markdown/hello-world.md b/cobra/output_markdown/hello-world.md index 09cd64d..a536674 100644 --- a/cobra/output_markdown/hello-world.md +++ b/cobra/output_markdown/hello-world.md @@ -1,4 +1,6 @@ --- -{Hello, world! http://itsfrommars.blogspot.com/2022/08/hello-world.html}--- +title: Hello, world! +canonicalUrl: http://itsfrommars.blogspot.com/2022/08/hello-world.html +--- I created this post from the Google API. Hello, world! \ No newline at end of file diff --git a/go.mod b/go.mod index e231a88..abffaec 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/subosito/gotenv v1.6.0 github.com/tidwall/gjson v1.16.0 golang.org/x/oauth2 v0.18.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( From 55a49ce90f30ab6fe50176c8473f9e762d55ba50 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:04:58 +0000 Subject: [PATCH 21/25] feat: Add overwrite option for Markdown Also exists for Blogger but pushing isn't implemented yet --- cobra/cmd/platforms.go | 29 +++++++++++++++++++++-------- cobra/main.go | 9 +++++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 115993c..2df34fa 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -51,8 +51,9 @@ type PostData struct { // } type Blogger struct { - Name string - BlogUrl string + Name string + BlogUrl string + Overwrite bool } // Return the access token, refresh token (if one was not provided), and an error (if one occurred). @@ -151,8 +152,10 @@ func (b Blogger) GetName() string { return b.Name } func (b Blogger) GetType() string { return "blogger" } type Markdown struct { - Name string + Name string + // ContentDir, for retrieving, should only be used if treating the passed post path as relative results in no file found ContentDir string + Overwrite bool } func (m Markdown) GetName() string { return m.Name } @@ -177,10 +180,18 @@ func (m Markdown) Push(data PostData, options PlatformOptions) error { } } // Check if the file already exists - if _, err := fs.Stat(filePath); err == nil { - log.Error("file already exists", "file", filePath) - return nil + if _, err := fs.Stat(filePath); err == nil && !m.Overwrite { + return fmt.Errorf("file already exists and overwrite is false for file: %s", filePath) + } else if err != nil && !os.IsNotExist(err) { // If the error is not a "file does not exist" error + return err + } else if err == nil && m.Overwrite { // If the file exists and overwrite is true, remove the file + log.Info("Removing file as overwrite is true", "file", filePath) + err := fs.Remove(filePath) + if err != nil { + return err + } } + // Create the file file, err := fs.Create(filePath) if err != nil { @@ -219,13 +230,15 @@ func CreateDestination(destMap map[string]interface{}) (Destination, error) { switch destMap["type"] { case "blogger": return Blogger{ - Name: destMap["name"].(string), - BlogUrl: destMap["blog_url"].(string), + Name: destMap["name"].(string), + BlogUrl: destMap["blog_url"].(string), + Overwrite: destMap["overwrite"].(bool), }, nil case "markdown": return Markdown{ Name: destMap["name"].(string), ContentDir: destMap["content_dir"].(string), + Overwrite: destMap["overwrite"].(bool), }, nil default: return nil, fmt.Errorf("unknown destination type: %s", destMap["type"]) diff --git a/cobra/main.go b/cobra/main.go index 0cd4d5a..b5db32d 100644 --- a/cobra/main.go +++ b/cobra/main.go @@ -43,15 +43,16 @@ func initConfig() { log.Debug("Config file not found, creating a new one") viper.SetDefault("destinations", []map[string]interface{}{ { - "name": "blogger", - "type": "blogger", - "blog_url": "https://example.com", - "blog_id": "1234567890", + "name": "blogger", + "type": "blogger", + "blog_url": "https://example.com", + "overwrite": false, }, { "name": "markdown1", "type": "markdown", "content_dir": "content", + "overwrite": false, }, }) log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) From 11ab12136007707cefca843c111959ca414d5330 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:21:12 +0000 Subject: [PATCH 22/25] refactor: add prepareBlogger function to improve readability --- cobra/cmd/platforms.go | 1 + cobra/cmd/publish.go | 69 ++++++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 2df34fa..af48b57 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -53,6 +53,7 @@ type PostData struct { type Blogger struct { Name string BlogUrl string + // https://developers.google.com/blogger/docs/3.0/reference/posts/delete Overwrite bool } diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 5c0e530..d3a783c 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/charmbracelet/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -85,35 +87,7 @@ var publishCmd = &cobra.Command{ var options PlatformOptions switch source.GetType() { case "blogger": - // Convert source to Blogger - var blogger Blogger - if tmpBlogger, ok := source.(Blogger); ok { - log.Debug("Asserted that source is Blogger successfully") - blogger = tmpBlogger - } else { - log.Fatal("This shoud never happen but Blogger source assertion failed") - } - // If the refresh token exists in Viper, pass that to Blogger.Authorize. Otherwise, pass an empty string - refreshToken := viper.GetString("google-refresh-token") - var accessToken string - var err error - if refreshToken == "" { - log.Warn("No refresh token found in Viper") - accessToken, refreshToken, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), "") - if err != nil { - log.Fatal(err) - } - // Write the refresh token to the config file - log.Info("Writing refresh token to Viper") - viper.Set("google-refresh-token", refreshToken) - err = viper.WriteConfig() - if err != nil { - log.Fatal(err) - } - } else { - log.Info("Found refresh token in Viper") - accessToken, _, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) - } + blogger, accessToken, err := prepareBlogger(source) if err != nil { log.Fatal(err) } @@ -171,3 +145,40 @@ func init() { viper.BindEnv("google-client-secret", "CROSS_BLOGGER_GOOGLE_CLIENT_SECRET") viper.BindEnv("google-refresh-token", "GOOGLE_REFRESH_TOKEN") } + +// Return the Blogger object and a string with the access token. +func prepareBlogger(source Source) (Blogger, string, error) { + // Convert source to Blogger + var blogger Blogger + if tmpBlogger, ok := source.(Blogger); ok { + log.Debug("Asserted that source is Blogger successfully") + blogger = tmpBlogger + } else { + return Blogger{}, "", fmt.Errorf("failed to assert that source is Blogger - potentially due to being called on a non-Blogger source") + } + // If the refresh token exists in Viper, pass that to Blogger.Authorize. Otherwise, pass an empty string + refreshToken := viper.GetString("google-refresh-token") + var accessToken string + var err error + if refreshToken == "" { + log.Warn("No refresh token found in Viper") + accessToken, refreshToken, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), "") + if err != nil { + return Blogger{}, "", err + } + // Write the refresh token to the config file + log.Info("Writing refresh token to Viper") + viper.Set("google-refresh-token", refreshToken) + err = viper.WriteConfig() + if err != nil { + return Blogger{}, "", err + } + } else { + log.Info("Found refresh token in Viper") + accessToken, _, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) + } + if err != nil { + return Blogger{}, "", err + } + return blogger, accessToken, nil +} From 277c5a67e7d710bec15eb4efdb4b6ba3881e895f Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:44:43 +0000 Subject: [PATCH 23/25] feat: add support for publishing to Blogger fix: prevent panic when a config key is not set refactor: rename `PlatformOptions` to `PushPullOptions` --- .vscode/launch.json | 6 +- cobra/cmd/platforms.go | 122 ++++++++++++++++++++++++++++++++++------- cobra/cmd/publish.go | 66 ++++++++++++++++------ 3 files changed, 154 insertions(+), 40 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3b48105..b9c9d03 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,12 +11,12 @@ "console": "integratedTerminal" }, { - "name": "Run publish (dry run)", + "name": "Publish from blogger", "type": "go", "request": "launch", "program": "${workspaceFolder}/cobra", // https://www.blogger.com/edit-profile.g - "args": ["publish", "blogger", "${input:PostURL}", "markdown"], + "args": ["publish", "blogger", "${input:PostURL}", "blogger"], // "envFile": "${workspaceFolder}/cobra/.env", "console": "integratedTerminal" } @@ -26,7 +26,7 @@ "id": "PostURL", "type": "promptString", "description": "Enter the URL of the Blogger post to publish", - "default": "https://itsfrommars.blogspot.com/2022/08/hello-world.html" + "default": "https://itsfrommars.blogspot.com/2024/06/hello-world_78.html" } ] } \ No newline at end of file diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index af48b57..491d4c5 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -17,18 +17,18 @@ import ( ) type Destination interface { - Push(PostData, PlatformOptions) error + Push(PostData, PushPullOptions) error GetName() string GetType() string } type Source interface { - Pull(PlatformOptions) (PostData, error) + Pull(PushPullOptions) (PostData, error) GetName() string GetType() string } -type PlatformOptions struct { +type PushPullOptions struct { AccessToken string BlogId string PostUrl string @@ -51,8 +51,8 @@ type PostData struct { // } type Blogger struct { - Name string - BlogUrl string + Name string + BlogUrl string // https://developers.google.com/blogger/docs/3.0/reference/posts/delete Overwrite bool } @@ -107,7 +107,7 @@ func (b Blogger) getBlogId(accessToken string) (string, error) { } return id.(string), nil } -func (b Blogger) Pull(options PlatformOptions) (PostData, error) { +func (b Blogger) Pull(options PushPullOptions) (PostData, error) { log.Info("Blogger pull called", "options", options) postPath := strings.Replace(options.PostUrl, b.BlogUrl, "", 1) client := resty.New() @@ -145,8 +145,58 @@ func (b Blogger) Pull(options PlatformOptions) (PostData, error) { }, nil } -func (b Blogger) Push(data PostData, options PlatformOptions) error { - log.Error("not implemented") +func (b Blogger) Push(data PostData, options PushPullOptions) error { + // Set the client + client := resty.New() + blogId := options.BlogId + + // Delete any post with the same ID + if b.Overwrite { + // Get the list of existing posts + resp, err := client.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken)). + SetResult(&map[string]interface{}{}). + Get("https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts") + if err != nil { + return err + } + posts := (*resp.Result().(*map[string]interface{}))["items"].([]interface{}) + + // Check if a post with the same title already exists + for _, p := range posts { + post := p.(map[string]interface{}) + if post["title"].(string) == data.Title { + // Delete the post + _, err := client.R(). + SetQueryParam("useTrash", "true"). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken)). + Delete("https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/" + post["id"].(string)) + if err != nil { + return err + } + log.Info("Moved post with the same title to trash", "title", data.Title) + // If break is used and there are multiple posts with the same title, only the first one will be deleted + // break + } + } + } + log.Warn("Blogger does not support setting the canonical URL") + // Prepare the request + req := client.R().SetHeader("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken)).SetBody(map[string]interface{}{ + "title": data.Title, + "content": data.Html, + // "url": data.CanonicalUrl, + }).SetResult(&map[string]interface{}{}) + // Make the request + resp, err := req.Post("https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts") + if err != nil { + return err + } + if resp.StatusCode() != 200 { + return fmt.Errorf("failed to post: %s", resp.String()) + } + result := (*resp.Result().(*map[string]interface{})) + log.Debug("Posted successfully", "result", result) return nil } func (b Blogger) GetName() string { return b.Name } @@ -164,7 +214,7 @@ func (m Markdown) GetType() string { return "markdown" } // Push the data to the contentdir with the title as the filename using gosimple/slug. // The markdown file should have YAML frontmatter compatible with Hugo. -func (m Markdown) Push(data PostData, options PlatformOptions) error { +func (m Markdown) Push(data PostData, options PushPullOptions) error { // Create the file, if it exists, log an error and return fs := afero.NewOsFs() slug := slug.Make(data.Title) @@ -222,24 +272,43 @@ func (m Markdown) Push(data PostData, options PlatformOptions) error { return nil } -func (m Markdown) Pull(options PlatformOptions) (PostData, error) { +func (m Markdown) Pull(options PushPullOptions) (PostData, error) { log.Info("Markdown pull called", "options", options) return PostData{}, nil } func CreateDestination(destMap map[string]interface{}) (Destination, error) { + name, ok := destMap["name"].(string) + if !ok || name == "" { + return nil, fmt.Errorf("name is required") + } + switch destMap["type"] { case "blogger": + blogUrl, ok := destMap["blog_url"].(string) + if !ok || blogUrl == "" { + return nil, fmt.Errorf("blog_url is required for blogger") + } + + overwrite, _ := destMap["overwrite"].(bool) // If not set or not a bool, defaults to false + return Blogger{ - Name: destMap["name"].(string), - BlogUrl: destMap["blog_url"].(string), - Overwrite: destMap["overwrite"].(bool), + Name: name, + BlogUrl: blogUrl, + Overwrite: overwrite, }, nil case "markdown": + contentDir, ok := destMap["content_dir"].(string) + if !ok || contentDir == "" { + return nil, fmt.Errorf("content_dir is required for markdown") + } + + overwrite, _ := destMap["overwrite"].(bool) // If not set or not a bool, defaults to false + return Markdown{ - Name: destMap["name"].(string), - ContentDir: destMap["content_dir"].(string), - Overwrite: destMap["overwrite"].(bool), + Name: name, + ContentDir: contentDir, + Overwrite: overwrite, }, nil default: return nil, fmt.Errorf("unknown destination type: %s", destMap["type"]) @@ -247,16 +316,29 @@ func CreateDestination(destMap map[string]interface{}) (Destination, error) { } func CreateSource(sourceMap map[string]interface{}) (Source, error) { + // In Go, ifa type assertion fails, it will return the zero value of the type and false. + name, ok := sourceMap["name"].(string) + if !ok || name == "" { + return nil, fmt.Errorf("name is required") + } + switch sourceMap["type"] { case "blogger": + blogUrl, ok := sourceMap["blog_url"].(string) + if !ok || blogUrl == "" { + return nil, fmt.Errorf("blog_url is required for blogger") + } + return Blogger{ - Name: sourceMap["name"].(string), - BlogUrl: sourceMap["blog_url"].(string), + Name: name, + BlogUrl: blogUrl, }, nil case "file": + // If the content_dir is not set, set it to null as its not required + contentDir, _ := sourceMap["content_dir"].(string) return Markdown{ - Name: sourceMap["name"].(string), - ContentDir: sourceMap["content_dir"].(string), + Name: name, + ContentDir: contentDir, }, nil default: return nil, fmt.Errorf("unknown source type: %s", sourceMap["type"]) diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index d3a783c..8107629 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -84,18 +84,15 @@ var publishCmd = &cobra.Command{ // } // Pull the data from the source - var options PlatformOptions + var options PushPullOptions switch source.GetType() { case "blogger": - blogger, accessToken, err := prepareBlogger(source) + _, accessToken, blogId, err := prepareBlogger(source, nil) if err != nil { log.Fatal(err) } - blogId, err := blogger.getBlogId(accessToken) - if err != nil { - log.Fatal(err) - } - options = PlatformOptions{ + + options = PushPullOptions{ AccessToken: accessToken, BlogId: blogId, PostUrl: args[1], @@ -112,14 +109,30 @@ var publishCmd = &cobra.Command{ // For each destination, push the data for _, destination := range destinationSlice { + var found bool = true switch destination.GetType() { case "markdown": - options := PlatformOptions{} + options = PushPullOptions{} + + case "blogger": + _, accessToken, blogId, err := prepareBlogger(nil, destination) + if err != nil { + log.Fatal(err) + } + options = PushPullOptions{ + AccessToken: accessToken, + BlogId: blogId, + } + default: + found = false + } + if found { err := destination.Push(postData, options) if err != nil { log.Fatal(err) } - + } else { + log.Error("Destination type not found", "type", destination.GetType()) } } }, @@ -146,15 +159,29 @@ func init() { viper.BindEnv("google-refresh-token", "GOOGLE_REFRESH_TOKEN") } -// Return the Blogger object and a string with the access token. -func prepareBlogger(source Source) (Blogger, string, error) { +// Return the Blogger object and a string with the access token, the blog ID, and an error (if one occurred) +func prepareBlogger(source Source, destination Destination) (Blogger, string, string, error) { + // Check if the user passed a source or destination. Exactly one should be passed. + var platform interface{} + if source == nil && destination == nil { + return Blogger{}, "", "", fmt.Errorf("no source or destination passed") + } else if source != nil && destination != nil { + return Blogger{}, "", "", fmt.Errorf("both source and destination passed") + } else if source != nil { + platform = source + } else if destination != nil { + platform = destination + } else { + return Blogger{}, "", "", fmt.Errorf("failed to determine if source or destination was passed") + } + // Convert source to Blogger var blogger Blogger - if tmpBlogger, ok := source.(Blogger); ok { + if tmpBlogger, ok := platform.(Blogger); ok { log.Debug("Asserted that source is Blogger successfully") blogger = tmpBlogger } else { - return Blogger{}, "", fmt.Errorf("failed to assert that source is Blogger - potentially due to being called on a non-Blogger source") + return Blogger{}, "", "", fmt.Errorf("failed to assert that source is Blogger - potentially due to being called on a non-Blogger source") } // If the refresh token exists in Viper, pass that to Blogger.Authorize. Otherwise, pass an empty string refreshToken := viper.GetString("google-refresh-token") @@ -164,21 +191,26 @@ func prepareBlogger(source Source) (Blogger, string, error) { log.Warn("No refresh token found in Viper") accessToken, refreshToken, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), "") if err != nil { - return Blogger{}, "", err + return Blogger{}, "", "", err } // Write the refresh token to the config file log.Info("Writing refresh token to Viper") viper.Set("google-refresh-token", refreshToken) err = viper.WriteConfig() if err != nil { - return Blogger{}, "", err + return Blogger{}, "", "", err } } else { log.Info("Found refresh token in Viper") accessToken, _, err = blogger.authorize(viper.GetString("google-client-id"), viper.GetString("google-client-secret"), refreshToken) } if err != nil { - return Blogger{}, "", err + return Blogger{}, "", "", err + } + + blogId, err := blogger.getBlogId(accessToken) + if err != nil { + return Blogger{}, "", "", err } - return blogger, accessToken, nil + return blogger, accessToken, blogId, nil } From d8c857959bf7c2fbd24cca135027fe0f0ceaa925 Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:43:30 +0000 Subject: [PATCH 24/25] feat!: Publish from Markdown --- .vscode/launch.json | 24 +++++++++- cobra/.gitignore | 3 +- cobra/cmd/platforms.go | 72 ++++++++++++++++++++++++---- cobra/cmd/publish.go | 10 +++- cobra/output_markdown/hello-world.md | 6 --- go.mod | 3 ++ go.sum | 7 ++- 7 files changed, 105 insertions(+), 20 deletions(-) delete mode 100644 cobra/output_markdown/hello-world.md diff --git a/.vscode/launch.json b/.vscode/launch.json index b9c9d03..1b85eea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,9 +16,17 @@ "request": "launch", "program": "${workspaceFolder}/cobra", // https://www.blogger.com/edit-profile.g - "args": ["publish", "blogger", "${input:PostURL}", "blogger"], + "args": ["publish", "blogger", "${input:PostURL}", "${input:Destination}"], // "envFile": "${workspaceFolder}/cobra/.env", "console": "integratedTerminal" + }, + { + "name": "Publish from Markdown", + "type": "go", + "request": "launch", + "program": "${workspaceFolder}/cobra", + "args": ["publish", "markdown", "${input:MarkdownPath}", "${input:Destination}"], + "console": "integratedTerminal" } ], "inputs": [ @@ -26,7 +34,19 @@ "id": "PostURL", "type": "promptString", "description": "Enter the URL of the Blogger post to publish", - "default": "https://itsfrommars.blogspot.com/2024/06/hello-world_78.html" + "default": "https://itsfrommars.blogspot.com/2024/06/hello-world_11.html" + }, + { + "id": "MarkdownPath", + "type": "promptString", + "description": "Enter the path of the Markdown file to publish", + "default": "hello-world.md" + }, + { + "id": "Destination", + "type": "promptString", + "description": "Enter the destination to publish", + "default": "markdown" } ] } \ No newline at end of file diff --git a/cobra/.gitignore b/cobra/.gitignore index ab8b69c..6b97628 100644 --- a/cobra/.gitignore +++ b/cobra/.gitignore @@ -1 +1,2 @@ -config.toml \ No newline at end of file +config.toml +*_markdown/ \ No newline at end of file diff --git a/cobra/cmd/platforms.go b/cobra/cmd/platforms.go index 491d4c5..7230b8d 100644 --- a/cobra/cmd/platforms.go +++ b/cobra/cmd/platforms.go @@ -1,12 +1,15 @@ package cmd import ( + "bytes" "fmt" "os" "path/filepath" "strings" "github.com/gosimple/slug" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" "gopkg.in/yaml.v2" md "github.com/JohannesKaufmann/html-to-markdown" @@ -14,6 +17,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" "github.com/spf13/afero" + "go.abhg.dev/goldmark/frontmatter" ) type Destination interface { @@ -32,6 +36,12 @@ type PushPullOptions struct { AccessToken string BlogId string PostUrl string + Filepath string +} + +type Frontmatter struct { + Title string `yaml:"title"` + CanonicalUrl string `yaml:"canonicalURL"` } type PostData struct { @@ -251,15 +261,12 @@ func (m Markdown) Push(data PostData, options PushPullOptions) error { // After the function returns, close the file defer file.Close() // Create the frontmatter - frontmatter := struct { - Title string `yaml:"title"` - CanonicalUrl string `yaml:"canonicalUrl"` - }{ + postFrontmatter := Frontmatter{ Title: data.Title, CanonicalUrl: data.CanonicalUrl, } // Convert the frontmatter to YAML - frontmatterYaml, err := yaml.Marshal(frontmatter) + frontmatterYaml, err := yaml.Marshal(postFrontmatter) if err != nil { return err } @@ -273,8 +280,57 @@ func (m Markdown) Push(data PostData, options PushPullOptions) error { } func (m Markdown) Pull(options PushPullOptions) (PostData, error) { - log.Info("Markdown pull called", "options", options) - return PostData{}, nil + // Get the file path + fs := afero.NewOsFs() + // Treat the post path as relative to the content dir + // However, if the content dir does not exist or the file is not found, treat the post path as a normal path without the content dir + filePath := filepath.Join(m.ContentDir, options.Filepath) + if _, err := fs.Stat(filePath); os.IsNotExist(err) { + filePath = options.Filepath + } + // Read the file + data, err := afero.ReadFile(fs, filePath) + if err != nil { + return PostData{}, err + } + markdown := string(data) + // Convert the markdown to HTML with Goldmark + // Use the Frontmatter extension to get the frontmatter + mdParser := goldmark.New(goldmark.WithExtensions(&frontmatter.Extender{})) + ctx := parser.NewContext() + var buf bytes.Buffer + err = mdParser.Convert([]byte(markdown), &buf, parser.WithContext(ctx)) + if err != nil { + return PostData{}, err + } + // Get the frontmatter + markdownFrontmatter := Frontmatter{} + frontmatterData := frontmatter.Get(ctx) + if err := frontmatterData.Decode(&markdownFrontmatter); err != nil { + return PostData{}, err + } + // Check if title and canonical URL are set + if markdownFrontmatter.Title == "" { + return PostData{}, fmt.Errorf("title is required in frontmatter") + } + if markdownFrontmatter.CanonicalUrl == "" { + log.Warn("canonical_url is not set in frontmatter") + } + // Convert the HTML to Markdown + html := buf.String() + // The frontmatter is stripped before converting to HTML + // Just convert the HTML to Markdown so the Markdown doesn't have the frontmatter (otherwise it would be duplicated) + markdown, err = md.NewConverter("", true, nil).ConvertString(html) + if err != nil { + return PostData{}, err + } + return PostData{ + Title: markdownFrontmatter.Title, + Html: html, + Markdown: markdown, + CanonicalUrl: markdownFrontmatter.CanonicalUrl, + }, nil + } func CreateDestination(destMap map[string]interface{}) (Destination, error) { @@ -333,7 +389,7 @@ func CreateSource(sourceMap map[string]interface{}) (Source, error) { Name: name, BlogUrl: blogUrl, }, nil - case "file": + case "markdown": // If the content_dir is not set, set it to null as its not required contentDir, _ := sourceMap["content_dir"].(string) return Markdown{ diff --git a/cobra/cmd/publish.go b/cobra/cmd/publish.go index 8107629..ec8f38b 100644 --- a/cobra/cmd/publish.go +++ b/cobra/cmd/publish.go @@ -98,7 +98,9 @@ var publishCmd = &cobra.Command{ PostUrl: args[1], } case "markdown": - log.Fatal("Markdown source not implemented") + options = PushPullOptions{ + Filepath: args[1], + } } // Pull the data from the source postData, err := source.Pull(options) @@ -127,6 +129,11 @@ var publishCmd = &cobra.Command{ found = false } if found { + // Check if this is a dry run + if viper.GetBool("dry-run") { + log.Info("Dry run - not pushing data") + continue + } err := destination.Push(postData, options) if err != nil { log.Fatal(err) @@ -140,7 +147,6 @@ var publishCmd = &cobra.Command{ func init() { RootCmd.AddCommand(publishCmd) - // Perhaps add a -f flag to force overwrite posts/files if they already exist publishCmd.Flags().StringP("title", "t", "", "Specify custom title instead of using the default") publishCmd.Flags().BoolP("dry-run", "r", false, "Don't actually publish") publishCmd.Flags().String("google-client-id", "", "Google OAuth client ID") diff --git a/cobra/output_markdown/hello-world.md b/cobra/output_markdown/hello-world.md deleted file mode 100644 index a536674..0000000 --- a/cobra/output_markdown/hello-world.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Hello, world! -canonicalUrl: http://itsfrommars.blogspot.com/2022/08/hello-world.html ---- - -I created this post from the Google API. Hello, world! \ No newline at end of file diff --git a/go.mod b/go.mod index abffaec..60474f6 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( github.com/spf13/viper v1.19.0 github.com/subosito/gotenv v1.6.0 github.com/tidwall/gjson v1.16.0 + github.com/yuin/goldmark v1.7.2 + go.abhg.dev/goldmark/frontmatter v0.2.0 golang.org/x/oauth2 v0.18.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -24,6 +26,7 @@ require ( require ( cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect diff --git a/go.sum b/go.sum index 1936e75..2797f0d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1Yl cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/JohannesKaufmann/html-to-markdown v1.4.0 h1:uaIPDub6VrBsQP0r5xKjpPo9lxMcuQF1L1pT6BiBdmw= github.com/JohannesKaufmann/html-to-markdown v1.4.0/go.mod h1:3p+lDUqSw+cxolZl7OINYzJ70JHXogXjyCl9UnMQ5gU= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= @@ -127,8 +129,11 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= +github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= +go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= From feb34e4061c911085fd653af59d5bd81abbaa8ff Mon Sep 17 00:00:00 2001 From: Angad Behl <77907286+slashtechno@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:10:18 +0000 Subject: [PATCH 25/25] feat!: Complete migration to Cobra docs: update README for use with Cobra --- .gitignore | 8 +- .vscode/launch.json | 8 +- README.md | 41 +-- args.go | 40 --- {cobra/cmd => cmd}/platforms.go | 2 +- {cobra/cmd => cmd}/publish.go | 0 {cobra/cmd => cmd}/root.go | 0 cobra/.gitignore | 2 - cobra/config.toml.example | 5 - cobra/main.go | 71 ----- config.toml.example | 26 ++ main.go | 470 ++++------------------------- {cobra/pkg => pkg}/oauth/google.go | 0 13 files changed, 120 insertions(+), 553 deletions(-) delete mode 100644 args.go rename {cobra/cmd => cmd}/platforms.go (99%) rename {cobra/cmd => cmd}/publish.go (100%) rename {cobra/cmd => cmd}/root.go (100%) delete mode 100644 cobra/.gitignore delete mode 100644 cobra/config.toml.example delete mode 100644 cobra/main.go create mode 100644 config.toml.example rename {cobra/pkg => pkg}/oauth/google.go (100%) diff --git a/.gitignore b/.gitignore index b602352..3a49983 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -test* -output.* -.env* -config.json -_debug* \ No newline at end of file +config.toml +*_markdown/ +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1b85eea..be362ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", "args": ["publish", "--help"], "console": "integratedTerminal" }, @@ -14,17 +14,17 @@ "name": "Publish from blogger", "type": "go", "request": "launch", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", // https://www.blogger.com/edit-profile.g "args": ["publish", "blogger", "${input:PostURL}", "${input:Destination}"], - // "envFile": "${workspaceFolder}/cobra/.env", + // "envFile": "${workspaceFolder}/.env", "console": "integratedTerminal" }, { "name": "Publish from Markdown", "type": "go", "request": "launch", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", "args": ["publish", "markdown", "${input:MarkdownPath}", "${input:Destination}"], "console": "integratedTerminal" } diff --git a/README.md b/README.md index 9bc9df6..56c9f12 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # cross-blogger -Cross-service (and cross-platform) blog posting utility +Soon-to-be headless CMS for static site generators powered by Google's Blogger. ### Installation #### Compiled Binaries @@ -11,25 +11,28 @@ Using `go install`, you can compile and add the program to the PATH. Either run `go install github.com/slashtechno/cross-blogger@latest`, follow the same process as compiling the program locally, but replace `go build` with `go install`. ### Usage -To use this program, just run the executable in the terminal. +Sources and destinations should first be configured in the `config.yaml` file. +For Google OAuth, the `--client-id` and `--client-secret` flags are required but can be set as environment variables (`CROSS_BLOGGER_GOOGLE_CLIENT_ID`/`CROSS_BLOGGER_GOOGLE_CLIENT_SECRET`). However these can also be set in the `config.yaml` file, passed as environment variables, or put in a `.env` file. When a refresh token is not provided, the program will commence the OAuth flow. This will write the refresh token, along with any other configuration, to the `config.yaml` file. If you prefer to use other methods to pass the credentials, you can remove the lines and use the other methods. #### Help Output -From `cross-blogger --help` +From `cross-blogger publish --help` ```text -Usage: cross-blogger.exe --client-id CLIENT-ID --client-secret CLIENT-SECRET [--refresh-token REFRESH-TOKEN] [--log-level LOG-LEVEL] [--log-color] [] - -Options: - --client-id CLIENT-ID - Google OAuth client ID [env: CLIENT_ID] - --client-secret CLIENT-SECRET - Google OAuth client secret [env: CLIENT_SECRET] - --refresh-token REFRESH-TOKEN - Google OAuth refresh token [env: REFRESH_TOKEN] - --log-level LOG-LEVEL - "debug", "info", "warning", "error", or "fatal" [default: info, env: LOG_LEVEL] - --log-color Force colored logs [default: false, env: LOG_COLOR] - --help, -h display this help and exit +Publish to a destination from a source. + Specify the source with the first positional argument. + The second positional argument is the specifier, such as a Blogger post URL or a file path. + All arguments after the first are treated as destinations. + Destinations should be the name of the destinations specified in the config file -Commands: - google-oauth Store Google OAuth refresh token - publish Publish to a destination +Usage: + cross-blogger publish [flags] + +Flags: + -r, --dry-run Don't actually publish + --google-client-id string Google OAuth client ID + --google-client-secret string Google OAuth client secret + --google-refresh-token string Google OAuth refresh token + -h, --help help for publish + -t, --title string Specify custom title instead of using the default + +Global Flags: + --config string config file path (default "config.toml") ``` diff --git a/args.go b/args.go deleted file mode 100644 index fb3ad28..0000000 --- a/args.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -type BloggerCmd struct { - BlogAddress string `arg:"positional, required" help:"Blog address to get content from"` - PostAddress string `arg:"positional, required" help:"Post slug to get content from"` -} - -type FileCmd struct { - Filepath string `arg:"positional, required" help:"Filepath to get content from"` -} - -type PublishCmd struct { - File *FileCmd `arg:"subcommand:file" help:"Publish from a file"` - Blogger *BloggerCmd `arg:"subcommand:blogger" help:"Publish from Blogger"` - // Perhaps instead of needing both a key and a value for destinations, parse a single value - // For example, check if it's a file, and if so, check the file ending to determine the type - // Maybe check if it contains blogger.com - // Of course, an override would be nice - Destinations map[string]string `arg:"--destinations, required" help:"Destination(s) to publish to\nAvailable destinations: blogger, markdown, html\nMake sure to specify with ="` - Title string `arg:"-t,--title" help:"Specify custom title instead of using the default"` - DryRun bool `arg:"-d,--dry-run" help:"Don't actually publish"` -} - -type GoogleOauthCmd struct { -} - -var Args struct { - // Subcommands - GoogleOauth *GoogleOauthCmd `arg:"subcommand:google-oauth" help:"Store Google OAuth refresh token"` - Publish *PublishCmd `arg:"subcommand:publish" help:"Publish to a destination"` - - // Google OAuth flags - ClientId string `arg:"--client-id, env:CLIENT_ID" help:"Google OAuth client ID"` - ClientSecret string `arg:"--I client-secret, env:CLIENT_SECRET" help:"Google OAuth client secret"` - RefreshToken string `arg:"--refresh-token, env:REFRESH_TOKEN" help:"Google OAuth refresh token" default:""` - - // Misc flags - LogLevel string `arg:"--log-level, env:LOG_LEVEL" help:"\"debug\", \"info\", \"warning\", \"error\", or \"fatal\"" default:"info"` - LogColor bool `arg:"--log-color, env:LOG_COLOR" help:"Force colored logs" default:"false"` -} diff --git a/cobra/cmd/platforms.go b/cmd/platforms.go similarity index 99% rename from cobra/cmd/platforms.go rename to cmd/platforms.go index 7230b8d..ef9379e 100644 --- a/cobra/cmd/platforms.go +++ b/cmd/platforms.go @@ -15,7 +15,7 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/charmbracelet/log" "github.com/go-resty/resty/v2" - "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" + "github.com/slashtechno/cross-blogger/pkg/oauth" "github.com/spf13/afero" "go.abhg.dev/goldmark/frontmatter" ) diff --git a/cobra/cmd/publish.go b/cmd/publish.go similarity index 100% rename from cobra/cmd/publish.go rename to cmd/publish.go diff --git a/cobra/cmd/root.go b/cmd/root.go similarity index 100% rename from cobra/cmd/root.go rename to cmd/root.go diff --git a/cobra/.gitignore b/cobra/.gitignore deleted file mode 100644 index 6b97628..0000000 --- a/cobra/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -config.toml -*_markdown/ \ No newline at end of file diff --git a/cobra/config.toml.example b/cobra/config.toml.example deleted file mode 100644 index 87244fc..0000000 --- a/cobra/config.toml.example +++ /dev/null @@ -1,5 +0,0 @@ -[[destinations]] -name = 'blogger' -blog_url = 'https://example.com' -type = 'blogger' - diff --git a/cobra/main.go b/cobra/main.go deleted file mode 100644 index b5db32d..0000000 --- a/cobra/main.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package main - -import ( - "io/fs" - - "github.com/charmbracelet/log" - "github.com/slashtechno/cross-blogger/cobra/cmd" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/subosito/gotenv" -) - -var cfgFile string - -func init() { - cobra.OnInitialize(initConfig) - cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.toml", "config file path") -} - -func initConfig() { - // Load a .env file if it exists - gotenv.Load() - // Tell Viper to use the prefix "CROSS_BLOGGER" for environment variables - viper.SetEnvPrefix("CROSS_BLOGGER") - // log.Debug(cfgFile) - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Use config.yaml in the current working directory. - viper.SetConfigFile("./config.toml") - } - - if err := viper.ReadInConfig(); err == nil { - log.Debug("", "config file:", viper.ConfigFileUsed()) - } else { - // If the config file is not found, create a file, write the default values and exit - // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError - if _, ok := err.(*fs.PathError); ok { - log.Debug("Config file not found, creating a new one") - viper.SetDefault("destinations", []map[string]interface{}{ - { - "name": "blogger", - "type": "blogger", - "blog_url": "https://example.com", - "overwrite": false, - }, - { - "name": "markdown1", - "type": "markdown", - "content_dir": "content", - "overwrite": false, - }, - }) - log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) - if err := viper.WriteConfigAs(cfgFile); err != nil { - log.Fatal("Failed to write config file:", err) - } - } else { - log.Fatal("Failed to read config file:", err) - } - } -} - -func main() { - log.SetLevel(log.DebugLevel) - cmd.Execute() -} diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..de67c60 --- /dev/null +++ b/config.toml.example @@ -0,0 +1,26 @@ +# type is a required field that specifies the type of the source or destination. +# name is the name of the source or destination. It is used to refer to the source or destination when running the command. +# overwrite is a boolean field that specifies whether to overwrite the file/post if it already exists. This is done by removing old files/posts that have the same title. +# blog_url is the URL of the blog +# content_dir is the directory where the markdown files are located +[[destinations]] +blog_url = 'https://example.com' +name = 'blog' +overwrite = false +type = 'blogger' + +[[destinations]] +content_dir = 'content' +name = 'otherblog' +overwrite = false +type = 'markdown' + +[[sources]] +blog_url = 'https://example.com' +name = 'someblog' +type = 'blogger' + +[[sources]] +content_dir = 'content' +name = 'aBlogInMarkdown' +type = 'markdown' diff --git a/main.go b/main.go index 9a8adc7..323ff3f 100644 --- a/main.go +++ b/main.go @@ -1,425 +1,83 @@ -// When running, use `go run .` package main import ( - "bufio" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" + "io/fs" - htmltomd "github.com/JohannesKaufmann/html-to-markdown" - "github.com/alexflint/go-arg" - mdlib "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/parser" - "github.com/imdario/mergo" - "github.com/joho/godotenv" - "github.com/sirupsen/logrus" - "github.com/skratchdot/open-golang/open" - "github.com/tidwall/gjson" + "github.com/charmbracelet/log" + "github.com/slashtechno/cross-blogger/cmd" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/subosito/gotenv" ) -func main() { - godotenv.Load(".env") - arg.MustParse(&Args) - - logrus.SetOutput(os.Stdout) - logrus.SetFormatter(&logrus.TextFormatter{PadLevelText: true, DisableQuote: true, ForceColors: Args.LogColor, DisableColors: !Args.LogColor}) - if Args.LogLevel == "debug" { - logrus.SetLevel(logrus.DebugLevel) - // Enable line numbers in debug logs - Doesn't help too much since a fatal error still needs to be debugged - logrus.SetReportCaller(true) - } else if Args.LogLevel == "info" { - logrus.SetLevel(logrus.InfoLevel) - } else if Args.LogLevel == "warning" { - logrus.SetLevel(logrus.WarnLevel) - } else if Args.LogLevel == "error" { - logrus.SetLevel(logrus.ErrorLevel) - } else if Args.LogLevel == "fatal" { - logrus.SetLevel(logrus.FatalLevel) - } else { - logrus.SetLevel(logrus.InfoLevel) - } +var cfgFile string - switch { - case Args.GoogleOauth != nil: - _, err := getAccessToken() - if err != nil { - logrus.Fatal(err) - } - case Args.Publish != nil: - var ( - title string - html string - markdown string - err error - ) - switch { - case Args.Publish.Blogger != nil: - title, html, markdown, err = getBloggerPost(Args.Publish.Blogger.BlogAddress, Args.Publish.Blogger.PostAddress) - if err != nil { - logrus.Fatal(err) - } - case Args.Publish.File != nil: - title, html, markdown, err = getFilePost(Args.Publish.File.Filepath, Args.Publish.Title) - if err != nil { - logrus.Fatal(err) - } - default: - // Could add an interactive mode here for user-friendliness - logrus.Fatal("No subcommand specified") - } - if Args.Publish.DryRun { - logrus.Debugf("Title: %s | HTML: %s | Markdown: %s", title, html, markdown) - } else { - err = publishPost(title, html, markdown, Args.Publish.Destinations) - } - if err != nil { - logrus.Fatal(err) - } - } +func init() { + cobra.OnInitialize(initConfig) + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.toml", "config file path") } -func publishPost(title string, html string, markdown string, destinations map[string]string) error { - for destination, destinationSpecifier := range destinations { - switch destination { - case "blogger": - // Get the blog ID - blogId, err := getBlogId(destinationSpecifier) - if err != nil { - return err - } - // Publish to Blogger - logrus.Info("Publishing to Blogger") - url := "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/" - payloadMap := map[string]interface{}{ - "kind": "blogger#post", - "blog": map[string]string{ - "id": blogId, - }, - "title": title, - "content": html, - } - accessToken, err := getAccessToken() - if err != nil { - return err - } - _, err = request(url, "POST", accessToken, payloadMap) - if err != nil { - return err - } - case "markdown": - logrus.Info("Publishing to Markdown") - cleanPathToFile := filepath.Clean(destinationSpecifier) - // Open the file in write-only mode (600) with append and create - file, err := os.OpenFile(cleanPathToFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) - if err != nil { - return err - } - // Write markdown to file - _, err = file.WriteString(markdown) - if err != nil { - return err - } - // Close the file - err = file.Close() - if err != nil { - return err - } - - } - } - return nil -} -func storeRefreshToken() (string, error) { // Rename to getRefreshToken(), perhaps? - err := checkNeededFlags(map[string]string{"clientId": Args.ClientId, "clientSecret": Args.ClientSecret}) - if err != nil { - return "", err - } - // Get the authorization code from the user - url := "https://accounts.google.com/o/oauth2/v2/auth?client_id=" + Args.ClientId + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fblogger&response_type=code&access_type=offline&prompt=consent" - // Open the URL in the default browser - err = open.Run(url) - fmt.Println("If the link didn't open, please manually go to the following link in your browser:") - // Print the URL - fmt.Printf("\n%v\n\n", url) - if err != nil { - return "", err - } - fmt.Println("Input the authorization code below") - authorizationCode, err := singleLineInput() - if err != nil { - return "", err - } - - // Get refresh token using the authorization code given by the user - url = "https://oauth2.googleapis.com/token?client_id=" + Args.ClientId + "&client_secret=" + Args.ClientSecret + "&code=" + authorizationCode + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&grant_type=authorization_code" - // Send a POST request to the URL with no authorization headers - resultBody, err := request(url, "POST", "", nil) - if err != nil { - return "", err - } - googleRefreshToken := gjson.Get(resultBody, "refresh_token").String() - logrus.Debugf("Refresh token: %s", googleRefreshToken) - // Merge the new environment variable with the existing environment variables using Mergo - env := map[string]string{"REFRESH_TOKEN": googleRefreshToken} - oldEnv, err := godotenv.Read() - if err != nil { - return "", err - } - err = mergo.Merge(&env, oldEnv) - // for key, value := range oldEnv { - // env[key] = value - // } - - if err != nil { - return "", err - } - - logrus.Debugf("New environment variables: %v | Old enviroment variables %v", env, oldEnv) - // May want to use filepath.Join() here - err = godotenv.Write(env, ".env") - if err != nil { - return "", err - } - return googleRefreshToken, nil -} - -func getAccessToken() (string, error) { - err := checkNeededFlags(map[string]string{"clientId": Args.ClientId, "clientSecret": Args.ClientSecret}) - if err != nil { - return "", err - } - var googleRefreshToken string - // Check if there is a refresh token present - if Args.RefreshToken == "" { - logrus.Print("No refresh token found. Please input the following information to get a refresh token.\n") - googleRefreshToken, err = storeRefreshToken() - if err != nil { - return "", err - } +func initConfig() { + // Load a .env file if it exists + gotenv.Load() + // Tell Viper to use the prefix "CROSS_BLOGGER" for environment variables + viper.SetEnvPrefix("CROSS_BLOGGER") + // log.Debug(cfgFile) + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) } else { - googleRefreshToken = Args.RefreshToken - - } - - // Get access token using the refresh token - url := "https://oauth2.googleapis.com/token?client_id=" + Args.ClientId + "&client_secret=" + Args.ClientSecret + "&refresh_token=" + googleRefreshToken + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&grant_type=refresh_token" - // Send a POST request to the URL with no authorization headers - resultBody, err := request(url, "POST", "", nil) - if err != nil { - return "", err - } - // Get the authorization token - accessToken := gjson.Get(resultBody, "access_token").String() - if accessToken == "" { - return "", errors.New("no access token found") - } else { - logrus.Debugf("Access token: %s", accessToken) - - } - return accessToken, nil -} - -func singleLineInput() (string, error) { - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - return "", err + // Use config.yaml in the current working directory. + viper.SetConfigFile("./config.toml") } - input = strings.TrimSpace(input) - // fmt.Print("\n") - return input, nil -} -func request(url string, requestType string, bearerAuth string, payloadMap map[string]interface{}) (string, error) { - // Send a request to the URL, with the URL which was passed to the function - var req *http.Request - var err error - // If payloadMap is nil, don't send a payload - if payloadMap == nil { - req, err = http.NewRequest(requestType, url, nil) - if err != nil { - return "", err - } + if err := viper.ReadInConfig(); err == nil { + log.Debug("", "config file:", viper.ConfigFileUsed()) } else { - // logrus.Debugf("Payload map: %v", payloadMap) - payloadBytes, err := json.Marshal(payloadMap) - if err != nil { - return "", err - } - payload := strings.NewReader(string(payloadBytes)) - req, err = http.NewRequest(requestType, url, payload) - if err != nil { - return "", err - } - } - - // If the bearerAuth parameter is true, set the Authorization header with an access token - if bearerAuth != "" { - req.Header.Add("Authorization", "Bearer "+bearerAuth) - } - // Make the actual request - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - // Convert the result body to a string and then return it - resultBodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return "", err - } - err = res.Body.Close() - if err != nil { - return "", err - } - // Debug the status code - // logrus.Debugf("Sending %s request to %s with payload %v, bearer authorization %s. Got status code %d", requestType, url, payloadMap, bearerAuth, res.StatusCode) - return string(resultBodyBytes), nil -} - -func getBlogId(blogAddress string) (string, error) { - // Send a GET request to the URL with bearer authorization - accessToken, err := getAccessToken() - logrus.Debugf("Blog address: %s", blogAddress) - url := "https://www.googleapis.com/blogger/v3/blogs/byurl?url=https%3A%2F%2F" + blogAddress - if err != nil { - return "", err - } - resultBody, err := request(url, "GET", accessToken, nil) - if err != nil { - return "", err - } - // logrus.Debugf("Result body: %s", resultBody) - // Get the blog ID - id := gjson.Get(resultBody, "id").String() - if id == "" { - return "", errors.New("no blog ID found") - } - return id, nil -} - -func getBloggerPost(blogAddress string, postAddress string) (string, string, string, error) { - path := strings.Replace(postAddress, blogAddress, "", 1) - blogID, err := getBlogId(blogAddress) - logrus.Debugf("Blog ID: %s | Path: %s", blogID, path) - if err != nil { - return "", "", "", err - } - accessToken, err := getAccessToken() - if err != nil { - return "", "", "", err - } - // https://www.googleapis.com/blogger/v3/blogs/[BLOG_ID]/posts/bypath?path=/{YEAR}/{MONTH}/{POST}.html - url := "https://www.googleapis.com/blogger/v3/blogs/" + blogID + "/posts/bypath?path=" + path - resultBody, err := request(url, "GET", accessToken, nil) - if err != nil { - return "", "", "", err - } - html := gjson.Get(resultBody, "content").String() - title := gjson.Get(resultBody, "title").String() - markdown, err := htmltomd.NewConverter("", true, nil).ConvertString(html) - if title == "" && html == "" && markdown == "" { - logrus.Debug(url) - return "", "", "", errors.New("no post found") - } - if err != nil { - return "", "", "", err - } - return title, html, markdown, nil -} - -func getFilePost(pathToFile string, defaultTitle string) (string, string, string, error) { - // If the file path is empty, ask for it (might be a good idea to remove this) - if pathToFile == "" { - fmt.Println("Enter the path to the file") - var err error - pathToFile, err = singleLineInput() - if err != nil { - return "", "", "", err - } - } - - cleanPathToFile := filepath.Clean(pathToFile) - - // Check if the file exists - _, err := os.Stat(cleanPathToFile) - if errors.Is(err, os.ErrNotExist) { - return "", "", "", errors.New("file does not exist") - } - - // Read the file - fileBytes, err := os.ReadFile(cleanPathToFile) - if err != nil { - return "", "", "", err - } - fileContent := string(fileBytes) - logrus.Debug("The file was read successfully") - var ( - title string - html string - markdown string - ) + // If the config file is not found, create a file, write the default values and exit + // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError + if _, ok := err.(*fs.PathError); ok { + log.Debug("Config file not found, creating a new one") + // Destinations + viper.SetDefault("destinations", []map[string]interface{}{ + { + "name": "blog", + "type": "blogger", + "blog_url": "https://example.com", + "overwrite": false, + }, + { + "name": "otherblog", + "type": "markdown", + "content_dir": "content", + "overwrite": false, + }, + }) + // Sources + viper.SetDefault("sources", []map[string]interface{}{ + { + "name": "someblog", + "type": "blogger", + "blog_url": "https://example.com", + }, + { + "name": "aBlogInMarkdown", + "type": "markdown", + "content_dir": "content", + }, + }) - // Check file extension - fileExtension := filepath.Ext(pathToFile) - switch fileExtension { - case ".html", ".htm": - // Set HTML to the file content - html = fileContent - // Convert to Markdown - markdown, err = htmltomd.NewConverter("", true, nil).ConvertString(fileContent) - if err != nil { - return "", "", "", err + if err := viper.WriteConfigAs(cfgFile); err != nil { + log.Fatal("Failed to write config file:", err) + } + log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) + } else { + log.Fatal("Failed to read config file:", err) } - - case ".md", ".markdown": - // Set Markdown to the file content - markdown = fileContent - // Convert to HTML - extensions := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(extensions) - html = string(mdlib.ToHTML([]byte(fileContent), parser, nil)) - - case ".txt": - // Not sure if plain text should be supported but it can easily be removed later - - // Set Markdown to the file content - markdown = fileContent - // Convert to HTML - extensions := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(extensions) - html = string(mdlib.ToHTML([]byte(fileContent), parser, nil)) - default: - return "", "", "", errors.New("file extension not supported") - } - if defaultTitle != "" { - title = defaultTitle - } else { - // Get the file name without the extension - fileName := filepath.Base(pathToFile) - fileNameWithoutExtension := strings.TrimSuffix(fileName, filepath.Ext(fileName)) - // Replace underscores with spaces (Might be a good idea to make this optional) - title = strings.ReplaceAll(fileNameWithoutExtension, "_", " ") } - return title, html, markdown, nil } -func checkNeededFlags(flags map[string]string) error { - message := "The following must be set" - for name, value := range flags { - if value == "" { - message += "\n- " + name - } - if message != "The following must be set" { - return errors.New(message) - } - } - return nil +func main() { + log.SetLevel(log.DebugLevel) + cmd.Execute() } - -// Check if the file exists -> Check file extension -> Convert to the other formats -> Check if user-defined title is set -> If user-defined title is not set, use the file name as the title -> Return the title, HTML, and Markdown and hopefully no errors. diff --git a/cobra/pkg/oauth/google.go b/pkg/oauth/google.go similarity index 100% rename from cobra/pkg/oauth/google.go rename to pkg/oauth/google.go