cobraとhuhを使って毎日のニュースをチェックできるCLIアプリを作ってみるハンズオン

# はじめに
こんにちは！最近はGoを集中的に触っているのですが、Goを使うと簡単にCLIアプリを作れることを知りました。本記事では、**cobra**と**huh**を使い、毎日の最新ニュースを取得し、LINEに通知してストックするCLIアプリをハンズオン形式で作ってみたいと思います。

:::note
技術記事だけでなく、ちゃんとニュース記事も読むべし！というモチベーションです。
:::

# 実装するもの
![news-cli.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/28e84a20-a4cd-fc1c-ffda-4600d0bf2a1f.gif)

コードはこちらです。

https://github.com/ayanami77/mynews-cli

# 使用技術
- go 1.21
- cobra v1.8.0
- huh v0.3.0
- News API v2
- LINE Notify API

### cobraとは
cobraはCLIアプリケーションを作るためのGo製のライブラリです。k8sやHugoなどの有名OSSのCLIにも採用されています。
> Cobra is a library for creating powerful modern CLI applications.

https://github.com/spf13/cobra

### huhとは
huhはCLIアプリケーション上でインタラクティブなTUIを実現するためのライブラリです。テキスト入力やマルチセレクト、スピナーなどを提供しています。なお、[Charm](https://github.com/charmbracelet)というプロジェクトのひとつとして位置づけられています。
> A simple, powerful library for building interactive forms and prompts in the terminal.

https://github.com/charmbracelet/huh

### News APIとは
世界中の現在進行形のニュースを提供してくれるAPIです。例えば、リクエストを送ると次のようなレスポンスを得ることができます。
```json
{
    "status": "ok",
    "totalResults": 30,
    "articles": [
        {
            "source": {
                "id": null,
                "name": "Sanspo.com"
            },
            "author": "サンスポ",
            "title": "【速報します】森保ジャパン、決勝トーナメント１回戦でバーレーンと対戦 - サンスポ",
            "description": "サッカー・アジア杯決勝トーナメント１回戦（３１日、日本－バーレーン、ドーハ）３大会ぶり５度目のアジア王者を目指す世界ランキング１７位の日本代表「森保ジャパン」…",
            "url": "https://www.sanspo.com/article/20240131-UUL772OVCFEOTHXRW3XQKVSOCY/",
            "urlToImage": "https://www.sanspo.com/resizer/vPiJ-YLm4bm-Yp_qgbG1ONOa8SM=/1200x630/filters:focal(1206x672:1216x682):quality(50)/cloudfront-ap-northeast-1.images.arcpublishing.com/sankei/H2YSC3HMDZMD3PUT5RKNAIVOTA.jpg",
            "publishedAt": "2024-01-31T08:17:00Z",
            "content": null
        },
    ]
}
```
https://newsapi.org/

# プロジェクト構成
```
├── LICENSE
├── api
│   ├── news.go
│   └── news_response.go
├── cmd
│   ├── list.go
│   ├── root.go
│   └── version.go
├── config
│   └── config.go
├── go.mod
├── go.sum
├── main.go
└── model
    └── news.go
```
# 事前準備
今回は、News APIとLINE Notify APIを利用するので、APIキー・トークンを発行しておきましょう。

### 1. News APIのAPIキーを発行する
まず、[Japan News API](https://newsapi.org/s/japan-news-api)へ飛び、Get API keyをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/24931775-4bd9-548e-54f9-65602320f272.png)

個人情報の入力をして、`I am an individual`を選択し、フォームを送信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/07effc38-fc8a-cadb-bad8-5fa3d2b6c7df.png)
無事、APIキーを発行できたと思います。

### 2. Notify APIのトークンを発行する
[LINE Notify](https://notify-bot.line.me/ja/)へ飛び、右上のログインをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/c75e691d-6fea-915a-bca0-28e7a1715ce8.png)

ログインできたら、自分のアカウント名をクリックし、マイページへと飛びます。アクセストークンを発行しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/f060ea85-a336-b115-9e98-91f7aef8a62f.png)

トークルームは、私は「1:1でLINE Notifyから通知を受け取る」を選択しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/fb80fa26-4c09-9be8-c3af-7440905956ef.png)

これでトークンを発行することができたかと思います。
なお、Line Notifyの公式アカウントを追加していない場合は、こちらに従って追加してください。

