7 Commits

Author SHA1 Message Date
613576f438 Reorganize the project layout, fix some logging quirks
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 19:13:42 -03:00
a2a93adfb6 Support follow/unfollow actions
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 14:31:48 -03:00
3b06d6b773 Yarn already implies where it comes from
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 13:53:55 -03:00
f045e36119 Switch to Cobra
Also:
- Fixed the Makefile
- Added a Taskfile.yml (see: https://taskfile.dev)
- Updated the manual page

Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 12:11:34 -03:00
60688d7a0e Do not repeat code whenever possible
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 07:53:26 -03:00
811028bebc Update roadmap
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 00:39:20 -03:00
57a2914fe3 Fix token retrieval
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2025-12-30 00:37:25 -03:00
18 changed files with 334 additions and 150 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
/mikuru-*
/mikuru

View File

@@ -1,13 +1,10 @@
VERSION = `git describe --tags` || echo MIRAI
GO = go
GOFLAGS = -v -buildvcs=false -buildmode=exe -ldflags "-w -X `${GO} list`.Version=${VERSION}"
GOFLAGS = -v -buildvcs=false -buildmode=exe -ldflags='-w -X "`${GO} list`.Version=${VERSION}" -X "`${GO} list`.Revision=${REVISION}"'
build: mikuru-login mikuru-post
VERSION ?= `git describe --tags`
REVISION ?= `git rev-list --all | wc -l`
mikuru-login:
${GO} build ${GOFLAGS} ./cmd/$@
mikuru-post:
${GO} build ${GOFLAGS} ./cmd/$@
build:
@${GO} build ${GOFLAGS}
clean:
rm -f mikuru-*
@rm -f mikuru

View File

@@ -1,7 +1,8 @@
# Mikuru
[Yarn.social](https://yarn.social) client where each task (except for logout) is handled by a separate program.
A [yarn.social](https://yarn.social) client from the future
## Current status
* [X] Follow/Unfollow
* [X] Login
* [X] Posting
* [ ] Timeline

30
Taskfile.yml Normal file
View File

@@ -0,0 +1,30 @@
# https://taskfile.dev
version: '3'
env:
GO: go
vars:
IMPORT: git.laidback.moe/shinyoukai/mikuru/mirai
tasks:
default:
cmds:
- task: build
build:
desc: Build the client
cmds:
- $GO build -ldflags='-s -w -X "{{.IMPORT}}.Version={{.VERSION}}" -X "{{.IMPORT}}.Revision={{.REVISION}}"' -v
vars:
REVISION:
sh: git rev-list --all | wc -l | tr -d ' '
VERSION:
sh: git describe --tags
clean:
desc: Remove generated files
cmds:
- rm -f mikuru
tidy:
desc: Update go.mod
cmds:
- $GO mod tidy

View File

@@ -1,67 +0,0 @@
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/term"
"go.yarn.social/client"
"git.laidback.moe/shinyoukai/mikuru"
)
var (
username string
password string
)
func init() {
xdg_config_home, err := os.UserConfigDir()
if err != nil {
fmt.Println("Unable to obtain user's configuration directory")
os.Exit(1)
}
configPath := xdg_config_home + "/mikuru.ini"
mikuru.Parse(configPath)
}
func main() {
cli, err := client.NewClient(client.WithURI(mikuru.Config.Host))
if err != nil {
fmt.Printf("%s", err)
fmt.Println("Error creating client")
os.Exit(1)
}
signin(cli)
}
func signin(cli *client.Client) {
fmt.Printf("Username: ")
fmt.Scanln(&username)
if len(username) == 0 {
fmt.Println("No value. Bailing out")
os.Exit(1)
}
fmt.Printf("Password: ")
data, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Printf("Unable to obtain password: %s\n", err)
os.Exit(1)
}
password := string(data)
res, err := cli.Login(username, password)
if err != nil {
fmt.Println("Unable to login")
os.Exit(1)
}
fmt.Println("Login successful")
fmt.Println("Place this token in your configuration file for later use")
fmt.Println("Do not share it with anyone, it's classified information")
fmt.Println("Trim surrounding braces before usage")
fmt.Printf("token = %v\n", res)
}

View File

@@ -1,50 +0,0 @@
package main
import (
"log"
"os"
"git.laidback.moe/shinyoukai/mikuru"
"go.yarn.social/client"
"github.com/tj/go-editor"
)
func init() {
config, err := os.UserConfigDir()
if err != nil {
log.Fatalf("%s\n")
}
configPath := config + "/mikuru.ini"
mikuru.Parse(configPath)
}
func main() {
cli, err := client.NewClient(
client.WithURI(mikuru.Config.Host),
client.WithToken(mikuru.Config.Token),
)
if err != nil {
log.Fatal(err)
}
write(cli)
}
func write(cli *client.Client) {
var post string
data, err := editor.Read()
if err != nil {
log.Fatal("Unable to read content from editor")
}
post = string(data)
_, err = cli.Post(post, "")
if err != nil {
log.Fatal("Unable to publish tweet")
}
}

45
follow.go Normal file
View File

@@ -0,0 +1,45 @@
package main
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.laidback.moe/shinyoukai/mikuru/mirai"
"go.yarn.social/client"
)
var followCmd = &cobra.Command{
Use: "follow <NICK> <URL>",
Short: "Track a twtxt.txt feed, located in a Yarn pod or otherwise",
Run: func(_ *cobra.Command, args []string) {
cli, err := client.NewClient(
client.WithURI(mirai.Config.Host),
client.WithToken(mirai.Config.Token),
)
if err != nil {
log.Fatal(err)
}
if len(args) != 2 {
log.Fatal("Not enough arguments")
}
nick := args[0]
url := args[1]
observe(cli, nick, url)
if err != nil {
log.Fatalf("Could not follow %s at %s\n", nick, url)
}
},
}
func init() {
mirai.ConfInit()
rootCmd.AddCommand(followCmd)
}
func observe(cli *client.Client, nick, url string) error {
err := cli.Follow(nick, url)
if err != nil {
return err
}
return nil
}

