diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..93572d9a --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/bin/sh +OSES="windows linux darwin" + +for os in $OSES; do + make slackdump-${os}.zip +done diff --git a/cmd/sdconv/sdconv.go b/cmd/sdconv/sdconv.go deleted file mode 100644 index 981d72b6..00000000 --- a/cmd/sdconv/sdconv.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "io/ioutil" - "log" - "os" - "regexp" - - "github.com/rusq/slackdump" -) - -const ( - outputTypeJSON = "json" - outputTypeText = "text" - - typeMessages = "Messages" - typeUsers = "Users" - typeChannels = "Channels" - - typeRegexp = `^{[\s\n]*"(Messages|Users|Channels)"` -) - -var outputType string -var ( - token string - cookie string -) -var downloadFlag bool -var re *regexp.Regexp - -func init() { - flag.StringVar(&outputType, "o", outputTypeText, "output `format`") - flag.BoolVar(&downloadFlag, "f", false, "download files using API token if SLACK_TOKEN environment variable is set") - re = regexp.MustCompile(typeRegexp) -} - -func main() { - flag.Parse() - - if downloadFlag { - token = os.Getenv("SLACK_TOKEN") - if token == "" { - log.Printf("file download requested but no token provided, skipping") - } - cookie = os.Getenv("COOKIE") - } - - input := os.Stdin - output := os.Stdout - - data, err := ioutil.ReadAll(input) - if err != nil { - log.Fatal(err) - } - - dataType := re.FindSubmatch(data) - if dataType == nil { - log.Fatal("can't determine entity") - } - entity := string(dataType[1]) - - var rep slackdump.Reporter - - switch entity { - case typeMessages: - var msgs slackdump.Messages - err = json.Unmarshal(data, &msgs) - if err == nil && token != "" { - log.Print("fetching files") - err = fetchFiles(&msgs, token, cookie) - } - msgs.SD.UpdateUserMap() - rep = msgs - case typeChannels: - var chans slackdump.Channels - err = json.Unmarshal(data, &chans) - rep = chans - case typeUsers: - var users slackdump.Users - err = json.Unmarshal(data, &users) - rep = users - } - if err != nil { - log.Fatal(err) - } - - if err = rep.ToText(output); err != nil { - log.Fatal(err) - } - -} - -func fetchFiles(m *slackdump.Messages, tokenID string, cookie string) error { - sd, err := slackdump.New(tokenID, cookie) - if err != nil { - return err - } - files := sd.GetFilesFromMessages(m) - return files.DumpToDir(m.ChannelID) -} diff --git a/cmd/slackdump/slackdump.go b/cmd/slackdump/slackdump.go index 0cd6ab64..172f9c27 100644 --- a/cmd/slackdump/slackdump.go +++ b/cmd/slackdump/slackdump.go @@ -10,6 +10,8 @@ import ( "log" "os" "os/signal" + "path/filepath" + "strings" "github.com/joho/godotenv" "github.com/rusq/slackdump" @@ -18,6 +20,9 @@ import ( const ( outputTypeJSON = "json" outputTypeText = "text" + + slackTokenEnv = "SLACK_TOKEN" + slackCookieEnv = "COOKIE" ) var _ = godotenv.Load() @@ -60,21 +65,8 @@ func (lf listFlags) present() bool { return lf.users || lf.channels } -func init() { - flag.Usage = func() { - fmt.Fprintf( - flag.CommandLine.Output(), - "Slackdump dumps messages and files from slack using the provided api token.\n"+ - "Will create a number of files having the channel_id as a name.\n"+ - "Files are downloaded into a respective folder with channel_id\n\n"+ - "Usage: %s [flags] [channel_id1 ... channel_idN]\n", - os.Args[0]) - flag.PrintDefaults() - } -} - func main() { - params, err := checkParameters() + params, err := checkParameters(os.Args[1:]) if err != nil { flag.Usage() log.Fatal(err) @@ -113,36 +105,48 @@ func createFile(filename string) (f io.WriteCloser, err error) { return os.Create(filename) } -func checkParameters() (params, error) { +func checkParameters(args []string) (params, error) { + fs := flag.NewFlagSet("", flag.ExitOnError) + fs.Usage = func() { + fmt.Fprintf( + flag.CommandLine.Output(), + "Slackdump dumps messages and files from slack using the provided api token.\n"+ + "Will create a number of files having the channel_id as a name.\n"+ + "Files are downloaded into a respective folder with channel_id name\n\n"+ + "Usage: %s [flags] [channel_id1 ... channel_idN]\n\n", + filepath.Base(os.Args[0])) + fs.PrintDefaults() + } + var p params - // flags - { - flag.BoolVar(&p.list.channels, "c", false, "list channels (aka conversations) and their IDs for export.") - flag.BoolVar(&p.list.users, "u", false, "list users and their IDs. ") - flag.BoolVar(&p.dumpFiles, "f", false, "enable files download") - flag.StringVar(&p.output.filename, "o", "-", "output `filename` for users and channels. Use '-' for standard\noutput.") - flag.StringVar(&p.output.format, "r", "", "report `format`. One of 'json' or 'text'") - flag.StringVar(&p.creds.token, "t", os.Getenv("SLACK_TOKEN"), "Specify slack `API_token`, get it here:\nhttps://api.slack.com/custom-integrations/legacy-tokens\n"+ - "It is also possible to define SLACK_TOKEN environment variable.") - flag.StringVar(&p.creds.cookie, "cookie", os.Getenv("COOKIE"), "d= cookie value") - flag.Parse() + fs.BoolVar(&p.list.channels, "c", false, "list channels (aka conversations) and their IDs for export.") + fs.BoolVar(&p.list.users, "u", false, "list users and their IDs. ") + fs.BoolVar(&p.dumpFiles, "f", false, "enable files download") + fs.StringVar(&p.output.filename, "o", "-", "output `filename` for users and channels. Use '-' for standard\noutput.") + fs.StringVar(&p.output.format, "r", "", "report `format`. One of 'json' or 'text'") + fs.StringVar(&p.creds.token, "t", os.Getenv(slackTokenEnv), "Specify slack `API_token`, (environment: "+slackTokenEnv+")") + fs.StringVar(&p.creds.cookie, "cookie", os.Getenv(slackCookieEnv), "d= cookie `value` (environment: "+slackCookieEnv+")") + fs.Parse(args) - os.Unsetenv("SLACK_TOKEN") - os.Unsetenv("COOKIE") + os.Unsetenv(slackTokenEnv) + os.Unsetenv(slackCookieEnv) - p.channelsToExport = flag.Args() - } + p.channelsToExport = fs.Args() + + return p, p.validate() +} +func (p *params) validate() error { if !p.creds.valid() { - return p, fmt.Errorf("slack token or cookie not specified") + return fmt.Errorf("slack token or cookie not specified") } if len(p.channelsToExport) == 0 && !p.list.present() { - return p, fmt.Errorf("no list flags specified and no channels to export") + return fmt.Errorf("no list flags specified and no channels to export") } - if !p.output.validFormat() { - return p, fmt.Errorf("invalid output type: %q, must use one of %v", p.output.format, []string{outputTypeJSON, outputTypeText}) + if !p.list.present() && !p.output.validFormat() { + return fmt.Errorf("invalid output type: %q, must use one of %v", p.output.format, []string{outputTypeJSON, outputTypeText}) } // channels and users listings will be in the text format (if not specified otherwise) @@ -153,8 +157,9 @@ func checkParameters() (params, error) { p.output.format = outputTypeJSON } } + p.creds.cookie = strings.TrimPrefix(p.creds.cookie, "d=") - return p, nil + return nil } func listEntities(ctx context.Context, output output, creds slackCreds, list listFlags) error { diff --git a/cmd/slackdump/slackdump_test.go b/cmd/slackdump/slackdump_test.go index cd5b3b54..dc0c58dc 100644 --- a/cmd/slackdump/slackdump_test.go +++ b/cmd/slackdump/slackdump_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func Test_output_validFormat(t *testing.T) { type fields struct { @@ -29,3 +33,60 @@ func Test_output_validFormat(t *testing.T) { }) } } + +func Test_checkParameters(t *testing.T) { + type args struct { + args []string + } + tests := []struct { + name string + args args + want params + wantErr bool + }{ + { + "channels", + args{[]string{"-c", "-t", "x", "-cookie", "d"}}, + params{ + list: listFlags{ + users: false, + channels: true, + }, + creds: slackCreds{ + token: "x", + cookie: "d", + }, + output: output{filename: "-"}, + channelsToExport: []string{}, + }, + false, + }, + { + "users", + args{[]string{"-u", "-t", "x", "-cookie", "d"}}, + params{ + list: listFlags{ + channels: false, + users: true, + }, + creds: slackCreds{ + token: "x", + cookie: "d", + }, + output: output{filename: "-"}, + channelsToExport: []string{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkParameters(tt.args.args) + if (err != nil) != tt.wantErr { + t.Errorf("checkParameters() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/messages.go b/messages.go index 10e11182..78da2dbb 100644 --- a/messages.go +++ b/messages.go @@ -9,16 +9,20 @@ import ( "github.com/slack-go/slack" ) -// Messages keeps slice of messages +// minMsgTimeApart defines the time interval in minutes to separate group +// of messages from a single user in the conversation. This increases the +// readability of the text output. +const minMsgTimeApart = 2 + +// Messages keeps the slice of messages. type Messages struct { Messages []slack.Message ChannelID string SD *SlackDumper } -// ToText outputs Messages m to io.Writer w in Text format -func (m Messages) ToText(w io.Writer) (err error) { - const minMsgTimeApart = 2 //minutes +// ToText outputs Messages m to io.Writer w in text format. +func (m *Messages) ToText(w io.Writer) (err error) { writer := bufio.NewWriter(w) defer writer.Flush() diff --git a/slackdump.go b/slackdump.go index f3c22999..0d424e07 100644 --- a/slackdump.go +++ b/slackdump.go @@ -2,6 +2,7 @@ package slackdump import ( "context" + "errors" "fmt" "io" "log" @@ -139,6 +140,17 @@ func (sd *SlackDumper) DumpMessages(ctx context.Context, channelID string, dumpF return &Messages{Messages: allMessages, ChannelID: channelID, SD: sd}, nil } +// ErrNoThread is the error indicating that error is not a threaded message. +var ErrNoThread = errors.New("message has no thread") + +// DumpThread retrieves all messages in the thread and returns them as a slice of messages. +func (sd *SlackDumper) DumpThread(m *slack.Message) ([]slack.Message, error) { + if m.ThreadTimestamp == "" { + return nil, ErrNoThread + } + panic("implement me") +} + // UpdateUserMap updates user[id]->*User mapping from the current Users slice. func (sd *SlackDumper) UpdateUserMap() error { if sd.Users.Len() == 0 {