https://help2.line.me/line_notify/web/pc?lang=ja&contentId=20003054

# 実装
### 1. プロジェクトの準備
go mod initで新しくGoモジュールを初期化します。
```bash
$ go mod init mynews
```
今回は、`cobra-cli`というcobraのセットアップを楽にするツールも導入していきます。
```bash
$ go install github.com/spf13/cobra-cli@latest
```
`cobra-cli`を使い、プロジェクトを初期化します。
```
$ cobra-cli init
```
現時点でこのような構成になっています。
```
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go
```
今回はversionコマンドとlistコマンドを実装するので、それらのファイルを追加していきます。
```bash
$ cobra-cli add version
$ cobra-cli add list
```
ここで一度、`main.go`を実行してみます。ここでは`root.go`の中身が表示されているはずです。
```bash
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  mynews [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  list        A brief description of your command
  version     A brief description of your command

Flags:
  -h, --help     help for mynews
  -t, --toggle   Help message for toggle

Use "mynews [command] --help" for more information about a command.
```
`root.go`の中身を軽く編集しておきます。
```go:cmd/root.go
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "mynews",
	Short: "mynews allows you to fetch current news articles and send them to your LINE app",
	Long:  `mynews is a command-line tool that allows you to fetch current news articles and send them to your LINE app. `,
}
```
それでは、実装を進めていきます。

### 2. versionコマンドを実装する
手始めにversionコマンドを実装してみます。とはいえ、中身の文字列を編集するだけです。
```go:cmd/version.go
var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of the mynews CLI tool",
	Long:  `Print the version number of the mynews CLI tool.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("v1.0.0")
	},
}
```
試しに、versionコマンドを実行してみます。特に問題はないはずです。
```bash
$ go run main.go version
v1.0.0
```
### 3. listコマンドを実装する ー API周りのロジックの記述
では、まず`model`というフォルダを新たに作成し、配下に`news.go`を作成してください。そこにnewsの構造体を定義しておきます。
```go:model/news.go
package model

type News struct {
	Author      string
	Title       string
	Description string
	URL         string
	PublishedAt string
}
```
次に、APIの取得周りを書いていきます。`config`というフォルダを作成し、その配下に`config.go`を作成します。ここに定数でAPIのエンドポイントや発行したキーを定義しておくことにします。（他の管理方法でも構いません）
```go:config/config.go
package config

const (
  NEWS_API_URI= "https://newsapi.org/v2"
  NEWS_API_KEY= "あなたの発行したAPIキー"
)
```
次に、`api`というフォルダを新たに作成し、配下に`news_response.go`を作成します。News APIのレスポンスを構造体で定義しておきます。
```go:api/news_response.go
package api

type NewsResponse struct {
	Status       string `json:"status"`
	TotalResults int    `json:"totalResults"`
	Articles     []struct {
		Author      string `json:"author"`
		Title       string `json:"title"`
		Description string `json:"description"`
		URL         string `json:"url"`
		PublishedAt string `json:"publishedAt"`
	} `json:"articles"`
 	Code    string `json:"code"`
	Message string `json:"message"`
}
```

さらに、`api`配下に`news.go`を作成し、データを取得する関数を記述します。
```go:api/news.go
package api

import (
...
)

func Fetch() (NewsResponse, error) {
	uri := config.NEWS_API_URI
	key := config.NEWS_API_KEY

	newsResponse := NewsResponse{}

    // Getリクエストを送信する
	res, err := http.Get(uri + fmt.Sprintf("/top-headlines?country=jp&apiKey=%s", key))
	if err != nil {
		return NewsResponse{}, err
	}
	defer res.Body.Close()

	byteArray, err := io.ReadAll(res.Body)
	if err != nil {
		return NewsResponse{}, err
	}

	if err := json.Unmarshal(byteArray, &newsResponse); err != nil {
		return NewsResponse{}, err
	}

	if newsResponse.Status != "ok" {
		return NewsResponse{}, fmt.Errorf("code: %s, message: %s", newsResponse.Code, newsResponse.Message)
	}

	return newsResponse, nil
}
```
ここまででNews APIを用いて、ニュース記事のデータを取得するロジックが実装できました。現時点でのプロジェクト構成は以下のようになります。
```
├── LICENSE
├── api
│   ├── news.go
│   └── news_response.go
├── cmd
│   ├── list.go
│   ├── root.go
│   └── version.go
├── config
│   └── config.go
├── go.mod
├── go.sum
├── main.go
└── model
    └── news.go
