Death and rebirth

Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
This commit is contained in:
2026-01-21 09:51:36 -03:00
parent 3736c10d58
commit ac17186cd4
12 changed files with 284 additions and 145 deletions

View File

@@ -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

View File

@@ -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

86
auth.go
View File

@@ -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")
}

View File

@@ -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()

52
delete.go Normal file
View File

@@ -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")
}

10
go.mod
View File

@@ -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
)

16
go.sum
View File

@@ -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=

72
login.go Normal file
View File

@@ -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")
}

36
logout.go Normal file
View File

@@ -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")
}

View File

@@ -1,5 +1,7 @@
// This file is part of Yuki
package main
// Offload all processing to Cobra
func main() {
Execute()
}

116
post.go
View File

@@ -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(), &note)
if err != nil {
log.Fatalf("%#v\n", err)
}
}

View File

@@ -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() {