Skip to content

Commit fb10089

Browse files
committed
another implementation of "fetch-messages"
1 parent 1a020d7 commit fb10089

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-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+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/joho/godotenv"
88
cli "github.com/urfave/cli/v2"
99
"github.com/vim-jp/slacklog-generator/subcmd"
10+
"github.com/vim-jp/slacklog-generator/subcmd/fetchmessages"
1011
"github.com/vim-jp/slacklog-generator/subcmd/serve"
1112
)
1213

@@ -27,6 +28,7 @@ func main() {
2728
subcmd.DownloadFilesCommand, // "download-files"
2829
subcmd.GenerateHTMLCommand, // "generate-html"
2930
serve.Command, // "serve"
31+
fetchmessages.NewCLICommand(), // "fetch-messages"
3032
}
3133

3234
err = app.Run(os.Args)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package fetchmessages
2+
3+
import (
4+
"context"
5+
"errors"
6+
"flag"
7+
"os"
8+
"path/filepath"
9+
"time"
10+
11+
cli "github.com/urfave/cli/v2"
12+
"github.com/vim-jp/slacklog-generator/internal/jsonwriter"
13+
"github.com/vim-jp/slacklog-generator/internal/slackadapter"
14+
"github.com/vim-jp/slacklog-generator/internal/slacklog"
15+
)
16+
17+
const dateFormat = "2006-01-02"
18+
19+
func toDateString(ti time.Time) string {
20+
return ti.Format(dateFormat)
21+
}
22+
23+
func parseDateString(s string) (time.Time, error) {
24+
l, err := time.LoadLocation("Asia/Tokeyo")
25+
if err != nil {
26+
return time.Time{}, err
27+
}
28+
ti, err := time.ParseInLocation(dateFormat, s, l)
29+
if err != nil {
30+
return time.Time{}, err
31+
}
32+
return ti, nil
33+
}
34+
35+
// Run runs "fetch-messages" sub-command. It fetch messages of a channel by a
36+
// day.
37+
func Run(args []string) error {
38+
var (
39+
token string
40+
datadir string
41+
date string
42+
verbose bool
43+
)
44+
fs := flag.NewFlagSet("fetch-messages", flag.ExitOnError)
45+
fs.StringVar(&token, "token", os.Getenv("SLACK_TOKEN"), `slack token. can be set by SLACK_TOKEN env var`)
46+
fs.StringVar(&datadir, "datadir", "_logdata", `directory to load/save data`)
47+
fs.StringVar(&date, "date", toDateString(time.Now()), `target date to get`)
48+
fs.BoolVar(&verbose, "verbose", false, "verbose log")
49+
err := fs.Parse(args)
50+
if err != nil {
51+
return err
52+
}
53+
if token == "" {
54+
return errors.New("SLACK_TOKEN environment variable requied")
55+
}
56+
return run(token, datadir, date, verbose)
57+
}
58+
59+
func run(token, datadir, date string, verbose bool) error {
60+
oldest, err := parseDateString(date)
61+
if err != nil {
62+
return err
63+
}
64+
latest := oldest.AddDate(0, 0, 1)
65+
66+
ct, err := slacklog.NewChannelTable(filepath.Join(datadir, "channels.json"), []string{"*"})
67+
if err != nil {
68+
return err
69+
}
70+
71+
for _, sch := range ct.Channels {
72+
outfile := filepath.Join(datadir, sch.ID, toDateString(oldest)+".json")
73+
fw, err := jsonwriter.CreateFile(outfile, true)
74+
if err != nil {
75+
return err
76+
}
77+
err = slackadapter.IterateCursor(context.Background(),
78+
slackadapter.CursorIteratorFunc(func(ctx context.Context, c slackadapter.Cursor) (slackadapter.Cursor, error) {
79+
r, err := slackadapter.ConversationsHistory(ctx, token, sch.ID, slackadapter.ConversationsHistoryParams{
80+
Cursor: c,
81+
Limit: 100,
82+
Oldest: &oldest,
83+
Latest: &latest,
84+
})
85+
if err != nil {
86+
return "", err
87+
}
88+
for _, m := range r.Messages {
89+
err := fw.Write(m)
90+
if err != nil {
91+
return "", err
92+
}
93+
}
94+
if m := r.ResponseMetadata; r.HasMore && m != nil {
95+
return m.NextCursor, nil
96+
}
97+
// HasMore && ResponseMetadata == nil は明らかにエラーだがいま
98+
// は握りつぶしてる
99+
return "", nil
100+
}))
101+
if err != nil {
102+
// ロールバック相当が好ましいが今はまだその時期ではない
103+
fw.Close()
104+
return err
105+
}
106+
err = fw.Close()
107+
if err != nil {
108+
return err
109+
}
110+
}
111+
112+
return nil
113+
}
114+
115+
// NewCLICommand creates a cli.Command, which provides "fetch-messages"
116+
// sub-command.
117+
func NewCLICommand() *cli.Command {
118+
var (
119+
token string
120+
datadir string
121+
date string
122+
verbose bool
123+
)
124+
return &cli.Command{
125+
Name: "fetch-messages",
126+
Usage: "fetch messages of channel by day",
127+
Action: func(c *cli.Context) error {
128+
return run(token, datadir, date, verbose)
129+
},
130+
Flags: []cli.Flag{
131+
&cli.StringFlag{
132+
Name: "token",
133+
Usage: "slack token. can be set by SLACK_TOKEN env var",
134+
EnvVars: []string{"SLACK_TOKEN"},
135+
Destination: &token,
136+
},
137+
&cli.StringFlag{
138+
Name: "datadir",
139+
Usage: "directory to load/save data",
140+
Value: "_logdata",
141+
Destination: &datadir,
142+
},
143+
&cli.StringFlag{
144+
Name: "date",
145+
Usage: "target date to get",
146+
Value: toDateString(time.Now()),
147+
Destination: &date,
148+
},
149+
&cli.BoolFlag{
150+
Name: "verbose",
151+
Usage: "verbose log",
152+
Destination: &verbose,
153+
},
154+
},
155+
}
156+
157+
}

0 commit comments

Comments
 (0)