```

### 3. listコマンドの実装 ー TUIの実装
まずはいつものように中身の文字列を編集しますが、ここで`run`関数を用意しておきます。
```go:cmd/list.go
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Fetch and display the latest news articles",
	Long:  `Fetches and displays the latest news articles.`,
	Run: func(cmd *cobra.Command, args []string) {
		run()
	},
}

func run () {}
```
では、先ほど実装した`api/news.go`の`Fetch`関数を利用し、`run`関数の中身を記述していきます。
```go:cmd/list.go
func run() { 
	newsResponse, err := api.Fetch() // データを取得 
	if err != nil {
		fmt.Println(err)
		return
	}
	newsList := make([]model.News, 0, len(newsResponse.Articles))

	for _, v := range newsResponse.Articles {
		news := model.News{
			Author:      v.Author,
			Title:       v.Title,
			Description: v.Description,
			PublishedAt: v.PublishedAt,
			URL:         v.URL,
		}
		fmt.Printf("> %s\n", news.Title) // 試しにPrintfしてみます。
		newsList = append(newsList, news)
	}
}
```
だいぶ実装できてきましたね。listコマンドを実行してみましょう。
```bash
$ go run main.go list
> 異次元緩和に効果、「2年で2％」巡り議論＝13年下半期・日銀議事録 - ロイター (Reuters Japan)
> 明るい2大惑星・金星と木星。共に観測できるのは2月まで。2024年最小の満月は「スノームーン」 - tenki.jp
> 不正が相次ぐトヨタグループ 広がる不安で中古車市場にも影響が より一層厳しさを増す顧客の目 - CBCニュース【CBCテレビ公式】
> タイ最大野党に違憲判決 不敬罪見直しは「国家転覆」 - 日本経済新聞
以下続く...
```
ニュースが取得できて、表示されていれば上手くいっています。
ここでhuhの出番です。huhのパッケージをimportに追記し`go mod tidy`を実行します。
```diff_go:cmd/list.go
import (
	"fmt"
	"mynews/api"
	"mynews/model"

+	"github.com/charmbracelet/huh"
	"github.com/spf13/cobra"
)
```
```
$ go mod tidy
```
huhを使って、マルチセレクトを実装していきます。
```go:cmd/list.go
func multiSelectNews(newsList []model.News) ([]model.News, error) {
	options := make([]huh.Option[model.News], 0, len(newsList))
	for _, v := range newsList {
		options = append(options, huh.NewOption(v.Title, v))
	}

	selectedNewsList := []model.News{}

	form := huh.NewForm(
		huh.NewGroup(
			huh.NewMultiSelect[model.News]().
				Options(options...).
				Title("News").
				Value(&selectedNewsList).
				Validate(validateMultiSelect),
		),
	)

	err := form.Run()
	if err != nil {
		return []model.News{}, err
	}

	return selectedNewsList, nil
}

func validateMultiSelect(selectedNewsList []model.News) error {
	if len(selectedNewsList) == 0 {
		return errors.New("You must select at least 1 article.")
	}
	return nil
}

