From 3736c10d58c33dc038ed7e7e6dca6df1b35d0255 Mon Sep 17 00:00:00 2001 From: Shin'ya Minazuki Date: Sat, 3 Jan 2026 11:31:22 -0300 Subject: [PATCH] Yuki N. > COMMIT; Signed-off-by: Shin'ya Minazuki --- .gitignore | 1 + COPYING | 15 +++++++++ README.md | 3 ++ Taskfile.yml | 30 ++++++++++++++++++ auth.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ config.go | 38 +++++++++++++++++++++++ go.mod | 19 ++++++++++++ go.sum | 30 ++++++++++++++++++ main.go | 5 +++ post.go | 66 ++++++++++++++++++++++++++++++++++++++++ root.go | 18 +++++++++++ version.go | 17 +++++++++++ 12 files changed, 328 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 README.md create mode 100644 Taskfile.yml create mode 100644 auth.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 post.go create mode 100644 root.go create mode 100644 version.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d894b17 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/yuki diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..9b33981 --- /dev/null +++ b/COPYING @@ -0,0 +1,15 @@ +ISC License (ISCL) + +Copyright (C) 2026 Shin'ya Minazuki + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ce6cf5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Yuki (有希) +An incomplete client for ActivityPub, which shouldn't even be paid attention to. + diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..74c84af --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,30 @@ +# https://taskfile.dev + +version: '3' + +env: + GO: go + GOTELEMETRY: off + +vars: + IMPORT: git.laidback.moe/shinyoukai/yuki + VERSION: 0.0.0 +tasks: + default: + cmds: + - task: build + build: + desc: Build the interface + cmds: + - $GO build -v -ldflags='-w -X {{.IMPORT}}.Version=$VERSION' -buildvcs=false -o yuki + silent: true + 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 new file mode 100644 index 0000000..09a097c --- /dev/null +++ b/auth.go @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000..c82db35 --- /dev/null +++ b/config.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "os" + "gopkg.in/ini.v1" +) + +var Config struct { + ClientID string + ClientSecret string + Host string + Token string +} + +func ConfInit() { + config, err := os.UserConfigDir() + if err != nil { + log.Println("Unable to obtain user's configuration directory") + log.Fatal(err) + } + configPath := config + "/yuki.ini" + Parse(configPath) +} + +func Parse(file string) error { + cfg, err := ini.Load(file) + if err != nil { + 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() + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..335cd2f --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.laidback.moe/shinyoukai/yuki + +go 1.25.2 + +require ( + github.com/mattn/go-mastodon v0.0.10 + github.com/spf13/cobra v1.10.2 + github.com/tj/go-editor v1.0.0 + gopkg.in/ini.v1 v1.67.0 +) + +require ( + github.com/gorilla/websocket v1.5.3 // 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f1e67cd --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +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= +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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +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= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..736ef31 --- /dev/null +++ b/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + Execute() +} diff --git a/post.go b/post.go new file mode 100644 index 0000000..91964a5 --- /dev/null +++ b/post.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "log" + + "github.com/mattn/go-mastodon" + "github.com/spf13/cobra" + "github.com/tj/go-editor" +) + +var ( + visibility string +) + +var postCmd = &cobra.Command{ + Use: "post", + Short: "Send a message", + Run: func(_ *cobra.Command, args []string) { + doPost(args) + }, +} + +func init() { + ConfInit() + rootCmd.AddCommand(postCmd) + + postCmd.Flags().StringVarP(&visibility, "scope", "s", "public", "Visibility") +} + +func doPost(args []string) { + var text string + + data, err := editor.Read() + if err != nil { + log.Fatal(err) + } + + text = string(data) + + 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 new file mode 100644 index 0000000..6e3c8b7 --- /dev/null +++ b/root.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "yuki", + Short: "An extraterrestial (but limited) activitypub client", +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + log.Fatal(err) + } +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..6b53525 --- /dev/null +++ b/version.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" +) + +var ( + Revision = "0" + Version = "0" +) + +func FullVersion() string { + return fmt.Sprintf("%s (r%s)", Version, Revision) +} +func PrintVersion() string { + return fmt.Sprintf("%s", Version) +}