Skip to content

Commit 64024c9

Browse files
committed
another implementation of "fetch-messages"
1 parent 6b24402 commit 64024c9

File tree

6 files changed

+258
-0
lines changed

6 files changed

+258
-0
lines changed

internal/jsonwriter/jsonwriter.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package jsonwriter
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
)
7+
8+
// WriteCloser writes objects as JSON array. It provides persistent layer for JSON value.
9+
type WriteCloser interface {
10+
Write(interface{}) error
11+
Close() error
12+
}
13+
14+
type fileWriter struct {
15+
name string
16+
reverse bool
17+
18+
// FIXME: いったん全部メモリにためるのであんまよくない
19+
buf []interface{}
20+
}
21+
22+
func (fw *fileWriter) Write(v interface{}) error {
23+
// XXX: 排他してないのでgoroutineからは使えない
24+
fw.buf = append(fw.buf, v)
25+
return nil
26+
}
27+
28+
func (fw *fileWriter) Close() error {
29+
// FIXME: ファイルの作成が Close まで遅延している。本来なら CreateFile のタ
30+
// イミングでやるのが好ましいが、いましばらく目を瞑る
31+
f, err := os.Create(fw.name)
32+
if err != nil {
33+
return err
34+
}
35+
defer f.Close()
36+
if fw.reverse {
37+
reverse(fw.buf)
38+
fw.reverse = false
39+
}
40+
err = json.NewEncoder(f).Encode(fw.buf)
41+
if err != nil {
42+
return err
43+
}
44+
fw.buf = nil
45+
return nil
46+
}
47+
48+
// CreateFile creates a WriteCloser which implemented by file.
49+
func CreateFile(name string, reverse bool) (WriteCloser, error) {
50+
return &fileWriter{name: name}, nil
51+
}
52+
53+
func reverse(x []interface{}) {
54+
for i, j := 0, len(x)-1; i < j; {
55+
x[i], x[j] = x[j], x[i]
56+
i++
57+
j--
58+
}
59+
}