```

`cmd/list.go`に処理を追記します。

```diff_go:cmd/list.go
func run() {
	newsResponse, err := api.Fetch()
	if err != nil {
		fmt.Println(err)
		return
	}
	newsList := make([]model.News, 0, newsResponse.TotalResults)

	for _, v := range newsResponse.Articles {
		news := model.News{
			Author:      v.Author,
			Title:       v.Title,
			Description: v.Description,
			PublishedAt: v.PublishedAt,
			URL:         v.URL,
		}
-       fmt.Printf("> %s\n", news.Title)
		newsList = append(newsList, news)
	}

+	selectedNewsList, err := multiSelectNews(newsList)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(selectedNewsList) // // 試しにPrintlnしてみます。
}
```
では、listコマンドを実行し、複数選択した上でEnterしてみます。
```bash
$ go run main.go list
┃ News                                                                                          
┃ > ✓ 森保ジャパンで圧倒的存在感…毎熊晟矢、Jリーグから現れた”ワールドクラス”。バーレーン戦飛躍の舞台裏／レビュー - Goal.com                                                                              
┃   • 【ロッテ】吉井監督がキャンプインセレモニー出席「『ちむどんどん』するような野球を目指す」 - ニッカンスポーツ                                                                                         
┃   ✓ 名神高速 車に物が当たりフロントガラス割れる 男性が意識不明 | NHK - nhk.or.jp                 
┃   • 滞る設備投資、人手不足が影 コーセーやアサヒGHD - 日本経済新聞                
┃   ✓ 米ＦＲＢ、金利据え置き：識者はこうみる - ロイター (Reuters Japan)          
┃   • 映画「機動戦士ガンダムSEED FREEDOM」冒頭映像6分半が本日2月1日20時よりプレミア公開！ 新規カットを含む場面写も到着 - GAME Watch                                                  
┃   • ＡＬＳ嘱託殺人、医師に懲役２３年を求刑 - 読売新聞オンライン                  
┃   • 柿沢未途 前法務副大臣 衆議院に議員辞職願を提出 | NHK - nhk.or.jp            
┃   • 中村米吉が入籍を発表「おしゃべりと笑いに満ちた家庭を築いていきたい」 - ステージナタリー
┃   • 長野 宮田村 3年前の殺人未遂容疑で指名手配 暴力団幹部を逮捕 | NHK - nhk.or.jp
┃   • 『ブギウギ』水上恒司“笑顔”のクランクアップの裏側 愛助として最後の名演をCPが称賛 - リアルサウンド
┃   • Co-opシューター『HELLDIVERS 2』民主主義のため、敵も味方も儚く吹き飛ぶ過酷なミッショントレイラー【State of Play速報】 - Game*Spark
```

何かしらの出力結果が得られていれば、ここまでの実装に問題はありません。

ここまでの`cmd/list.go`と`api/news.go`の実装を示します。
```go:cmd/list.go
package cmd

import (
	"errors"
	"fmt"
	"mynews/api"
	"mynews/model"

	"github.com/charmbracelet/huh"
	"github.com/spf13/cobra"
)

// listCmd represents the list command
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Fetch and display the latest news articles",
	Long:  `Fetches and displays the latest news articles.`,
	Run: func(cmd *cobra.Command, args []string) {
		run()
	},
}

func run() {
	newsResponse, err := api.Fetch()
	if err != nil {
		fmt.Println(err)
		return
	}
	newsList := make([]model.News, 0, len(newsResponse.Articles))

	for _, v := range newsResponse.Articles {
		news := model.News{
			Author:      v.Author,
			Title:       v.Title,
			Description: v.Description,
			PublishedAt: v.PublishedAt,
			URL:         v.URL,
		}
		newsList = append(newsList, news)
	}

	selectedNewsList, err := multiSelectNews(newsList)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(selectedNewsList)
}

func multiSelectNews(newsList []model.News) ([]model.News, error) {
	options := make([]huh.Option[model.News], 0, len(newsList))
	for _, v := range newsList {
		options = append(options, huh.NewOption(v.Title, v))
	}

	selectedNewsList := []model.News{}

	form := huh.NewForm(
		huh.NewGroup(
			huh.NewMultiSelect[model.News]().
				Options(options...).
				Title("News").
				Value(&selectedNewsList).
				Validate(validateMultiSelect),
		),
	)

	err := form.Run()
	if err != nil {
		return []model.News{}, err
	}

	return selectedNewsList, nil
}

func validateMultiSelect(selectedNewsList []model.News) error {
	if len(selectedNewsList) == 0 {
		return errors.New("You should select at least 1 article.")
	}
	return nil
}

func init() {
	rootCmd.AddCommand(listCmd)
}
```

```go:api/news.go
package api

import (
	"encoding/json"
	"fmt"
	"io"
	"mynews/config"
	"net/http"
)