5
go.mod
View File

@@ -3,6 +3,8 @@ module git.laidback.moe/shinyoukai/mikuru
go 1.24.0
require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.2
github.com/tj/go-editor v1.0.0
go.yarn.social/client v0.0.0-20250420114029-410ad71a453e
golang.org/x/term v0.38.0
@@ -10,8 +12,9 @@ require (
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.yarn.social/types v0.0.0-20250420113154-c5a6df6d2f22 // indirect
golang.org/x/sys v0.39.0 // indirect

9
go.sum
View File

@@ -1,6 +1,9 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -11,14 +14,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yarn.social/client v0.0.0-20250420114029-410ad71a453e h1:A4wok2/cSAbvWLjHhtCrW8dvv1XlgcGRj7H15150HhA=
go.yarn.social/client v0.0.0-20250420114029-410ad71a453e/go.mod h1:+VjffjmcZMHIudGsZlnbPnavk0oSK6xf4olzExiojhg=
go.yarn.social/types v0.0.0-20250420113154-c5a6df6d2f22 h1:M34aQ3AaRKE2t7JOLwoemJ65r0e8c1z4TwuHzogHvZE=

70
login.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"fmt"
"strings"
"syscall"
"golang.org/x/term"
"go.yarn.social/client"
"git.laidback.moe/shinyoukai/mikuru/mirai"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
username string
password string
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate against a Yarn pod",
Aliases: []string{"auth", "signin"},
Args: cobra.MaximumNArgs(0),
Run: func(_ *cobra.Command, _ []string) {
cli, err := client.NewClient(client.WithURI(mirai.Config.Host))
if err != nil {
log.Fatalf("Unable to create client\n%s\n", err)
}
signin(cli)
},
}
func init() {
mirai.ConfInit()
rootCmd.AddCommand(loginCmd)
}
func signin(cli *client.Client) {
fmt.Printf("Username: ")
fmt.Scanln(&username)
if len(username) == 0 {
log.Fatal("No value. Bailing out")
}
fmt.Printf("Password: ")
data, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
log.Fatalf("Unable to obtain password\n%s\n", err)
}
password := string(data)
res, err := cli.Login(username, password)
if err != nil {
log.Fatalf("Unable to login\n%s\n", err)
}
token := strings.Trim(fmt.Sprintf(res.Token), "{}")
fmt.Println("Login successful")
fmt.Println("Place this token in your configuration file for later use")
fmt.Println("Do not share it with anyone, it's classified information!")
fmt.Println("")
fmt.Printf("token = %v\n", token)
}

