commit 3736c10d58c33dc038ed7e7e6dca6df1b35d0255 Author: Shin'ya Minazuki Date: Sat Jan 3 11:31:22 2026 -0300 Yuki N. > COMMIT; Signed-off-by: Shin'ya Minazuki 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) +}