func Fetch() (NewsResponse, error) {
	uri := config.NEWS_API_URI
	key := config.NEWS_API_KEY

	newsResponse := NewsResponse{}

	res, err := http.Get(uri + fmt.Sprintf("/top-headlines?country=jp&apiKey=%s", key))
	if err != nil {
		return NewsResponse{}, err
	}
	defer res.Body.Close()

	byteArray, err := io.ReadAll(res.Body)
	if err != nil {
		return NewsResponse{}, err
	}

	if err := json.Unmarshal(byteArray, &newsResponse); err != nil {
		return NewsResponse{}, err
	}

	if newsResponse.Status != "ok" {
		return NewsResponse{}, fmt.Errorf("code: %s, message: %s", newsResponse.Code, newsResponse.Message)
	}

	return newsResponse, nil
}
```

### 4. listコマンドの実装 ー LINEに取得したニュース記事を送信する
ここまでお疲れさまです。最後にLINEへ選択したニュース記事をまとめて送信する実装を行います。

まず、`config/config.go`にAPIのエンドポイントや発行したトークンを追記します。
```diff_go:config/config.go
package config

const (
	NEWS_API_URI     = "https://newsapi.org/v2"
	NEWS_API_KEY     = "あなたの発行したAPIキー"
+   NOTIFY_API_URI   = "https://notify-api.line.me/api/notify"
+	NOTIFY_API_TOKEN = "あなたの発行したトークン"
)
```
続いて、Notify APIのレスポンスの構造体を`api/news_response.go`に追記しておきます。
```diff_go:api/news_response.go
package api

type NewsResponse struct {
	Status       string `json:"status"`
	TotalResults int    `json:"totalResults"`
	Articles     []struct {
		Author      string `json:"author"`
		Title       string `json:"title"`
		Description string `json:"description"`
		URL         string `json:"string"`
		PublishedAt string `json:"publishedAt"`
	} `json:"articles"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

+ type NotifyResponse struct {
+	Status int `json:"status"`
+}
```
`api/news.go`にLINEへのデータ送信のロジックを追記します。詳しいNotify APIの仕様はドキュメントに記載されていますので各自ご確認ください。

https://notify-bot.line.me/doc/ja/

```go:api/news.go
func Notify(newsList []model.News) error {
	message := ""
	for i, v := range newsList {
		content := fmt.Sprintf("\n[ %d ]----------\n%s\n日時: %s\n%s\n--------------\n", i+1, v.Title, v.PublishedAt, v.URL)
		message += content
	}

	form := url.Values{}
	form.Add("message", message)
	body := strings.NewReader(form.Encode())

    // リクエストを作成する
	uri := config.NOTIFY_API_URI
	req, err := http.NewRequest("POST", uri, body)
	if err != nil {
		return err
	}
    
    // アクセストークンをヘッダにセットする
	accessToken := config.NOTIFY_API_TOKEN
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client := http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	byteArray, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}

	response := NotifyResponse{}

	err = json.Unmarshal(byteArray, &response)
	if err != nil {
		return err
	}

	err = checkStatus(response.Status)
	if err != nil {
		return err
	}
	return nil
}

func checkStatus(status int) error {
	switch status {
	case http.StatusOK:
		return nil
	case http.StatusBadRequest:
		return errors.New("Bad request")
	case http.StatusUnauthorized:
		return errors.New("Invalid access token")
	case http.StatusInternalServerError:
		return errors.New("Server-side error occurred")
	default:
		return errors.New("Unknown status code received")
	}
}
```

先ほど実装したapi/news.goの`Notify`関数を`cmd/list.go`の`run`関数に追記します。

```diff_go:cmd/list.go
func run() {
	newsResponse, err := api.Fetch()
	if err != nil {
		fmt.Println(err)
		return
	}
	newsList := make([]model.News, 0, len(newsResponse.Articles))

	for _, v := range newsResponse.Articles {
		news := model.News{
			Author:      v.Author,
			Title:       v.Title,
			Description: v.Description,
			PublishedAt: v.PublishedAt,
			URL:         v.URL,
		}
		newsList = append(newsList, news)
	}

	selectedNewsList, err := multiSelectNews(newsList)
	if err != nil {
		fmt.Println(err)
		return
	}
-   fmt.Println(selectedNewsList)

+	err = api.Notify(selectedNewsList)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println("Notify to your app!")
}
```
では、listコマンドを実行してみましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/2acf9f95-46de-0183-45e9-568a7b92ac76.png)

このようにLINE側に送信できていれば、問題ありません。お疲れさまでした！！！

最後に、`cmd/list.go`と`api/news.go`の完成コードを示しておきます。

