Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
feat: クリップボード内の画像を添付 (#96)
Browse files Browse the repository at this point in the history
* feat(app/cmd_tweet): ヘルプの文言を調整

* feat: クリップボード内の画像を添付 #95

* feat(log): CLIでのエラー出力の形式を変更

* docs(commands): `--clipboard` の説明を追記

* docs(CHANGELOG): 更新
  • Loading branch information
arrow2nd authored Oct 28, 2022
1 parent a612265 commit fd74c9e
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 52 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

## [Unreleased]

## [v2.0.8] - 2022-10-27
## [v2.0.8] - 2022-10-28

### Added

- ピン止めツイートの投票・引用元を取得する
- クリップボード内の画像を添付してツイート

### Changed

- 末尾が `_color` の色設定でも W3C の色名が使えるよう変更
- CLI モードでのエラー出力表示形式を変更
- ツイート完了時に添付された画像枚数を表示するよう変更

### Fixed

Expand Down
11 changes: 7 additions & 4 deletions api/media.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -29,8 +30,10 @@ type UploadImageResponse struct {
}

// UploadImage : 画像をアップロード
func (a *API) UploadImage(base64Image string) (*UploadImageResponse, error) {
func (a *API) UploadImage(rawImage []byte) (*UploadImageResponse, error) {
v := url.Values{}

base64Image := base64.StdEncoding.EncodeToString(rawImage)
v.Add("media_data", base64Image)

res, err := a.client.Client.PostForm(mediaUploadEndpoint, v)
Expand All @@ -45,10 +48,10 @@ func (a *API) UploadImage(base64Image string) (*UploadImageResponse, error) {
}

decoder := json.NewDecoder(res.Body)
raw := &UploadImageResponse{}
if err := decoder.Decode(raw); err != nil {
rawRes := &UploadImageResponse{}
if err := decoder.Decode(rawRes); err != nil {
return nil, fmt.Errorf("upload image decode error: %w", err)
}

return raw, nil
return rawRes, nil
}
108 changes: 69 additions & 39 deletions app/cmd_tweet.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package app

import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"syscall"

"github.com/arrow2nd/nekome/v2/cli"
"github.com/skanehira/clipboard-image/v2"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
"golang.org/x/term"
Expand All @@ -21,12 +23,9 @@ func (a *App) newTweetCmd() *cli.Command {
longHelp := `Post a tweet.
If the tweet statement is omitted, the internal editor is invoked if from the TUI, or the external editor if from the CLI.
Tips: If 'feature.use_external_editor' in preferences.toml is true, an external editor will be launched even from the TUI.
Also, setting 'feature.use_external_editor' to true in preferences.toml will launch the external editor even from the TUI.`

When specifying multiple images, please separate them with commas.
You may attach up to four images at a time.`

example := `tweet にゃーん --image cute_cat.png,very_cute_cat.png
example := `tweet にゃーん --image cat.png,dog.png
echo "にゃーん" | nekome tweet`

return &cli.Command{
Expand All @@ -37,10 +36,11 @@ You may attach up to four images at a time.`
UsageArgs: "[text]",
Example: example,
SetFlag: func(f *pflag.FlagSet) {
f.StringP("quote", "q", "", "specify the ID of the tweet to quote")
f.StringP("reply", "r", "", "specify the ID of the tweet to which you are replying")
f.StringP("editor", "e", os.Getenv("EDITOR"), "specify which editor to use (default is $EDITOR)")
f.StringSliceP("image", "i", nil, "specify the image to attach (if there is more than one comma separated)")
f.StringP("quote", "q", "", "quotes the tweet with the specified ID")
f.StringP("reply", "r", "", "send a reply to the tweet with the specified ID")
f.StringP("editor", "e", os.Getenv("EDITOR"), "specify the editor to use for editing")
f.StringSliceP("image", "i", nil, "attach the image (if there is more than one comma separated)")
f.BoolP("clipboard", "c", false, "attach the image in the clipboard (if the --image is specified, it takes precedence)")
},
Run: a.execTweetCmd,
}
Expand All @@ -50,28 +50,26 @@ func (a *App) execTweetCmd(c *cli.Command, f *pflag.FlagSet) error {
pref := shared.conf.Pref
text := ""

// 標準入力を受け取る
if f.NArg() == 0 && !term.IsTerminal(int(syscall.Stdin)) {
// 標準入力を受け取る
stdin, _ := ioutil.ReadAll(os.Stdin)
text = string(stdin)
} else {
// 引数を全てスペースで連結
text = strings.Join(f.Args(), " ")
}

editor, _ := f.GetString("editor")
quoteId, _ := f.GetString("quote")
replyId, _ := f.GetString("reply")
images, _ := f.GetStringSlice("image")

if text == "" {
// テキストエリアを開く
if !shared.isCommandLineMode && !pref.Feature.UseExternalEditor {
a.view.ShowTextArea(pref.Text.TweetTextAreaHint, func(s string) {
execPostTweet(s, quoteId, replyId, images)
execPostTweet(f, s)
})
return nil
}

editor, _ := f.GetString("editor")

// エディタを開く
t, err := a.editTweetExternalEditor(editor)
if err != nil {
Expand All @@ -81,14 +79,13 @@ func (a *App) execTweetCmd(c *cli.Command, f *pflag.FlagSet) error {
text = t
}

execPostTweet(text, quoteId, replyId, images)
execPostTweet(f, text)

return nil
}

// editTweetExternalEditor : 外部エディタでツイートを編集する
func (a *App) editTweetExternalEditor(editor string) (string, error) {
// 一時ファイル作成
tmpFilePath := path.Join(os.TempDir(), ".nekome_tweet_tmp")
if _, err := os.Create(tmpFilePath); err != nil {
return "", err
Expand All @@ -111,34 +108,48 @@ func (a *App) editTweetExternalEditor(editor string) (string, error) {
}

// execPostTweet : ツイートを投稿
func execPostTweet(text, quoteId, replyId string, images []string) {
text = trimEndNewline(text)
func execPostTweet(f *pflag.FlagSet, t string) {
images, _ := f.GetStringSlice("image")
text := trimEndNewline(t)

// 文章も画像もない場合キャンセル
if text == "" && len(images) == 0 {
return
}

quoteID, _ := f.GetString("quote")
replyID, _ := f.GetString("reply")
existClipboardImage, _ := f.GetBool("clipboard")

post := func() {
var mediaIids []string
mediaIDs := []string{}

if existImages := len(images) > 0; existImages || existClipboardImage {
var err error

if existImages {
mediaIDs, err = uploadImages(images)
} else {
mediaIDs, err = uploadImageFromClipboard()
}

// 画像をアップロード
if images != nil {
ids, err := uploadImages(images)
if err != nil {
shared.SetErrorStatus("Upload Image", err.Error())
shared.SetErrorStatus("Media", err.Error())
return
}

mediaIids = ids
}

if err := shared.api.PostTweet(text, quoteId, replyId, mediaIids); err != nil {
if err := shared.api.PostTweet(text, quoteID, replyID, mediaIDs); err != nil {
shared.SetErrorStatus("Tweet", err.Error())
return
}

shared.SetStatus("Tweeted", text)
statusLabel := "Tweeted"
if len(mediaIDs) > 0 {
statusLabel += fmt.Sprintf(" / %d attached images", len(mediaIDs))
}

shared.SetStatus(statusLabel, text)
}

// 確認画面不要 or コマンドラインモードならそのまま実行
Expand All @@ -147,11 +158,11 @@ func execPostTweet(text, quoteId, replyId string, images []string) {
return
}

// 実行しようとしている操作名
operationType := "tweet"

if replyId != "" {
if replyID != "" {
operationType = "reply"
} else if quoteId != "" {
} else if quoteID != "" {
operationType = "quote tweet"
}

Expand All @@ -166,7 +177,27 @@ func execPostTweet(text, quoteId, replyId string, images []string) {
})
}

// uploadImages : 画像をアップロード
// uploadImageFromClipboard : クリップボードの画像をアップロード
func uploadImageFromClipboard() ([]string, error) {
r, err := clipboard.Read()
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r); err != nil {
return nil, err
}

res, err := shared.api.UploadImage(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("upload failed: %w", err)
}

return []string{res.MediaIDString}, nil
}

// uploadImages : 複数の画像をアップロード
func uploadImages(images []string) ([]string, error) {
imagesCount := len(images)

Expand Down Expand Up @@ -197,8 +228,7 @@ func uploadImages(images []string) ([]string, error) {
return fmt.Errorf("failed to load file (%s)", image)
}

base64Image := base64.StdEncoding.EncodeToString(rawImage)
res, err := shared.api.UploadImage(base64Image)
res, err := shared.api.UploadImage(rawImage)
if err != nil {
return fmt.Errorf("upload failed (%s): %w", image, err)
}
Expand All @@ -216,10 +246,10 @@ func uploadImages(images []string) ([]string, error) {

close(ch)

mediaIds := []string{}
mediaIDs := []string{}
for id := range ch {
mediaIds = append(mediaIds, id)
mediaIDs = append(mediaIDs, id)
}

return mediaIds, nil
return mediaIDs, nil
}
11 changes: 7 additions & 4 deletions docs/en/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ nekome tweet [flags] [text]
#### Flags

- `-e <editor command>` `--editor <editor command>`
- Specify which editor to use
- Specify the editor to use for editing
- If omitted, the value of `$EDITOR` is specified
- `-i <file path>` `--image <file path>`
- Image to be attached
- Attach the image
- To specify multiple images, separate them with `,`
- `-c` `--clipboard`
- Attach the image in the clipboard
- If the --image is specified, it takes precedence
- `-q <tweet id>` `--quote <tweet id>`
- Specify the ID of the tweet to quote
- Quotes the tweet with the specified ID
- `-r <tweet ID>` `--reply <tweet ID>`
- Specify the ID of the tweet to which you are replying
- Send a reply to the tweet with the specified ID

## Commands available from CLI

Expand Down
9 changes: 6 additions & 3 deletions docs/ja/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ nekome tweet [フラグ] [テキスト]
- 使用するエディタを指定します
- 省略した場合、`$EDITOR` の値が指定されます
- `-i <ファイルパス>` `--image <ファイルパス>`
- 添付する画像を指定します
- 画像を添付します
- 複数指定する場合、`,` で区切ってください
- `-c` `--clipboard`
- クリップボード内の画像を添付します
- `--image` が指定されている場合、そちらが優先されます
- `-q <ツイートID>` `--quote <ツイートID>`
- 引用するツイートを指定します
- ツイートを引用します
- `-r <ツイートID>` `--reply <ツイートID>`
- リプライ先のツイートを指定します
- リプライを送信します

## CLI でのみ使用できるコマンド

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/skanehira/clipboard-image v1.0.0 // indirect
github.com/skanehira/clipboard-image/v2 v2.0.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/skanehira/clipboard-image v1.0.0 h1:MJ5PeXxDMteS0HCsjvuoMscBi+AtoqCiPX7bZ2OAxDE=
github.com/skanehira/clipboard-image v1.0.0/go.mod h1:WAxMgBkENpa206RHfrqV/5y8Kq7CitAozlvVxQxa9gs=
github.com/skanehira/clipboard-image/v2 v2.0.0 h1:Kp+RNOgIlgzDkP3EskwuBnM0Fk4sc+HgcWE5RC+PnNI=
github.com/skanehira/clipboard-image/v2 v2.0.0/go.mod h1:NXSYl4FJinIUFKJfeP1lGz8DIEUYjnEqwdMZ777S1E0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
2 changes: 1 addition & 1 deletion log/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ func Exit(s string) {

// ErrorExit : エラーを出力して終了
func ErrorExit(e string, c ExitCode) {
fmt.Fprintf(os.Stderr, "[Error] %s\n", e)
fmt.Fprintf(os.Stderr, "Error: %s\n", e)
os.Exit(c.GetInt())
}

0 comments on commit fd74c9e

Please sign in to comment.