Repository: Ptt-official-app/Ptt-backend (go)

Issue:
[BUG] C6-1-1 發文時，在 raw 欄位的格式錯誤
### 發生了什麼事？ / The Problem
1. 在發出新文章後的 raw 應該要有發文時應有的 作者 標題 時間 等列，以及發信站等資訊，這些東西應該要在 Usecase 當中被設定，而不是交由 Client 自由設定，自由設定的部分應該要在編輯文章後才能做自由設定（但即便如此，最後面還是需要附加修改時間和修改IP等資訊）
2. 回傳的 raw 欄位的資料應該以 Base64 編碼，目前是沒有特別編碼的。


### 如何重現 / To Reproduce
請參考 testing-script 的 C6-1-1.sh
1. 取得可以在 test 版發文的 Access Token 權限
2. 送出 `curl -v -s http://localhost:8081/v1/boards/test/articles -H "Authorization: bearer $ACCESS_TOKEN" -d 'action=add_article' --data-urlencode 'title=[測試] test0904' --data-urlencode 'article=12345'`

可以看到回傳的結果 raw 欄位並沒有預期的作者等列，而且不是以 Base64 編碼

### 預期的行為 / Expected behavior
1. raw 回傳欄位應要有作者等列
https://www.ptt.cc/bbs/SYSOP/M.1577824363.A.5DA.html
![截圖 2021-09-05 下午11 13 16](https://user-images.githubusercontent.com/600238/132131840-8e774a0b-4174-416a-96fc-3e49f5ec91a2.png)

2. raw 欄位應以 base64 編碼

### 螢幕截圖 / Screenshots
![截圖 2021-09-05 下午11 10 06](https://user-images.githubusercontent.com/600238/132131752-e26d8416-5e6c-4ffa-85a4-74dd262cb95e.png)

### 開發環境 / Environment
Package version: cb8fb31f7a480dce1f29406be6c6d215d365c37e 

Pull request:
修正錯誤 C6-1-1
* 修正新增文章時 raw 函數沒有 encode 為 base64 格式

<!-- 原則上不接受沒有 Issue ID 的 PR。 -->
<!-- We don't accept PRs which has no Issue ID. -->

## 👏 解決掉的 issue / Resolved Issues
- close #283 

## ⛏ 變更內容 / Details of Changes
<!-- 簡要陳列您的修改 -->
<!-- List down your changes concisely -->
- 修正 `usecase.CreateArticle` 函數使其存成檔案時符合 ptt 原始格式
- 修正 `POST /v1/boards/test/articles` 時 raw 欄位不為 base64 encode 的問題

## 需要討論的部份
* 目前 raw 欄位是將字串 encode 成 big5 binary 再使用 base64 編碼的。

## 修改後的結果

```bash
$ TZ='Asia/Taipei' go run . &
$ curl -s http://localhost:8081/v1/boards/test/articles -H "Authorization: bearer $ACCESS_TOKEN" -d 'action=add_article' --data-urlencode 'title=[測試] test0904' --data-urlencode 'article=12345你好 hihi'
```
回傳
```
{
  "data": {
    "parsed": {
      "author_id": "SYSOP",
      "author_name": "SYSOP",
      "board_name": "test",
      "is_header_modied": false,
      "post_time": "09/18",
      "sender_info": {},
      "signature": {},
      "text": {
        "text": "12345你好 hihi"
      },
      "title": "[測試] test0904"
    },
    "raw": "5L2c6ICFOiBTWVNPUCAo56WeKSDnnIvmnb86IHRlc3QK5qiZ6aGMOiBb5ris6KmmXSB0ZXN0MDkwNArmmYLplpM6IFNhdCBTZXAgMTggMTg6Mzc6NTQgMjAyMQoKCjEyMzQ15L2g5aW9IGhpaGkKCi0tCuKAuyDnmbzkv6Hnq5k6IOaWsOaJuei4oui4oihwdHQyLmNjKSwg5L6G6IeqOiAzNi4yMzAuMjE0LjIyNArigLsg5paH56ug57ay5Z2AOiBodHRwOi8vd3d3LnB0dC5jYy9iYnMvdGVzdC9NLjE2MzE5NjE0NzQuQS4yMjEuaHRtbAo="
  }
}
```
並使用 `iconv` 確認檔案內容
```bash
$ iconv -f BIG5 -t UTF-8 M.1631961474.A.221
```
輸出
```
作者: SYSOP (神) 看板: test
標題: [測試] test0904
時間: Sat Sep 18 18:37:54 2021


12345你好 hihi

--
※ 發信站: 新批踢踢(ptt2.cc), 來自: 36.230.214.224
※ 文章網址: http://www.ptt.cc/bbs/test/M.1631961474.A.221.html
```

Patch:
diff --git a/internal/config/config.go b/internal/config/config.go
index 40cf4b2..2ef2371 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+	"github.com/Ptt-official-app/go-bbs"
 	"github.com/pelletier/go-toml"
 )
 
@@ -90,3 +91,24 @@ func applyConfig(config *Config, rawConfig *toml.Tree) {
 		config.MailDriver = s
 	}
 }
