From 5e7944832bd5fbf5b7d360fe946296c78303608e Mon Sep 17 00:00:00 2001
From: catatsuy <catatsuy@catatsuy.org>
Date: Sat, 27 Apr 2024 17:02:11 +0900
Subject: [PATCH 1/4] Update slack file upload process with new channel ID

---
 internal/cli/cli.go                           |  32 +-
 internal/cli/cli_test.go                      | 111 ++-----
 internal/config/config.go                     |  24 +-
 internal/config/config_test.go                |  57 +++-
 internal/config/testdata/config.toml          |   2 +-
 .../config/testdata/config_deprecated.toml    |   8 +
 internal/slack/client.go                      |  69 ++--
 internal/slack/client_test.go                 | 297 +++++++++++++-----
 internal/slack/export_test.go                 |   8 -
 9 files changed, 341 insertions(+), 267 deletions(-)
 create mode 100644 internal/config/testdata/config_deprecated.toml

diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index cf9b841..cb3af60 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -69,7 +69,8 @@ func (c *CLI) Run(args []string) int {
 	flags := flag.NewFlagSet("notify_slack", flag.ContinueOnError)
 	flags.SetOutput(c.errStream)
 
-	flags.StringVar(&c.conf.PrimaryChannel, "channel", "", "specify channel (unavailable for new Incoming Webhooks)")
+	flags.StringVar(&c.conf.Channel, "channel", "", "specify channel (unavailable for new Incoming Webhooks)")
+	flags.StringVar(&c.conf.ChannelID, "channel-id", "", "specify channel id (for uploading a file)")
 	flags.StringVar(&c.conf.SlackURL, "slack-url", "", "slack url (Incoming Webhooks URL)")
 	flags.StringVar(&c.conf.Token, "token", "", "token (for uploading to snippet)")
 	flags.StringVar(&c.conf.Username, "username", "", "specify username (unavailable for new Incoming Webhooks)")
@@ -178,10 +179,7 @@ func (c *CLI) Run(args []string) int {
 	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
 	defer stop()
 
-	channel := c.conf.PrimaryChannel
-	if channel == "" {
-		channel = c.conf.Channel
-	}
+	channel := c.conf.Channel
 
 	param := &slack.PostTextParam{
 		Channel:   channel,
@@ -216,18 +214,8 @@ func (c *CLI) Run(args []string) int {
 	return ExitCodeOK
 }
 
-func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, filetype string) error {
-	channel := c.conf.PrimaryChannel
-	if channel == "" {
-		channel = c.conf.SnippetChannel
-	}
-	if channel == "" {
-		channel = c.conf.Channel
-	}
-
-	if channel == "" {
-		return fmt.Errorf("must specify channel for uploading to snippet")
-	}
+func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, snippetType string) error {
+	channelID := c.conf.ChannelID
 
 	var reader io.ReadCloser
 	if filename == "" {
@@ -254,12 +242,12 @@ func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, filet
 	}
 
 	param := &slack.PostFileParam{
-		Channel:  channel,
-		Filename: uploadFilename,
-		Content:  string(content),
-		Filetype: filetype,
+		ChannelID:   channelID,
+		Filename:    uploadFilename,
+		SnippetType: snippetType,
 	}
-	err = c.sClient.PostFile(ctx, param)
+
+	err = c.sClient.PostFile(ctx, param, content)
 	if err != nil {
 		return err
 	}
diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
index 1b4e4c7..30875f8 100644
--- a/internal/cli/cli_test.go
+++ b/internal/cli/cli_test.go
@@ -9,16 +9,17 @@ import (
 
 	"github.com/catatsuy/notify_slack/internal/config"
 	"github.com/catatsuy/notify_slack/internal/slack"
+	"github.com/google/go-cmp/cmp"
 )
 
 type fakeSlackClient struct {
 	slack.Slack
 
-	FakePostFile func(ctx context.Context, param *slack.PostFileParam) error
+	FakePostFile func(ctx context.Context, param *slack.PostFileParam, content []byte) error
 }
 
-func (c *fakeSlackClient) PostFile(ctx context.Context, param *slack.PostFileParam) error {
-	return c.FakePostFile(ctx, param)
+func (c *fakeSlackClient) PostFile(ctx context.Context, param *slack.PostFileParam, content []byte) error {
+	return c.FakePostFile(ctx, param, content)
 }
 
 func (c *fakeSlackClient) PostText(ctx context.Context, param *slack.PostTextParam) error {
@@ -48,33 +49,23 @@ func TestUploadSnippet(t *testing.T) {
 		conf:    config.NewConfig(),
 	}
 
-	err := cl.uploadSnippet(context.Background(), "", "", "")
-	want := "must specify channel"
-	if err == nil || !strings.Contains(err.Error(), want) {
-		t.Errorf("error = %v; want %q", err, want)
-	}
-
-	cl.conf.Channel = "normal_channel"
-	err = cl.uploadSnippet(context.Background(), "testdata/nofile.txt", "", "")
-	want = "no such file or directory"
+	cl.conf.ChannelID = "C12345678"
+	err := cl.uploadSnippet(context.Background(), "testdata/nofile.txt", "", "")
+	want := "no such file or directory"
 	if err == nil || !strings.Contains(err.Error(), want) {
 		t.Errorf("error = %v; want %q", err, want)
 	}
 
 	cl.sClient = &fakeSlackClient{
-		FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error {
-			if param.Channel != cl.conf.Channel {
-				t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel)
-			}
-
+		FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error {
 			expectedFilename := "testdata/upload.txt"
 			if param.Filename != expectedFilename {
 				t.Errorf("expected %s; got %s", expectedFilename, param.Filename)
 			}
 
 			expectedContent := "upload_test\n"
-			if param.Content != expectedContent {
-				t.Errorf("expected %q; got %q", expectedContent, param.Content)
+			if diff := cmp.Diff(expectedContent, string(content)); diff != "" {
+				t.Errorf("unexpected diff: (-want +got):\n%s", diff)
 			}
 
 			return nil
@@ -87,19 +78,15 @@ func TestUploadSnippet(t *testing.T) {
 	}
 
 	cl.sClient = &fakeSlackClient{
-		FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error {
-			if param.Channel != cl.conf.Channel {
-				t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel)
-			}
-
+		FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error {
 			expectedFilename := "overwrite.txt"
 			if param.Filename != expectedFilename {
 				t.Errorf("expected %s; got %s", expectedFilename, param.Filename)
 			}
 
 			expectedContent := "upload_test\n"
-			if param.Content != expectedContent {
-				t.Errorf("expected %q; got %q", expectedContent, param.Content)
+			if diff := cmp.Diff(expectedContent, string(content)); diff != "" {
+				t.Errorf("unexpected diff: (-want +got):\n%s", diff)
 			}
 
 			return nil
@@ -112,9 +99,9 @@ func TestUploadSnippet(t *testing.T) {
 	}
 
 	cl.sClient = &fakeSlackClient{
-		FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error {
-			if param.Channel != cl.conf.Channel {
-				t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel)
+		FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error {
+			if param.ChannelID != cl.conf.ChannelID {
+				t.Errorf("expected %s; got %s", cl.conf.ChannelID, param.ChannelID)
 			}
 
 			expectedFilename := "overwrite.txt"
@@ -122,75 +109,21 @@ func TestUploadSnippet(t *testing.T) {
 				t.Errorf("expected %s; got %s", expectedFilename, param.Filename)
 			}
 
-			expectedContent := "upload_test\n"
-			if param.Content != expectedContent {
-				t.Errorf("expected %q; got %q", expectedContent, param.Content)
-			}
-
-			expectedFiletype := "diff"
-			if param.Filetype != expectedFiletype {
-				t.Errorf("expected %s; got %s", expectedFiletype, param.Filetype)
-			}
-
-			return nil
-		},
-	}
-
-	err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "overwrite.txt", "diff")
-	if err != nil {
-		t.Errorf("expected nil; got %v", err)
-	}
-
-	cl.conf.SnippetChannel = "snippet_channel"
-
-	cl.sClient = &fakeSlackClient{
-		FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error {
-			if param.Channel != cl.conf.SnippetChannel {
-				t.Errorf("expected %s; got %s", cl.conf.SnippetChannel, param.Channel)
-			}
-
-			expectedFilename := "testdata/upload.txt"
-			if param.Filename != expectedFilename {
-				t.Errorf("expected %s; got %s", expectedFilename, param.Filename)
-			}
-
-			expectedContent := "upload_test\n"
-			if param.Content != expectedContent {
-				t.Errorf("expected %q; got %q", expectedContent, param.Content)
-			}
-
-			return nil
-		},
-	}
-
-	err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "", "")
-	if err != nil {
-		t.Errorf("expected nil; got %v", err)
-	}
-
-	cl.conf.PrimaryChannel = "primary_channel"
-
-	cl.sClient = &fakeSlackClient{
-		FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error {
-			if param.Channel != cl.conf.PrimaryChannel {
-				t.Errorf("expected %s; got %s", cl.conf.PrimaryChannel, param.Channel)
-			}
-
-			expectedFilename := "testdata/upload.txt"
-			if param.Filename != expectedFilename {
-				t.Errorf("expected %s; got %s", expectedFilename, param.Filename)
+			expectedSnippetType := "diff"
+			if param.SnippetType != expectedSnippetType {
+				t.Errorf("expected %s; got %s", expectedSnippetType, param.SnippetType)
 			}
 
 			expectedContent := "upload_test\n"
-			if param.Content != expectedContent {
-				t.Errorf("expected %q; got %q", expectedContent, param.Content)
+			if diff := cmp.Diff(expectedContent, string(content)); diff != "" {
+				t.Errorf("unexpected diff: (-want +got):\n%s", diff)
 			}
 
 			return nil
 		},
 	}
 
-	err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "", "")
+	err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "overwrite.txt", "diff")
 	if err != nil {
 		t.Errorf("expected nil; got %v", err)
 	}
diff --git a/internal/config/config.go b/internal/config/config.go
index e078281..bbf41b4 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -16,9 +16,9 @@ var (
 type Config struct {
 	SlackURL       string
 	Token          string
-	PrimaryChannel string
 	Channel        string
 	SnippetChannel string
+	ChannelID      string
 	Username       string
 	IconEmoji      string
 	Duration       time.Duration
@@ -42,7 +42,13 @@ func (c *Config) LoadEnv() error {
 	}
 
 	if c.SnippetChannel == "" {
-		c.SnippetChannel = os.Getenv("NOTIFY_SLACK_SNIPPET_CHANNEL")
+		if os.Getenv("NOTIFY_SLACK_SNIPPET_CHANNEL") != "" {
+			return fmt.Errorf("the NOTIFY_SLACK_SNIPPET_CHANNEL option is deprecated")
+		}
+	}
+
+	if c.ChannelID == "" {
+		c.ChannelID = os.Getenv("NOTIFY_SLACK_CHANNEL_ID")
 	}
 
 	if c.Username == "" {
@@ -70,6 +76,7 @@ type slackConfig struct {
 	Token          string
 	Channel        string
 	SnippetChannel string `toml:"snippet_channel"`
+	ChannelID      string `toml:"channel_id"`
 	Username       string
 	IconEmoji      string `toml:"icon_emoji"`
 	Interval       string
@@ -109,16 +116,19 @@ func (c *Config) LoadTOML(filename string) error {
 			c.Channel = slackConfig.Channel
 		}
 	}
-	if c.SnippetChannel == "" {
-		if slackConfig.SnippetChannel != "" {
-			c.SnippetChannel = slackConfig.SnippetChannel
-		}
-	}
 	if c.Username == "" {
 		if slackConfig.Username != "" {
 			c.Username = slackConfig.Username
 		}
 	}
+	if slackConfig.SnippetChannel != "" {
+		return fmt.Errorf("the snippet_channel option is deprecated")
+	}
+	if c.ChannelID == "" {
+		if slackConfig.ChannelID != "" {
+			c.ChannelID = slackConfig.ChannelID
+		}
+	}
 	if c.IconEmoji == "" {
 		if slackConfig.IconEmoji != "" {
 			c.IconEmoji = slackConfig.IconEmoji
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 3239e10..93271de 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -3,6 +3,7 @@ package config_test
 import (
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 	"time"
 
@@ -27,9 +28,9 @@ func TestLoadTOML(t *testing.T) {
 	if c.Channel != expectedChannel {
 		t.Errorf("got %s, want %s", c.Channel, expectedChannel)
 	}
-	expectedSnippetChannel := "#general"
-	if c.SnippetChannel != expectedSnippetChannel {
-		t.Errorf("got %s, want %s", c.SnippetChannel, expectedSnippetChannel)
+	expectedChannelID := "C12345678"
+	if c.ChannelID != expectedChannelID {
+		t.Errorf("got %s, want %s", c.ChannelID, expectedChannelID)
 	}
 	expectedUsername := "deploy!"
 	if c.Username != expectedUsername {
@@ -45,11 +46,24 @@ func TestLoadTOML(t *testing.T) {
 	}
 }
 
+func TestLoadTOML_Deprecated(t *testing.T) {
+	c := NewConfig()
+	err := c.LoadTOML("./testdata/config_deprecated.toml")
+	if err == nil {
+		t.Fatal("expected error, but got nil")
+	}
+
+	expected := "the snippet_channel option is deprecated"
+	if !strings.Contains(err.Error(), expected) {
+		t.Errorf("got %s, want %s", err.Error(), expected)
+	}
+}
+
 func TestLoadEnv(t *testing.T) {
 	expectedSlackURL := "https://hooks.slack.com/aaaaa"
 	expectedToken := "xoxp-token"
 	expectedChannel := "#test"
-	expectedSnippetChannel := "#general"
+	expectedChannelID := "C12345678"
 	expectedUsername := "deploy!"
 	expectedIconEmoji := ":rocket:"
 	expectedIntervalStr := "2s"
@@ -58,7 +72,7 @@ func TestLoadEnv(t *testing.T) {
 	t.Setenv("NOTIFY_SLACK_WEBHOOK_URL", expectedSlackURL)
 	t.Setenv("NOTIFY_SLACK_TOKEN", expectedToken)
 	t.Setenv("NOTIFY_SLACK_CHANNEL", expectedChannel)
-	t.Setenv("NOTIFY_SLACK_SNIPPET_CHANNEL", expectedSnippetChannel)
+	t.Setenv("NOTIFY_SLACK_CHANNEL_ID", expectedChannelID)
 	t.Setenv("NOTIFY_SLACK_USERNAME", expectedUsername)
 	t.Setenv("NOTIFY_SLACK_ICON_EMOJI", expectedIconEmoji)
 	t.Setenv("NOTIFY_SLACK_INTERVAL", expectedIntervalStr)
@@ -81,8 +95,8 @@ func TestLoadEnv(t *testing.T) {
 		t.Errorf("got %s, want %s", c.Channel, expectedChannel)
 	}
 
-	if c.SnippetChannel != expectedSnippetChannel {
-		t.Errorf("got %s, want %s", c.SnippetChannel, expectedSnippetChannel)
+	if c.ChannelID != expectedChannelID {
+		t.Errorf("got %s, want %s", c.ChannelID, expectedChannelID)
 	}
 
 	if c.Username != expectedUsername {
@@ -98,6 +112,35 @@ func TestLoadEnv(t *testing.T) {
 	}
 }
 
+func TestLoadEnv_Deprecated(t *testing.T) {
+	expectedSlackURL := "https://hooks.slack.com/aaaaa"
+	expectedToken := "xoxp-token"
+	expectedChannel := "#test"
+	expectedSnippetChannel := "#general"
+	expectedUsername := "deploy!"
+	expectedIconEmoji := ":rocket:"
+	expectedIntervalStr := "2s"
+
+	t.Setenv("NOTIFY_SLACK_WEBHOOK_URL", expectedSlackURL)
+	t.Setenv("NOTIFY_SLACK_TOKEN", expectedToken)
+	t.Setenv("NOTIFY_SLACK_CHANNEL", expectedChannel)
+	t.Setenv("NOTIFY_SLACK_SNIPPET_CHANNEL", expectedSnippetChannel)
+	t.Setenv("NOTIFY_SLACK_USERNAME", expectedUsername)
+	t.Setenv("NOTIFY_SLACK_ICON_EMOJI", expectedIconEmoji)
+	t.Setenv("NOTIFY_SLACK_INTERVAL", expectedIntervalStr)
+
+	c := NewConfig()
+	err := c.LoadEnv()
+	if err == nil {
+		t.Fatal("expected error, but got nil")
+	}
+
+	expected := "the NOTIFY_SLACK_SNIPPET_CHANNEL option is deprecated"
+	if !strings.Contains(err.Error(), expected) {
+		t.Errorf("got %s, want %s", err.Error(), expected)
+	}
+}
+
 func TestLoadTOMLFilename(t *testing.T) {
 	baseDir := "./testdata/"
 	defer SetUserHomeDir(baseDir)()
diff --git a/internal/config/testdata/config.toml b/internal/config/testdata/config.toml
index fe205cd..88e32f5 100644
--- a/internal/config/testdata/config.toml
+++ b/internal/config/testdata/config.toml
@@ -2,7 +2,7 @@
 url = "https://hooks.slack.com/aaaaa"
 token = "xoxp-token"
 channel = "#test"
-snippet_channel = "#general"
+channel_id = "C12345678"
 username = "deploy!"
 icon_emoji = ":rocket:"
 interval = "2s"
diff --git a/internal/config/testdata/config_deprecated.toml b/internal/config/testdata/config_deprecated.toml
new file mode 100644
index 0000000..fe205cd
--- /dev/null
+++ b/internal/config/testdata/config_deprecated.toml
@@ -0,0 +1,8 @@
+[slack]
+url = "https://hooks.slack.com/aaaaa"
+token = "xoxp-token"
+channel = "#test"
+snippet_channel = "#general"
+username = "deploy!"
+icon_emoji = ":rocket:"
+interval = "2s"
diff --git a/internal/slack/client.go b/internal/slack/client.go
index a4ae46d..82f1faa 100644
--- a/internal/slack/client.go
+++ b/internal/slack/client.go
@@ -15,7 +15,6 @@ import (
 )
 
 var (
-	slackFilesUploadURL            = "https://slack.com/api/files.upload"
 	filesGetUploadURLExternalURL   = "https://slack.com/api/files.getUploadURLExternal"
 	filesCompleteUploadExternalURL = "https://slack.com/api/files.completeUploadExternal"
 )
@@ -45,10 +44,11 @@ type PostTextParam struct {
 }
 
 type PostFileParam struct {
-	Channel  string
-	Content  string
-	Filename string
-	Filetype string
+	ChannelID   string
+	Filename    string
+	AltText     string
+	Title       string
+	SnippetType string
 }
 
 type GetUploadURLExternalResParam struct {
@@ -60,7 +60,7 @@ type GetUploadURLExternalResParam struct {
 
 type Slack interface {
 	PostText(ctx context.Context, param *PostTextParam) error
-	PostFile(ctx context.Context, param *PostFileParam) error
+	PostFile(ctx context.Context, param *PostFileParam, content []byte) error
 }
 
 func NewClient(urlStr string, logger *slog.Logger) (*Client, error) {
@@ -145,60 +145,35 @@ func (c *Client) PostText(ctx context.Context, param *PostTextParam) error {
 	return nil
 }
 
-type apiFilesUploadRes struct {
-	OK bool `json:"ok"`
-}
-
-func (c *Client) PostFile(ctx context.Context, param *PostFileParam) error {
-	if param.Content == "" {
-		return fmt.Errorf("the content of the file is empty")
+func (c *Client) PostFile(ctx context.Context, param *PostFileParam, content []byte) error {
+	uParam := &GetUploadURLExternalResParam{
+		Filename:    param.Filename,
+		Length:      len(content),
+		SnippetType: param.SnippetType,
+		AltText:     param.AltText,
 	}
 
-	v := url.Values{}
-	v.Set("token", c.Token)
-	v.Set("content", param.Content)
-	v.Set("filename", param.Filename)
-	v.Set("channels", param.Channel)
-
-	if param.Filetype != "" {
-		v.Set("filetype", param.Filetype)
-	}
-
-	req, err := http.NewRequest("POST", slackFilesUploadURL, strings.NewReader(v.Encode()))
-	if err != nil {
-		return err
-	}
-
-	req = req.WithContext(ctx)
-
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	res, err := c.HTTPClient.Do(req)
+	uploadURL, fileID, err := c.GetUploadURLExternalURL(ctx, uParam)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed to get upload url: %w", err)
 	}
-	defer res.Body.Close()
 
-	b, err := io.ReadAll(res.Body)
+	err = c.UploadToURL(ctx, param.Filename, uploadURL, content)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed to upload file: %w", err)
 	}
 
-	c.Logger.Debug("request", "url", req.URL.String(), "method", req.Method, "header", req.Header, "status", res.StatusCode, "body", b)
-
-	if res.StatusCode != http.StatusOK {
-		return fmt.Errorf("failed to read res.Body and the status code of the response from slack was not 200; body: %s", b)
+	cParam := &CompleteUploadExternalParam{
+		FileID:    fileID,
+		Title:     param.Title,
+		ChannelID: param.ChannelID,
 	}
 
-	apiRes := apiFilesUploadRes{}
-	err = json.Unmarshal(b, &apiRes)
+	err = c.CompleteUploadExternal(ctx, cParam)
 	if err != nil {
-		return fmt.Errorf("response returned from slack is not json: %w", err)
+		return fmt.Errorf("failed to complete upload: %w", err)
 	}
 
-	if !apiRes.OK {
-		return fmt.Errorf("response has failed; body: %s", b)
-	}
 	return nil
 }
 
diff --git a/internal/slack/client_test.go b/internal/slack/client_test.go
index 6bd727f..3713cb0 100644
--- a/internal/slack/client_test.go
+++ b/internal/slack/client_test.go
@@ -10,6 +10,7 @@ import (
 	"net/url"
 	"os"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -108,8 +109,13 @@ func TestPostText_Fail(t *testing.T) {
 	}
 
 	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		b, err := os.ReadFile("testdata/post_text_fail.html")
+		if err != nil {
+			t.Fatal(err)
+		}
+
 		w.WriteHeader(http.StatusNotFound)
-		http.ServeFile(w, r, "testdata/post_text_fail.html")
+		w.Write(b)
 	})
 
 	c, err := NewClient(testAPIServer.URL, slog.New(slog.NewTextHandler(io.Discard, nil)))
@@ -136,19 +142,24 @@ func TestPostFile_Success(t *testing.T) {
 
 	slackToken := "slack-token"
 
-	param := &PostFileParam{
-		Channel:  "test-channel",
-		Content:  "testtesttest",
+	param := &GetUploadURLExternalResParam{
 		Filename: "test.txt",
+		Length:   100,
 	}
 
-	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) {
 		contentType := r.Header.Get("Content-Type")
 		expectedType := "application/x-www-form-urlencoded"
 		if contentType != expectedType {
 			t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType)
 		}
 
+		authorization := r.Header.Get("Authorization")
+		expectedAuth := "Bearer " + slackToken
+		if authorization != expectedAuth {
+			t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization)
+		}
+
 		bodyBytes, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Fatal(err)
@@ -161,53 +172,115 @@ func TestPostFile_Success(t *testing.T) {
 		}
 
 		expectedV := url.Values{}
-		expectedV.Set("token", slackToken)
-		expectedV.Set("content", param.Content)
 		expectedV.Set("filename", param.Filename)
-		expectedV.Set("channels", param.Channel)
+		expectedV.Set("length", strconv.Itoa(param.Length))
 
-		if !reflect.DeepEqual(actualV, expectedV) {
-			t.Fatalf("expected %q to equal %q", actualV, expectedV)
+		if diff := cmp.Diff(expectedV, actualV); diff != "" {
+			t.Errorf("unexpected diff: (-want +got):\n%s", diff)
 		}
 
-		http.ServeFile(w, r, "testdata/post_files_upload_ok.json")
+		http.ServeFile(w, r, "testdata/files_get_upload_url_external_ok.json")
 	})
 
-	defer SetSlackFilesUploadURL(testAPIServer.URL)()
+	defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")()
 
 	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = c.PostFile(context.Background(), param)
+	uploadURL, fileID, err := c.GetUploadURLExternalURL(context.Background(), param)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expectedUploadURL := "https://files.slack.com/upload/v1/ABC123456"
+	if uploadURL != expectedUploadURL {
+		t.Fatalf("expected %q to equal %q", uploadURL, expectedUploadURL)
+	}
+
+	expectedFileID := "F123ABC456"
+	if fileID != expectedFileID {
+		t.Fatalf("expected %q to equal %q", fileID, expectedFileID)
+	}
+}
+
+func TestPostFile_FailCallFunc(t *testing.T) {
+	muxAPI := http.NewServeMux()
+	testAPIServer := httptest.NewServer(muxAPI)
+	defer testAPIServer.Close()
+
+	slackToken := "slack-token"
+
+	muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) {
+		panic("unexpected call")
+	})
+
+	defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")()
 
+	_, err := NewClientForPostFile("", slog.New(slog.NewTextHandler(io.Discard, nil)))
+	expectedErrorPart := "provide Slack token"
+	if err == nil {
+		t.Fatal("expected error, but nothing was returned")
+	} else if !strings.Contains(err.Error(), expectedErrorPart) {
+		t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart)
+	}
+
+	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), nil)
+	expectedErrorPart = "provide filename and length"
+	if err == nil {
+		t.Fatal("expected error, but nothing was returned")
+	} else if !strings.Contains(err.Error(), expectedErrorPart) {
+		t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart)
+	}
+
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), &GetUploadURLExternalResParam{})
+	expectedErrorPart = "provide filename"
+	if err == nil {
+		t.Fatal("expected error, but nothing was returned")
+	} else if !strings.Contains(err.Error(), expectedErrorPart) {
+		t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart)
+	}
+
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), &GetUploadURLExternalResParam{Filename: "test.txt"})
+	expectedErrorPart = "provide length"
+	if err == nil {
+		t.Fatal("expected error, but nothing was returned")
+	} else if !strings.Contains(err.Error(), expectedErrorPart) {
+		t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart)
+	}
 }
 
-func TestPostFile_Success_provideFiletype(t *testing.T) {
+func TestPostFile_FailAPINotOK(t *testing.T) {
 	muxAPI := http.NewServeMux()
 	testAPIServer := httptest.NewServer(muxAPI)
 	defer testAPIServer.Close()
 
 	slackToken := "slack-token"
 
-	param := &PostFileParam{
-		Channel:  "test-channel",
-		Content:  "testtesttest",
+	param := &GetUploadURLExternalResParam{
 		Filename: "test.txt",
-		Filetype: "diff",
+		Length:   100,
 	}
 
-	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) {
 		contentType := r.Header.Get("Content-Type")
 		expectedType := "application/x-www-form-urlencoded"
 		if contentType != expectedType {
 			t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType)
 		}
 
+		authorization := r.Header.Get("Authorization")
+		expectedAuth := "Bearer " + slackToken
+		if authorization != expectedAuth {
+			t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization)
+		}
+
 		bodyBytes, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Fatal(err)
@@ -220,139 +293,191 @@ func TestPostFile_Success_provideFiletype(t *testing.T) {
 		}
 
 		expectedV := url.Values{}
-		expectedV.Set("token", slackToken)
-		expectedV.Set("content", param.Content)
 		expectedV.Set("filename", param.Filename)
-		expectedV.Set("filetype", param.Filetype)
-		expectedV.Set("channels", param.Channel)
+		expectedV.Set("length", strconv.Itoa(param.Length))
+
+		if diff := cmp.Diff(expectedV, actualV); diff != "" {
+			t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+		}
 
-		if !reflect.DeepEqual(actualV, expectedV) {
-			t.Fatalf("expected %q to equal %q", actualV, expectedV)
+		w.WriteHeader(http.StatusForbidden)
+		w.Header().Set("Content-Type", "application/json")
+
+		b, err := os.ReadFile("testdata/files_get_upload_url_external_fail.json")
+		if err != nil {
+			t.Fatal(err)
 		}
 
-		http.ServeFile(w, r, "testdata/post_files_upload_ok.json")
+		w.Write(b)
 	})
 
-	defer SetSlackFilesUploadURL(testAPIServer.URL)()
+	defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")()
 
 	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = c.PostFile(context.Background(), param)
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), param)
 
-	if err != nil {
-		t.Fatal(err)
+	if err == nil {
+		t.Fatal("expected error, but nothing was returned")
+	} else {
+		expected := "status code: 403"
+		if !strings.Contains(err.Error(), expected) {
+			t.Errorf("expected %q to contain %q", err.Error(), expected)
+		}
+
+		expectedBodyPart := `"invalid_auth"`
+		if !strings.Contains(err.Error(), expectedBodyPart) {
+			t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart)
+		}
 	}
 }
 
-func TestPostFile_FailNotOk(t *testing.T) {
+func TestPostFile_FailAPIStatusOK(t *testing.T) {
 	muxAPI := http.NewServeMux()
 	testAPIServer := httptest.NewServer(muxAPI)
 	defer testAPIServer.Close()
 
 	slackToken := "slack-token"
 
-	param := &PostFileParam{
-		Channel:  "test-channel",
-		Content:  "testtesttest",
+	param := &GetUploadURLExternalResParam{
 		Filename: "test.txt",
+		Length:   100,
 	}
 
-	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, "testdata/post_files_upload_fail.json")
-	})
-
-	defer SetSlackFilesUploadURL(testAPIServer.URL)()
+	muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) {
+		contentType := r.Header.Get("Content-Type")
+		expectedType := "application/x-www-form-urlencoded"
+		if contentType != expectedType {
+			t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType)
+		}
 
-	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
-	if err != nil {
-		t.Fatal(err)
-	}
+		authorization := r.Header.Get("Authorization")
+		expectedAuth := "Bearer " + slackToken
+		if authorization != expectedAuth {
+			t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization)
+		}
 
-	err = c.PostFile(context.Background(), param)
+		bodyBytes, err := io.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer r.Body.Close()
 
-	if err == nil {
-		t.Fatal("expected error, but nothing was returned")
-	}
+		actualV, err := url.ParseQuery(string(bodyBytes))
+		if err != nil {
+			t.Fatal(err)
+		}
 
-	expected := `response has failed; body: {"ok":false,"error":"invalid_auth"}`
-	if !strings.Contains(err.Error(), expected) {
-		t.Fatalf("expected %q to contain %q", err.Error(), expected)
-	}
-}
+		expectedV := url.Values{}
+		expectedV.Set("filename", param.Filename)
+		expectedV.Set("length", strconv.Itoa(param.Length))
 
-func TestPostFile_FailNotResponseStatusCodeNotOK(t *testing.T) {
-	muxAPI := http.NewServeMux()
-	testAPIServer := httptest.NewServer(muxAPI)
-	defer testAPIServer.Close()
+		if diff := cmp.Diff(expectedV, actualV); diff != "" {
+			t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+		}
 
-	slackToken := "slack-token"
+		w.Header().Set("Content-Type", "application/json")
 
-	param := &PostFileParam{
-		Channel:  "test-channel",
-		Content:  "testtesttest",
-		Filename: "test.txt",
-	}
+		b, err := os.ReadFile("testdata/files_get_upload_url_external_fail_invalid_arguments.json")
+		if err != nil {
+			t.Fatal(err)
+		}
 
-	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusNotFound)
-		http.ServeFile(w, r, "testdata/post_files_upload_fail.json")
+		w.Write(b)
 	})
 
-	defer SetSlackFilesUploadURL(testAPIServer.URL)()
+	defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")()
 
 	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = c.PostFile(context.Background(), param)
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), param)
 
 	if err == nil {
 		t.Fatal("expected error, but nothing was returned")
-	}
+	} else {
+		expected := "response has failed"
+		if !strings.Contains(err.Error(), expected) {
+			t.Errorf("expected %q to contain %q", err.Error(), expected)
+		}
 
-	expected := `failed to read res.Body and the status code of the response from slack was not 200; body: {"ok":false,"error":"invalid_auth"}`
-	if !strings.Contains(err.Error(), expected) {
-		t.Fatalf("expected %q to contain %q", err.Error(), expected)
+		expectedBodyPart := `"invalid_arguments"`
+		if !strings.Contains(err.Error(), expectedBodyPart) {
+			t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart)
+		}
 	}
 }
 