internal/slackadapter/common.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package slackadapter
2+
3+
// Error represents error response of Slack.
4+
type Error struct {
5+
Ok bool `json:"ok"`
6+
Err string `json:"error"`
7+
}
8+
9+
// Error returns error message.
10+
func (err *Error) Error() string {
11+
return err.Err
12+
}
13+
14+
// NextCursor is cursor for next request.
15+
type NextCursor struct {
16+
NextCursor Cursor `json:"next_cursor"`
17+
}
18+
19+
// Cursor is type of cursor of Slack API.
20+
type Cursor string
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package slackadapter
2+
3+
import (
4+
"context"
5+
"errors"
6+
"time"
7+
8+
"github.com/vim-jp/slacklog-generator/internal/slacklog"
9+
)
10+
11+
// ConversationsHistoryParams is optional parameters for ConversationsHistory
12+
type ConversationsHistoryParams struct {
13+
Cursor Cursor `json:"cursor,omitempty"`
14+
Inclusive bool `json:"inclusive,omitempty"`
15+
Latest *time.Time `json:"latest,omitempty"`
16+
Limit int `json:"limit,omitempty"`
17+
Oldest *time.Time `json:"oldest,omitempty"`
18+
}
19+
20+
// ConversationsHistoryReponse is response for ConversationsHistory
21+
type ConversationsHistoryReponse struct {
22+
Ok bool `json:"ok"`
23+
Messages []*slacklog.Message `json:"messages,omitempty"`
24+
HasMore bool `json:"has_more"`
25+
PinCount int `json:"pin_count"`
26+
ResponseMetadata *NextCursor `json:"response_metadata"`
27+
}
28+
29+
// ConversationsHistory gets conversation messages in a channel.
30+
func ConversationsHistory(ctx context.Context, token, channel string, params ConversationsHistoryParams) (*ConversationsHistoryReponse, error) {
31+
// TODO: call Slack's conversations.history
32+
return nil, errors.New("not implemented yet")
33+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package slackadapter
2+
3+
import "context"
4+
5+
// CursorIterator is requirements of IterateCursor iterates with cursor.
6+
type CursorIterator interface {
7+
Iterate(context.Context, Cursor) (Cursor, error)
8+
}
9+
10+
// CursorIteratorFunc is a function which implements CursorIterator.
11+
type CursorIteratorFunc func(context.Context, Cursor) (Cursor, error)
12+
13+
// Iterate is an implementation for CursorIterator.
14+
func (fn CursorIteratorFunc) Iterate(ctx context.Context, c Cursor) (Cursor, error) {
15+
return fn(ctx, c)
16+
}
17+
18+
// IterateCursor iterates CursorIterator until returning empty cursor.
19+
func IterateCursor(ctx context.Context, iter CursorIterator) error {
20+
var c Cursor
21+
for {
22+
err := ctx.Err()
23+
if err != nil {
24+
return err
25+
}
26+
next, err := iter.Iterate(ctx, c)
27+
if err != nil {
28+
return err
29+
}
30+
if next == Cursor("") {
31+
return nil
32+
}
33+
c = next
34+
}
35+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package fetchmessages
2+
3+
import (
4+
"context"
5+
"errors"
6+
"flag"
7+
"os"
8+
"path/filepath"
9+
"time"
10+
11+
"github.com/vim-jp/slacklog-generator/internal/jsonwriter"
12+
"github.com/vim-jp/slacklog-generator/internal/slackadapter"
13+
"github.com/vim-jp/slacklog-generator/internal/slacklog"
14+
)
15+
16+
const dateFormat = "2006-01-02"
17+
18+
func toDateString(ti time.Time) string {
19+
return ti.Format(dateFormat)
20+
}
21+
22+
func parseDateString(s string) (time.Time, error) {
23+
l, err := time.LoadLocation("Asia/Tokeyo")
24+
if err != nil {
25+
return time.Time{}, err
26+
}
27+
ti, err := time.ParseInLocation(dateFormat, s, l)
28+
if err != nil {
29+
return time.Time{}, err
30+
}
31+
return ti, nil
32+
}
33+
34+
// Run runs "fetch-messages" sub-command. It fetch messages of a channel by a
35+
// day.
36+
func Run(args []string) error {
37+
var (
38+
token string
39+
datadir string
40+
date string
41+
verbose bool
42+
)
43+
fs := flag.NewFlagSet("fetch-messages", flag.ExitOnError)
44+
fs.StringVar(&token, "token", os.Getenv("SLACK_TOKEN"), `slack token. can be set by SLACK_TOKEN env var`)
45+
fs.StringVar(&datadir, "datadir", "_logdata", `directory to load/save data`)
46+
fs.StringVar(&date, "date", toDateString(time.Now()), `target date to get`)
47+
fs.BoolVar(&verbose, "verbose", false, "verbose log")
48+
err := fs.Parse(args)
49+
if err != nil {
50+
return err
51+
}
52+
if token == "" {
53+
return errors.New("SLACK_TOKEN environment variable requied")
54+
}
55+
oldest, err := parseDateString(date)
56+
if err != nil {
57+
return err
58+
}
59+
latest := oldest.AddDate(0, 0, 1)
60+
61+
ct, err := slacklog.NewChannelTable(filepath.Join(datadir, "channels.json"), []string{"*"})
62+
if err != nil {
63+
return err
64+
}
65+
66+
for _, sch := range ct.Channels {
67+
outfile := filepath.Join(datadir, sch.ID, toDateString(oldest)+".json")
68+
fw, err := jsonwriter.CreateFile(outfile, true)
69+
if err != nil {
70+
return err
71+
}
72+
err = slackadapter.IterateCursor(context.Background(),
73+
slackadapter.CursorIteratorFunc(func(ctx context.Context, c slackadapter.Cursor) (slackadapter.Cursor, error) {
74+
r, err := slackadapter.ConversationsHistory(ctx, token, sch.ID, slackadapter.ConversationsHistoryParams{
75+
Cursor: c,
76+
Limit: 100,
77+
Oldest: &oldest,
78+
Latest: &latest,
79+
})
80+
if err != nil {
81+
return "", err
82+
}
83+
for _, m := range r.Messages {
84+
err := fw.Write(m)
85+
if err != nil {
86+
return "", err
87+
}
88+
}
89+
if m := r.ResponseMetadata; r.HasMore && m != nil {
90+
return m.NextCursor, nil
91+
}
92+
// HasMore && ResponseMetadata == nil は明らかにエラーだがいま
93+
// は握りつぶしてる
94+
return "", nil
95+
}))
96+
if err != nil {
97+
fw.Close()
98+
return err
99+
}
100+
err = fw.Close()
101+
if err != nil {
102+
return err
103+
}
104+
}
105+
106+
return nil
107+
}

subcmd/subcmd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package subcmd
33
import (
44
"fmt"
55
"os"
6+
7+
"github.com/vim-jp/slacklog-generator/subcmd/fetchmessages"
68
)
79

810
func Run() error {
@@ -28,6 +30,8 @@ func Run() error {
2830
return DownloadFiles(args)
2931
case "generate-html":
3032
return GenerateHTML(args)
33+
case "fetch-messages":
34+
return fetchmessages.Run(args)
3135
}
3236

3337
return fmt.Errorf("unknown subcmd: %s", subCmdName)

0 commit comments

Comments
 (0)