```go:cmd/list.go
package cmd

import (
	"errors"
	"fmt"
	"mynews/api"
	"mynews/model"

	"github.com/charmbracelet/huh"
	"github.com/spf13/cobra"
)

// listCmd represents the list command
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Fetch and display the latest news articles",
	Long:  `Fetches and displays the latest news articles.`,
	Run: func(cmd *cobra.Command, args []string) {
		run()
	},
}

func run() {
	newsResponse, err := api.Fetch()
	if err != nil {
		fmt.Println(err)
		return
	}
	newsList := make([]model.News, 0, len(newsResponse.Articles))

	for _, v := range newsResponse.Articles {
		news := model.News{
			Author:      v.Author,
			Title:       v.Title,
			Description: v.Description,
			PublishedAt: v.PublishedAt,
			URL:         v.URL,
		}
		newsList = append(newsList, news)
	}

	selectedNewsList, err := multiSelectNews(newsList)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = api.Notify(selectedNewsList)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Notify to your line app!")
}

func multiSelectNews(newsList []model.News) ([]model.News, error) {
	options := make([]huh.Option[model.News], 0, len(newsList))
	for _, v := range newsList {
		options = append(options, huh.NewOption(v.Title, v))
	}

	selectedNewsList := []model.News{}

	form := huh.NewForm(
		huh.NewGroup(
			huh.NewMultiSelect[model.News]().
				Options(options...).
				Title("News").
				Value(&selectedNewsList).
				Validate(validateMultiSelect),
		),
	)

	err := form.Run()
	if err != nil {
		return []model.News{}, err
	}

	return selectedNewsList, nil
}

func validateMultiSelect(selectedNewsList []model.News) error {
	if len(selectedNewsList) == 0 {
		return errors.New("You should select at least 1 article.")
	}
	return nil
}

func init() {
	rootCmd.AddCommand(listCmd)
}
```

```go:api/news.go
package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mynews/config"
	"mynews/model"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func Fetch() (NewsResponse, error) {
	uri := config.NEWS_API_URI
	key := config.NEWS_API_KEY

	newsResponse := NewsResponse{}

	res, err := http.Get(uri + fmt.Sprintf("/top-headlines?country=jp&apiKey=%s", key))
	if err != nil {
		return NewsResponse{}, err
	}
	defer res.Body.Close()

	byteArray, err := io.ReadAll(res.Body)
	if err != nil {
		return NewsResponse{}, err
	}

	if err := json.Unmarshal(byteArray, &newsResponse); err != nil {
		return NewsResponse{}, err
	}

	if newsResponse.Status != "ok" {
		return NewsResponse{}, fmt.Errorf("code: %s, message: %s", newsResponse.Code, newsResponse.Message)
	}

	return newsResponse, nil
}

func Notify(newsList []model.News) error {
	message := ""
	for i, v := range newsList {
		content := fmt.Sprintf("\n[ %d ]----------\n%s\n日時: %s\n%s\n--------------\n", i+1, v.Title, v.PublishedAt, v.URL)
		message += content
	}

	form := url.Values{}
	form.Add("message", message)
	body := strings.NewReader(form.Encode())

	uri := config.NOTIFY_API_URI
	req, err := http.NewRequest("POST", uri, body)
	if err != nil {
		return err
	}

	accessToken := config.NOTIFY_API_TOKEN
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client := http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	byteArray, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}

	response := NotifyResponse{}

	err = json.Unmarshal(byteArray, &response)
	if err != nil {
		return err
	}

	err = checkStatus(response.Status)
	if err != nil {
		return err
	}
	return nil
}

func checkStatus(status int) error {
	switch status {
	case http.StatusOK:
		return nil
	case http.StatusBadRequest:
		return errors.New("Bad request")
	case http.StatusUnauthorized:
		return errors.New("Invalid access token")
	case http.StatusInternalServerError:
		return errors.New("Server-side error occurred")
	default:
		return errors.New("Unknown status code received")
	}
}
```

# おわりに
本記事では、cobraとhuhを使って毎日のニュースをチェックできるCLIアプリを作ってみました。いかがだったでしょうか。また、筆者が記事を書き慣れておらず拙い文章で、分かりづらい箇所もあったかと思います。何かご指摘等あれば、頂きたいです。ここまで読んでくださった方は、ありがとうございました！