-func TestPostFile_FailNotJSON(t *testing.T) {
+func TestPostFile_FailBrokenJSON(t *testing.T) {
 	muxAPI := http.NewServeMux()
 	testAPIServer := httptest.NewServer(muxAPI)
 	defer testAPIServer.Close()
 
 	slackToken := "slack-token"
 
-	param := &PostFileParam{
-		Channel:  "test-channel",
-		Content:  "testtesttest",
+	param := &GetUploadURLExternalResParam{
 		Filename: "test.txt",
+		Length:   100,
 	}
 
-	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, "testdata/post_text_fail.html")
+	muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) {
+		contentType := r.Header.Get("Content-Type")
+		expectedType := "application/x-www-form-urlencoded"
+		if contentType != expectedType {
+			t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType)
+		}
+
+		authorization := r.Header.Get("Authorization")
+		expectedAuth := "Bearer " + slackToken
+		if authorization != expectedAuth {
+			t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization)
+		}
+
+		bodyBytes, err := io.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer r.Body.Close()
+
+		actualV, err := url.ParseQuery(string(bodyBytes))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		expectedV := url.Values{}
+		expectedV.Set("filename", param.Filename)
+		expectedV.Set("length", strconv.Itoa(param.Length))
+
+		if diff := cmp.Diff(expectedV, actualV); diff != "" {
+			t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+		}
+
+		w.Header().Set("Content-Type", "text/plain")
+
+		w.Write([]byte("this is not json"))
 	})
 
