【DiscordGo】Embedにローカルの画像を添付する + 僕が詰まったこと

## 目標
- Embedにローカルで保存されている画像を使う

## やること
- メッセージが作成されたとき、ローカル画像を添付したEmbedを送信する

## 使うもの
- [Go](https://go.dev)
- [DiscordGo](https://github.com/bwmarrin/discordgo) - Go言語でDiscord botを作るパッケージ

※環境構築は割愛

## ディレクトリ構成

```
project_root/
  ├ main.go
  ├ utils
  │   └ file.go - package utils
  └ images
      ├ image_1.webp
      └ thumbnail_1.webp
```

## 要点
一部を抜粋しています。実装は[次の見出しを参照](#実装)
### 1. 作業ディレクトリを基準に画像への絶対パスを作る。
```go:utils/file.go
// 現在の作業ディレクトリを取得
currentDir, err := os.Getwd()
if err != nil {
	return nil, "", "", fmt.Errorf("error getting current working directory: %w", err)
}

// 作業ディレクトリを基準にファイルの絶対パスを作成
log.Printf("cd: %s", currentDir)
absFilePath := fmt.Sprintf("%s/images/%s", currentDir, fileName)
```
:::note info
Linuxのsystemdでbotを常駐させるときの**WorkingDirectory**にあたります。
```cmd:/etc/systemd/system/your_service.service
[Service]
WorkingDirectory=/home/your_user/project_root/
:::

### 2. ファイルを読み込む
```go:utils/fie.go
file, err := os.Open(absFilePath) // 生成した絶対パス
contentType := "image/webp"
 ```

### 3. 使用時に関数を呼び出す
### 4. `discordgo.File{}`を作る
```go:main.go
// 1. *os.Fileを取得
imageFile, contentType, err := utils.PrepareFile(imageFileName)
if err != nil {
    log.Printf("Error preparing file: %v", err)
    return
}

// 2. discordgo.File{}を作成
imageDiscordFile := &discordgo.File{
    Name: imagePath, 
    ContentType:contentType, 
    Reader: imageFile,
}
```

### 5. `s.ChannelMessageSendComplex`を使って送信する
```go:main.go
// 送信するデータを作成
sendData := &discordgo.MessageSend{
    Embed: embed,
    Files: []*discordgo.File{
    	imageDiscordFile,
    	thumbnailDiscordFile,
    },
}

// 送信
s.ChannelMessageSendComplex("your channelID", sendData)
```
普通のEmbedを送信する際の`s.ChannelMessageSendEmbed()`ではありません。



## 実装
以下の順に記載
1.  utils/file.go
2.  main.go
```go:utils/file.go
package utils

// ファイルを*os.Fileとして返す関数
func PrepareFile(fileName string) (*os.File, string, string, error) {
	// 現在の作業ディレクトリを取得
	currentDir, err := os.Getwd()
	if err != nil {
		return nil, "", "", fmt.Errorf("error getting current working directory: %w", err)
	}

	// 作業ディレクトリを基準にファイルの絶対パスを作成
	log.Printf("cd: %s", currentDir)
	absFilePath := fmt.Sprintf("%s/images/%s", currentDir, fileName)

	// ファイルを開く
	file, err := os.Open(absFilePath)
	if err != nil {
		return nil, "", "", fmt.Errorf("error opening file: %w", err)
	} else {
		log.Printf("successful open file")
	}
 
    // Mineタイプを設定する。拡張子が複数ある時はここで条件分岐
	contentType := "image/webp"

	// *os.File, MIMEタイプを返す
	return file, contentType, nil
}
```


```go:main.go
package main

func main() {
    // セッションを作成する
    session, err := discordgo.New("Bot " + "your token")
    // ...略

    // ハンドラを登録する
    // 第二引数でトリガーを指定(今回は「メッセージが作成されたとき」)
    session.AddHandler(func (s *discordgo.Session, m *discordgo.MessageCreate) {

        imageFileName := "image_1.webp"
        thumbnailFileName := "thumbnail_1.webp"

        // 1. *os.Fileを取得
    	imageFile, contentType, err := utils.PrepareFile(imageFileName)
    	if err != nil {
    		log.Printf("Error preparing file: %v", err)
    		return
    	}

        // 2. discordgo.File{}を作成
    	imageDiscordFile := &discordgo.File{
    		Name: imagePath, 
    		ContentType:contentType, 
    		Reader: imageFile,
    	}

        // 1. 以下同じ
    	thumbnailFile, contentType, err := utils.PrepareFile(thumbnailFileName)
    	if err != nil {
    		log.Printf("Error preparing file: %v", err)
    		return
    	}
     
        // 2
    	thumbnailDiscordFile := &discordgo.File{
    		Name: thumbnailPath, 
    		ContentType:contentType, 
    		Reader: thumbnailFile,
    	}
     
		embed := &discordgo.MessageEmbed{
    		Type: "rich",
    		Title: "タイトル"
    		Color: (255 << 16) | (255 << 8) | 0, // R|G|B のシフト演算で指定 
    		Image: &discordgo.MessageEmbedImage{
    			URL: fmt.Sprintf("attachment://%s", imageFileName),	// 本題
    		},
    		Thumbnail: &discordgo.MessageEmbedThumbnail{
    			URL: fmt.Sprintf("attachment://%s", thumbnailFileName), // 本題
    		},
    	}

        // 送信するデータを作成
        sendData := &discordgo.MessageSend{
    		Embed: embed,
    		Files: []*discordgo.File{
    			imageDiscordFile,
    			thumbnailDiscordFile,
    		},
    	}

        // 送信
        s.ChannelMessageSendComplex("your channelID", sendData)
        // × s.ChannelMessageSendEmbed()
})

    // ...略
     
}
```

## 筆者が遭遇したエラー
### `*bytes.Buffer`を使わない
はじめ、`PrepareFile()`では`*bytes.Buffer`を返していた。
```go: discordgo.File{}の定義
// File stores info about files you e.g. send in messages.
type File struct {
	Name        string
	ContentType string
	Reader      io.Reader
}
```
`Reader`に`*bytes.Buffer`を渡してもエラーは発生しないが、Embedに画像が表示されなかった。`PrepareFile()`の戻り値を`*os.File`にして`Reader`に渡したところ、画像が表示された。chatGPTなどがBufferを使ったコードを書くことがあるので、画像が表示されない場合は注意してほしい。