5
main.go Normal file
View File

@@ -0,0 +1,5 @@
package main
func main() {
Execute()
}

View File

@@ -3,15 +3,28 @@
.Os
.Sh NAME
.Nm mikuru
.Nd A yarn-compatible twtxt client from the future
.Nd A yarn.social client from the future
.Sh SYNOPSIS
.Nm
.Op Cm login
.Op Cm post
.Sh DESCRIPTION
This client has every single part of its functionality
handled by a separate program.
.Sh SEE ALSO
.Xr mikuru-login 1
.Xr mikuru-post 1
.Xr mikuru-timeline 1
This program was made by reverse engineering the reference client
due to lack of known API documentation for the client library it uses.
.Pp
It doesn't share any code with it, however.
.Sh USAGE
.Bl -tag -width 11n
.It login
Authenticate with a Yarn.social pod,
it will ask you for your credentials
(passwords are not echoed)
.It post
Publish a tweet to a Yarn.social pod,
it will open your text editor (usually $EDITOR),
and will fail if the variable is unset.
.El
.Sh FILES
.Pa ~/.config/mikuru.ini
.Sh AUTHORS
.An Shin'ya Minazuki Aq Mt shinyoukai@laidback.moe

View File

@@ -1,6 +1,8 @@
package mikuru
package mirai
import (
"log"
"os"
"gopkg.in/ini.v1"
)
@@ -9,6 +11,16 @@ var Config struct {
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 + "/mikuru.ini"
Parse(configPath)
}
func Parse(file string) error {
cfg, err := ini.Load(file)
if err != nil {

17
mirai/version.go Normal file
View File

@@ -0,0 +1,17 @@
package mirai
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)
}

48
post.go Normal file
View File

@@ -0,0 +1,48 @@
package main
import (
"git.laidback.moe/shinyoukai/mikuru/mirai"
"go.yarn.social/client"
"github.com/tj/go-editor"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var postCmd = &cobra.Command{
Use: "post",
Aliases: []string{"tweet"},
Short: "Publish a new post to a Yarn pod",
Run: func(_ *cobra.Command, args []string) {
cli, err := client.NewClient(
client.WithURI(mirai.Config.Host),
client.WithToken(mirai.Config.Token),
)
if err != nil {
log.Fatalf("Unable to create client\n%s\n", err)
}
write(cli)
},
}
func init() {
rootCmd.AddCommand(postCmd)
mirai.ConfInit()
}
func write(cli *client.Client) {
var post string
data, err := editor.Read()
if err != nil {
log.Fatalf("Unable to read content from editor\n%s\n", err)
}
post = string(data)
_, err = cli.Post(post, "")
if err != nil {
log.Fatalf("Unable to publish tweet\n%s\n", err)
}
}

20
root.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.laidback.moe/shinyoukai/mikuru/mirai"
)
var rootCmd = &cobra.Command{
Use: "mikuru",
Short: "A client for Yarn.social from the future",
Version: mirai.FullVersion(),
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
log.Fatal(err)
}
}

44
unfollow.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.laidback.moe/shinyoukai/mikuru/mirai"
"go.yarn.social/client"
)
var unfollowCmd = &cobra.Command{
Use: "unfollow <NICK>",
Short: "Cease to track a feed",
Run: func(_ *cobra.Command, args []string) {
cli, err := client.NewClient(
client.WithURI(mirai.Config.Host),
client.WithToken(mirai.Config.Token),
)
if err != nil {
log.Fatal(err)
}
if len(args) != 1 {
log.Fatal("Not enough arguments")
}
nick := args[0]
leave_alone(cli, nick)
if err != nil {
log.Fatalf("Could not unfollow %s\n", nick)
}
},
}
func init() {
mirai.ConfInit()
rootCmd.AddCommand(unfollowCmd)
}
func leave_alone(cli *client.Client, nick string) error {
err := cli.Unfollow(nick)
if err != nil {
return err
}
return nil
}

View File

@@ -1,13 +0,0 @@
package mikuru
import (
"fmt"
)
var (
Version = "0"
)
func PrintVersion() string {
return fmt.Sprintf("%s", Version)
}