-	defer SetSlackFilesUploadURL(testAPIServer.URL)()
+	defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")()
 
 	c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil)))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = c.PostFile(context.Background(), param)
+	_, _, err = c.GetUploadURLExternalURL(context.Background(), param)
 
 	if err == nil {
 		t.Fatal("expected error, but nothing was returned")
-	}
-
-	expected := `response returned from slack is not json`
-	if !strings.Contains(err.Error(), expected) {
-		t.Fatalf("expected %q to contain %q", err.Error(), expected)
+	} else {
+		expectedBodyPart := `this is not json`
+		if !strings.Contains(err.Error(), expectedBodyPart) {
+			t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart)
+		}
 	}
 }
 
@@ -488,12 +613,12 @@ func TestCompleteUploadExternal_Success(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	params := &CompleteUploadExternalParam{
+	param := &CompleteUploadExternalParam{
 		FileID:    "file-id",
 		Title:     "file-title",
 		ChannelID: "C0NF841BK",
 	}
-	err = c.CompleteUploadExternal(context.Background(), params)
+	err = c.CompleteUploadExternal(context.Background(), param)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/slack/export_test.go b/internal/slack/export_test.go
index c17dc6b..70a70a6 100644
--- a/internal/slack/export_test.go
+++ b/internal/slack/export_test.go
@@ -1,13 +1,5 @@
 package slack
 
-func SetSlackFilesUploadURL(u string) (resetFunc func()) {
-	var tmp string
-	tmp, slackFilesUploadURL = slackFilesUploadURL, u
-	return func() {
-		slackFilesUploadURL = tmp
-	}
-}
-
 func SetFilesGetUploadURLExternalURL(u string) (resetFunc func()) {
 	var tmp string
 	tmp, filesGetUploadURLExternalURL = filesGetUploadURLExternalURL, u

From a025c5509de43cc8f738a3bddf07e766090b6aed Mon Sep 17 00:00:00 2001
From: catatsuy <catatsuy@catatsuy.org>
Date: Sat, 27 Apr 2024 17:05:50 +0900
Subject: [PATCH 2/4] Add new channel id setting for file uploads

---
 README.md | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 1a15da5..f7f1357 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,10 @@ The Slack API allows you to specify the filetype of a file when posting it as a
       config file name
 -channel string
       specify channel (unavailable for new Incoming Webhooks)
+-channel-id string
+      specify channel id (for uploading a file)
+-debug
+      debug mode (for developers)
 -filename string
       specify a file name (for uploading to snippet)
 -filetype string
@@ -92,6 +96,7 @@ The toml file contains the following information.
 url = "https://hooks.slack.com/services/**"
 token = "xoxp-xxxxx"
 channel = "#general"
+channel_id = "C12345678"
 username = "tester"
 icon_emoji = ":rocket:"
 interval = "1s"
@@ -149,7 +154,7 @@ Some settings for the Slack API can be provided using environment variables.
 NOTIFY_SLACK_WEBHOOK_URL
 NOTIFY_SLACK_TOKEN
 NOTIFY_SLACK_CHANNEL
-NOTIFY_SLACK_SNIPPET_CHANNEL
+NOTIFY_SLACK_CHANNEL_ID
 NOTIFY_SLACK_USERNAME
 NOTIFY_SLACK_ICON_EMOJI
 NOTIFY_SLACK_INTERVAL

From b70ee37a518ba88902daf2b19a0247a9e6e9403d Mon Sep 17 00:00:00 2001
From: catatsuy <catatsuy@catatsuy.org>
Date: Sat, 27 Apr 2024 17:12:53 +0900
Subject: [PATCH 3/4] Update Slack snippet instructions to use `channel_id`.

---
 README.md | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index f7f1357..ce7b95a 100644
--- a/README.md
+++ b/README.md
@@ -108,13 +108,10 @@ Note:
     * You can use the following options to customize your message when posting to Slack as text: `channel`, `username`, `icon_emoji`, and `interval`.
     * Due to a recent change in the specification for Incoming Webhooks, it is currently not possible to override the `channel`, `username`, and `icon_emoji` options when posting to Slack. For more information, please refer to [this resource](https://api.slack.com/messaging/webhooks#advanced_message_formatting)
     * You can create an Incoming Webhooks URL at https://slack.com/services/new/incoming-webhook
-  * To post a file as a snippet to Slack, you will need to provide both a `token` and a `channel`.
+  * To post a file as a snippet to Slack, you will need to provide both a `token` and a `channel_id`.
     * The `username` and `icon_emoji` options will be ignored when posting a file as a snippet to Slack.
     * For instructions on how to create a token, please see the next section.
-
-Tips:
-
-  * If you want to use a different default channel for snippets, you can specify it using the `snippet_channel` option.
+    * You cannot specify a channel because the slack api support only the channel_id.
 
 ### How to create a token
 

From 5702f4e30d9cc3fa911ba6e7c09ed156f50d8631 Mon Sep 17 00:00:00 2001
From: catatsuy <catatsuy@catatsuy.org>
Date: Sat, 27 Apr 2024 17:59:18 +0900
Subject: [PATCH 4/4] Update file uploading options to maintain compatibility

---
 CHANGELOG.md        | 14 ++++++++++++++
 README.md           |  8 ++++++--
 internal/cli/cli.go |  3 ++-
 3 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b1505f..c5be712 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
 # Change Log
 
+## [v0.5.0] - 
+
+### Breaking Changes
+
+- **API Deprecation**: As per [Slack's latest API update](https://api.slack.com/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay), `files.upload` is now deprecated. We have updated our tool to use the new APIs, `files.getUploadURLExternal` and `files.completeUploadExternal`.
+- **Channel Specification**: It is no longer possible to specify a `channel` for file uploads. You must now use `channel-id`.
+- **Filetype Option Update**: The `-filetype` option has been modified. Please use `-snippet-type` instead for specifying the type of file when uploading to a snippet.
+
+### Changed
+
+* update Go version
+* update dependent libraries
+* update README
+
 ## [v0.4.14] - 2023-09-30
 
 ### Changed
diff --git a/README.md b/README.md
index ce7b95a..6578344 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ The Slack API allows you to specify the filetype of a file when posting it as a
 -filename string
       specify a file name (for uploading to snippet)
 -filetype string
-      specify a filetype (for uploading to snippet)
+      [compatible] specify a filetype for uploading to snippet. This option is maintained for compatibility. Please use -snippet-type instead.
 -icon-emoji string
       specify icon emoji (unavailable for new Incoming Webhooks)
 -interval duration
@@ -72,6 +72,8 @@ The Slack API allows you to specify the filetype of a file when posting it as a
       slack url (Incoming Webhooks URL)
 -snippet
       switch to snippet uploading mode
+-snippet-type string
+      specify a snippet_type (for uploading to snippet)
 -token string
       token (for uploading to snippet)
 -username string
@@ -111,7 +113,9 @@ Note:
   * To post a file as a snippet to Slack, you will need to provide both a `token` and a `channel_id`.
     * The `username` and `icon_emoji` options will be ignored when posting a file as a snippet to Slack.
     * For instructions on how to create a token, please see the next section.
-    * You cannot specify a channel because the slack api support only the channel_id.
+    * You cannot specify a channel because the slack api support only the `channel_id`.
+    * If you don't specify `channel_id`, the file will be private. So, if you need to post a file public, you must specify `channel_id`.
+    * The Slack API can cause delays, so posting might take longer.
 
 ### How to create a token
 
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index cb3af60..2edda25 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -78,7 +78,8 @@ func (c *CLI) Run(args []string) int {
 	flags.DurationVar(&c.conf.Duration, "interval", time.Second, "interval")
 	flags.StringVar(&tomlFile, "c", "", "config file name")
 	flags.StringVar(&uploadFilename, "filename", "", "specify a file name (for uploading to snippet)")
-	flags.StringVar(&filetype, "filetype", "", "specify a filetype (for uploading to snippet)")
+	flags.StringVar(&filetype, "filetype", "", "[compatible] specify a filetype for uploading to snippet. This option is maintained for compatibility. Please use -snippet-type instead.")
+	flags.StringVar(&filetype, "snippet-type", "", "specify a snippet_type (for uploading to snippet)")
 
 	flags.BoolVar(&snippetMode, "snippet", false, "switch to snippet uploading mode")