diff --git a/README.md b/README.md index 4ce6cf5..d5adf9c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # Yuki (有希) -An incomplete client for ActivityPub, which shouldn't even be paid attention to. +An alternative client for [WriteFreely](https://writefreely.org) +## Features +* [X] Delete +* [ ] Edit +* [X] Login +* [X] Logout +* [X] Posting + * [X] Drafts + * [X] Font selection + * [X] Language selection + * [X] Syntax highlighting + * [X] Publish to collection + * [X] Right-to-left writing mode +* [X] Logout +* [ ] Update + +## Usage +TBD diff --git a/Taskfile.yml b/Taskfile.yml index 74c84af..8977f57 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,8 +7,9 @@ env: GOTELEMETRY: off vars: - IMPORT: git.laidback.moe/shinyoukai/yuki - VERSION: 0.0.0 + IMPORT: code.laidback.moe/yuki + VERSION: 2026.01.21 + tasks: default: cmds: @@ -16,15 +17,12 @@ tasks: build: desc: Build the interface cmds: - - $GO build -v -ldflags='-w -X {{.IMPORT}}.Version=$VERSION' -buildvcs=false -o yuki - silent: true + - $GO build -v -ldflags='-w -X {{.IMPORT}}.Version={{.VERSION}}' -buildvcs=false -o yuki clean: desc: Remove generated files cmds: - rm -f yuki - silent: true tidy: desc: Update go.mod cmds: - $GO mod tidy - silent: true diff --git a/auth.go b/auth.go deleted file mode 100644 index 09a097c..0000000 --- a/auth.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/url" - "os" - - "github.com/mattn/go-mastodon" - "github.com/spf13/cobra" -) - -var authCmd = &cobra.Command{ - Use: "auth", - Short: "Authenticate to a Mastodon/Pleroma instance", - Run: func(_ *cobra.Command, args []string) { - doLogin() - }, -} - -func init() { - ConfInit() - rootCmd.AddCommand(authCmd) -} - -func doLogin() { - client := &mastodon.AppConfig{ - Server: Config.Host, - ClientName: "Yuki", - Scopes: "read write follow", - Website: "https://projects.laidback.moe/yuki/", - RedirectURIs: "urn:ietf:wg:oauth:2.0:oob", - } - - app, err := mastodon.RegisterApp(context.Background(), client) - if err != nil { - log.Fatal(err) - } - - u, err := url.Parse(app.AuthURI) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Go to \n%s\n and copy/paste the given auth code\n", u) - var authCode string - fmt.Print("Paste the code here:") - fmt.Scanln(&authCode) - - conf := &mastodon.Config{ - Server: Config.Host, - ClientID: app.ClientID, - ClientSecret: app.ClientSecret, - } - - c := mastodon.NewClient(conf) - - err = c.GetUserAccessToken(context.Background(), authCode, app.RedirectURI) - - if err != nil { - log.Fatal(err) - } - - config, err := os.UserConfigDir() - if err != nil { - log.Println("Unable to obtain user's configuration directory") - log.Fatal(err) - } - configPath := config + "/yuki.ini" - - f, err := os.OpenFile(configPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - log.Fatal(err) - } - - defer f.Close() - - client_id := c.Config.ClientID - client_secret := c.Config.ClientSecret - access_token := c.Config.AccessToken - - f.WriteString("client_id = " + client_id + "\n") - f.WriteString("client_secret = " + client_secret + "\n") - f.WriteString("token = " + access_token + "\n") -} diff --git a/config.go b/config.go index c82db35..86be110 100644 --- a/config.go +++ b/config.go @@ -7,12 +7,14 @@ import ( ) var Config struct { - ClientID string - ClientSecret string Host string Token string } +var ( + ConfigPath string +) + func ConfInit() { config, err := os.UserConfigDir() if err != nil { @@ -29,8 +31,6 @@ func Parse(file string) error { return err } - Config.ClientID = cfg.Section("yuki").Key("client_id").String() - Config.ClientSecret = cfg.Section("yuki").Key("client_secret").String() Config.Host = cfg.Section("yuki").Key("host").String() Config.Token = cfg.Section("yuki").Key("token").String() diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..c94104c --- /dev/null +++ b/delete.go @@ -0,0 +1,52 @@ +package main + +import ( + "log" + + "code.laidback.moe/go-writefreely" + "github.com/spf13/cobra" +) + +var ( + id string +) + +var deleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"rm"}, + Short: "Permanently delete a published post", + Run: func(cmd *cobra.Command, args []string) { + id, err := cmd.Flags().GetString("id") + if err != nil { + log.Fatal("Unable to get id flag") + } + DoDelete(id, args) + }, +} + +func init() { + ConfInit() + rootCmd.AddCommand(deleteCmd) + deleteCmd.Flags().StringVar(&id, "id", "", "Post ID") +} + +func DoDelete(id string, args []string) { + writefreely.InstanceURL = Config.Host + + if len(id) == 0 { + log.Fatal("No ID has been provided!") + } + + c := writefreely.NewClient() + c.SetToken(Config.Token) + + err := c.DeletePost(&writefreely.PostParams{ + ID: id, + }) + + if err != nil { + log.Fatal(err) + } + + log.Println("Post has been deleted") +} diff --git a/go.mod b/go.mod index 335cd2f..ad08ed4 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,21 @@ -module git.laidback.moe/shinyoukai/yuki +module code.laidback.moe/yuki go 1.25.2 require ( - github.com/mattn/go-mastodon v0.0.10 + code.laidback.moe/go-writefreely v1.3.1 github.com/spf13/cobra v1.10.2 github.com/tj/go-editor v1.0.0 + golang.org/x/term v0.39.0 gopkg.in/ini.v1 v1.67.0 ) require ( - github.com/gorilla/websocket v1.5.3 // indirect + code.as/core/socks v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect + github.com/writeas/impart v1.1.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index f1e67cd..c697921 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ +code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= +code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= +code.laidback.moe/go-writefreely v1.3.1 h1:Gl8AYswfjx+yITYYwp79gPmmtEUn9Vjyr1Kr1EjqafM= +code.laidback.moe/go-writefreely v1.3.1/go.mod h1:wt1ofi/PFoZ/rFFY08wfGRf8WROEpFxc1ueMgzMlrL8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/mattn/go-mastodon v0.0.10 h1:wz1d/aCkJOIkz46iv4eAqXHVreUMxydY1xBWrPBdDeE= -github.com/mattn/go-mastodon v0.0.10/go.mod h1:YBofeqh7G6s787787NQR8erBYz6fKDu+KNMrn5RuD6Y= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -20,9 +20,13 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tj/go-editor v1.0.0 h1:VduwRZWk5gxHbzRkS4buB9BIFzAf6Jc+vd2bLZpHzSw= github.com/tj/go-editor v1.0.0/go.mod h1:Jj9Ze2Rn2yj5DmMppydZqr9LbkIcCWrehzZ+7udOT+w= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= +github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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= diff --git a/login.go b/login.go new file mode 100644 index 0000000..c89eba8 --- /dev/null +++ b/login.go @@ -0,0 +1,72 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "syscall" + + "code.laidback.moe/go-writefreely" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to a WriteFreely instance", + Run: func(cmd *cobra.Command, args []string) { + DoLogin(args) + }, +} + +func init() { + ConfInit() + rootCmd.AddCommand(loginCmd) +} + +// DoLogin creates a new client, passing the host defined +// in the configuration file as writefreely.InstanceURL, +// and authenticates to the instance. +func DoLogin(args []string) { + var user, pass string + writefreely.InstanceURL = Config.Host + + c := writefreely.NewClient() + + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Username: ") + user, err := reader.ReadString('\n') + if err != nil { + log.Fatal(err) + } + + fmt.Print("Password: ") + data, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + log.Fatal(err) + } + pass = string(data) + + u, err := c.Login(user, pass) + if err != nil { + log.Fatal(err) + } + + cfg, err := os.UserConfigDir() + if err != nil { + log.Fatal(err) + } + cfgPath := cfg + "/yuki.ini" + + f, err := os.OpenFile(cfgPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + f.WriteString("token = " + u.AccessToken + "\n") + + log.Println("Login successful") +} diff --git a/logout.go b/logout.go new file mode 100644 index 0000000..62b1a74 --- /dev/null +++ b/logout.go @@ -0,0 +1,36 @@ +package main + +import ( + "log" + + "code.laidback.moe/go-writefreely" + "github.com/spf13/cobra" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Log out from WriteFreely, invalidating the token", + Run: func(_ *cobra.Command, args []string) { + DoLogout(args) + }, +} + +func init() { + ConfInit() + rootCmd.AddCommand(logoutCmd) +} + +func DoLogout(args []string) { + writefreely.InstanceURL = Config.Host + + c := writefreely.NewClient() + c.SetToken(Config.Token) + + err := c.Logout() + if err != nil { + log.Fatal(err) + } + + log.Println("Logged out") + log.Println("Remove the token from the configuration file") +} diff --git a/main.go b/main.go index 736ef31..e832b1a 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,7 @@ +// This file is part of Yuki package main +// Offload all processing to Cobra func main() { Execute() } diff --git a/post.go b/post.go index 91964a5..04ef071 100644 --- a/post.go +++ b/post.go @@ -1,66 +1,108 @@ package main import ( - "context" "log" - "github.com/mattn/go-mastodon" + "code.laidback.moe/go-writefreely" "github.com/spf13/cobra" "github.com/tj/go-editor" ) var ( - visibility string + collection string + font string + lang string + rtl bool + title string ) var postCmd = &cobra.Command{ - Use: "post", - Short: "Send a message", - Run: func(_ *cobra.Command, args []string) { - doPost(args) + Use: "post", + Short: "Create a new post on the WriteFreely instance", + Aliases: []string{"new"}, + Run: func(cmd *cobra.Command, args []string) { + collection, err := cmd.Flags().GetString("collection") + if err != nil { + log.Fatal("Couldn't get the collection flag") + } + font, err := cmd.Flags().GetString("font") + if err != nil { + log.Fatal("Couldn't get the font flag") + } + lang, err := cmd.Flags().GetString("lang") + if err != nil { + log.Fatal("Couldn't get the language flag") + } + rtl, err := cmd.Flags().GetBool("rtl") + if err != nil { + log.Fatal("Couldn't get the right-to-left flag") + } + title, err := cmd.Flags().GetString("title") + if err != nil { + log.Fatal("Couldn't get the title flag") + } + + DoPost(collection, font, lang, rtl, title, args) }, } func init() { ConfInit() rootCmd.AddCommand(postCmd) - - postCmd.Flags().StringVarP(&visibility, "scope", "s", "public", "Visibility") + postCmd.Flags().StringVarP(&collection, "collection", "b", "", "Location for the post (default is Drafts if unset)") + postCmd.Flags().StringVarP(&font, "font", "f", "mono", "Which font to use for presentation") + postCmd.Flags().StringVarP(&lang, "lang", "l", "en", "Post language") + postCmd.Flags().BoolVarP(&rtl, "rtl", "r", false, "Whether to use right-to-left writing style (common in Middle Eastern languages)") + postCmd.Flags().StringVarP(&title, "title", "t", "", "Title for the entry") } -func doPost(args []string) { - var text string +func DoPost(collection, font, lang string, rtl bool, title string, args []string) { + writefreely.InstanceURL = Config.Host + var err error + var w *writefreely.Post + + if rtl { + log.Println("Using right-to-left writing style") + } + if len(title) == 0 { + log.Fatal("No title specified!") + } data, err := editor.Read() + + if err != nil { + log.Fatal("Unable to read content from editor") + } + + post := string(data) + + c := writefreely.NewClient() + c.SetToken(Config.Token) + + if len(collection) == 0 { + w, err = c.CreatePost(&writefreely.PostParams{ + Title: title, + Content: post, + Font: font, + Language: &lang, + IsRTL: &rtl, + }) + } else { + w, err = c.CreatePost(&writefreely.PostParams{ + Title: title, + Content: post, + Font: font, + Collection: collection, + Language: &lang, + IsRTL: &rtl, + }) + } + if err != nil { log.Fatal(err) } - text = string(data) + log.Println("Post created") + log.Printf("%s/%s\n", Config.Host, w.ID) - conf := &mastodon.Config{ - Server: Config.Host, - ClientID: Config.ClientID, - ClientSecret: Config.ClientSecret, - AccessToken: Config.Token, - } - - c := mastodon.NewClient(conf) - - _, err = c.GetAccountCurrentUser(context.Background()) - if err != nil { - log.Fatal(err) - } - - - note := mastodon.Toot{ - Status: text, - Visibility: visibility, - } - - _, err = c.PostStatus(context.Background(), ¬e) - - if err != nil { - log.Fatalf("%#v\n", err) - } } diff --git a/root.go b/root.go index 6e3c8b7..dfd3417 100644 --- a/root.go +++ b/root.go @@ -7,7 +7,7 @@ import ( var rootCmd = &cobra.Command{ Use: "yuki", - Short: "An extraterrestial (but limited) activitypub client", + Short: "An extraterrestial client for WriteFreely", } func Execute() {