+
+// Ptt default article template
+type ArticleArguments struct {
+	UserData      bbs.UserRecord
+	Article       bbs.ArticleRecord
+	BoardID       string
+	Content       string
+	PostANSICDate string
+}
+
+const PttArticleTemplate = `作者: {{ .UserData.UserID }} ({{ .UserData.Nickname}}) 看板: {{ .BoardID }}
+標題: {{ .Article.Title }}
+時間: {{ .PostANSICDate }}
+
+
+{{ .Content }}
+
+--
+※ 發信站: 新批踢踢(ptt2.cc), 來自: {{ .UserData.LastHost }}
+※ 文章網址: http://www.ptt.cc/bbs/{{ .BoardID }}/{{ .Article.Filename }}.html
+`
diff --git a/internal/delivery/http/route_create_article.go b/internal/delivery/http/route_create_article.go
index d62865b..b09b9ef 100644
--- a/internal/delivery/http/route_create_article.go
+++ b/internal/delivery/http/route_create_article.go
@@ -2,6 +2,7 @@ package http
 
 import (
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -71,7 +72,7 @@ func (delivery *Delivery) publishPost(w http.ResponseWriter, r *http.Request, bo
 
 	responseMap := map[string]interface{}{
 		"data": map[string]interface{}{
-			"raw": raw,
+			"raw": base64.StdEncoding.EncodeToString([]byte(raw)),
 			"parsed": map[string]interface{}{
 				"is_header_modied": false,
 				"author_id":        userID,
diff --git a/internal/repository/article.go b/internal/repository/article.go
index 44fbbba..15b426c 100644
--- a/internal/repository/article.go
+++ b/internal/repository/article.go
@@ -1,10 +1,14 @@
 package repository
 
 import (
+	"bytes"
 	"context"
+	"errors"
 	"fmt"
+	"text/template"
 	"time"
 
+	"github.com/Ptt-official-app/Ptt-backend/internal/config"
 	"github.com/Ptt-official-app/go-bbs"
 )
 
@@ -84,7 +88,8 @@ func (repo *repository) AppendArticle(ctx context.Context, userID, boardID, titl
 // TODO: return result from bbs response
 func (repo *repository) CreateArticle(ctx context.Context, userID, boardID, title, content string) (bbs.ArticleRecord, error) {
 	// get file name
-	now := time.Now().Format("01/02")
+	currentTime := time.Now()
+	now := currentTime.Format("01/02")
 	record, err := repo.db.CreateArticleRecord(boardID, userID, now, title)
 	if err != nil {
 		fmt.Println("CreateArticleRecord error:", err)
@@ -97,7 +102,38 @@ func (repo *repository) CreateArticle(ctx context.Context, userID, boardID, titl
 		return nil, err
 	}
 
-	err = repo.db.WriteBoardArticleFile(boardID, record.Filename(), bbs.Utf8ToBig5(content))
+	var userData bbs.UserRecord = nil
+	records, err := repo.GetUsers(ctx)
+	if err != nil {
+		return nil, err
+	}
+	for _, record := range records {
+		if record.UserID() == userID {
+			userData = record
+		}
+	}
+	if userData == nil {
+		return nil, errors.New("user ID not found")
+	}
+
+	t, err := template.New("Ptt-article-template").Parse(config.PttArticleTemplate)
+	if err != nil {
+		return nil, err
+	}
+
+	buffer := bytes.NewBuffer(nil)
+	err = t.Execute(buffer, config.ArticleArguments{
+		UserData:      userData,
+		Article:       record,
+		BoardID:       boardID,
+		Content:       content,
+		PostANSICDate: currentTime.Format(time.ANSIC),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = repo.db.WriteBoardArticleFile(boardID, record.Filename(), bbs.Utf8ToBig5(buffer.String()))
 	if err != nil {
 		fmt.Println("WriteBoardArticleFile error: %w", err)
 		return nil, err


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index f4b545e..80b04fd 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,9 +1,38 @@
 package config
 
 import (
+	"bytes"
 	"testing"
+	"text/template"
+	"time"
 )
 
+type MockUserRecord struct {
+}
+
+func (record *MockUserRecord) UserID() string {
+	return "unknown"
+}
+
+func (record *MockUserRecord) Nickname() string {
+	return "無名"
+}
+
+func (record *MockUserRecord) LastHost() string {
+	return "172.17.0.3"
+}
+
+type MockArticleRecord struct {
+}
+
+func (article *MockArticleRecord) Title() string {
+	return "[說明] this is a test article 這是一篇測試文章"
+}
+
+func (article *MockArticleRecord) Filename() string {
+	return "M.1633153171.A.221"
+}
+
 func TestNewConfig(t *testing.T) {
 	actual, err := NewConfig("testcase/01.toml", "")
 	if err != nil {
@@ -23,4 +52,72 @@ func TestNewConfig(t *testing.T) {
 		t.Errorf("bbshome not match, expected: %v, got: %v", expected.BBSHome, actual.BBSHome)
 		return
 	}
+
+	temp, err := template.New("Ptt-article-template").Parse(PttArticleTemplate)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	boardID := "test"
+	content := "你好123 hi"
+	currentTime := time.Now()
+	buffer := bytes.NewBuffer(nil)
+	err = temp.Execute(buffer, ArticleArguments{
+		UserData:      &MockUserRecord{},
+		Article:       &MockArticleRecord{},
+		BoardID:       boardID,
+		Content:       content,
+		PostANSICDate: currentTime.Format(time.ANSIC),
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+// the following mock function is not used... for now.
+func (article *MockArticleRecord) Modified() time.Time {
+	return time.Now()
+}
+
+func (article *MockArticleRecord) SetModified(newModified time.Time) {
+}
+
+func (article *MockArticleRecord) Recommend() int {
+	return 0
+}
+
+func (article *MockArticleRecord) Date() string {
+	return ""
+}
+func (article *MockArticleRecord) Money() int {
+	return 0
+}
+func (article *MockArticleRecord) Owner() string {
+	return "nothing"
+}
+
+func (record *MockUserRecord) HashedPassword() string {
+	return ""
+}
+func (record *MockUserRecord) VerifyPassword(password string) error {
+	return nil
+}
+func (record *MockUserRecord) RealName() string {
+	return ""
+}
+func (record *MockUserRecord) NumLoginDays() int {
+	return 0
+}
+func (record *MockUserRecord) NumPosts() int {
+	return 0
+}
+func (record *MockUserRecord) Money() int {
+	return 0
+}
+func (record *MockUserRecord) LastLogin() time.Time {
+	return time.Now()
+}
+func (record *MockUserRecord) UserFlag() uint32 {
+	return 0
 }