diff --git a/clienter_mock_test.go b/clienter_mock_test.go index 0473d2b3..2a56e299 100644 --- a/clienter_mock_test.go +++ b/clienter_mock_test.go @@ -183,6 +183,36 @@ func (mr *MockSlackerMockRecorder) ListBookmarks(channelID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBookmarks", reflect.TypeOf((*MockSlacker)(nil).ListBookmarks), channelID) } +// SearchFilesContext mocks base method. +func (m *MockSlacker) SearchFilesContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchFiles, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchFilesContext", ctx, query, params) + ret0, _ := ret[0].(*slack.SearchFiles) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchFilesContext indicates an expected call of SearchFilesContext. +func (mr *MockSlackerMockRecorder) SearchFilesContext(ctx, query, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchFilesContext", reflect.TypeOf((*MockSlacker)(nil).SearchFilesContext), ctx, query, params) +} + +// SearchMessagesContext mocks base method. +func (m *MockSlacker) SearchMessagesContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchMessages, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchMessagesContext", ctx, query, params) + ret0, _ := ret[0].(*slack.SearchMessages) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchMessagesContext indicates an expected call of SearchMessagesContext. +func (mr *MockSlackerMockRecorder) SearchMessagesContext(ctx, query, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMessagesContext", reflect.TypeOf((*MockSlacker)(nil).SearchMessagesContext), ctx, query, params) +} + // mockClienter is a mock of clienter interface. type mockClienter struct { ctrl *gomock.Controller @@ -397,3 +427,33 @@ func (mr *mockClienterMockRecorder) ListBookmarks(channelID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBookmarks", reflect.TypeOf((*mockClienter)(nil).ListBookmarks), channelID) } + +// SearchFilesContext mocks base method. +func (m *mockClienter) SearchFilesContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchFiles, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchFilesContext", ctx, query, params) + ret0, _ := ret[0].(*slack.SearchFiles) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchFilesContext indicates an expected call of SearchFilesContext. +func (mr *mockClienterMockRecorder) SearchFilesContext(ctx, query, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchFilesContext", reflect.TypeOf((*mockClienter)(nil).SearchFilesContext), ctx, query, params) +} + +// SearchMessagesContext mocks base method. +func (m *mockClienter) SearchMessagesContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchMessages, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchMessagesContext", ctx, query, params) + ret0, _ := ret[0].(*slack.SearchMessages) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchMessagesContext indicates an expected call of SearchMessagesContext. +func (mr *mockClienterMockRecorder) SearchMessagesContext(ctx, query, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMessagesContext", reflect.TypeOf((*mockClienter)(nil).SearchMessagesContext), ctx, query, params) +} diff --git a/cmd/slackdump/internal/record/search.go b/cmd/slackdump/internal/record/search.go new file mode 100644 index 00000000..f52f0dd4 --- /dev/null +++ b/cmd/slackdump/internal/record/search.go @@ -0,0 +1,46 @@ +package record + +import ( + "context" + "errors" + "strings" + + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" + "github.com/rusq/slackdump/v3/internal/chunk" + "github.com/rusq/slackdump/v3/internal/chunk/control" +) + +var CmdSearch = &base.Command{ + UsageLine: "slackdump search [flags] query terms", + Short: "records search results matching the given query", + Long: `Searches for messages matching criteria.`, + RequireAuth: true, + Run: runSearch, + PrintFlags: true, +} + +func runSearch(ctx context.Context, cmd *base.Command, args []string) error { + if len(args) == 0 { + base.SetExitStatus(base.SInvalidParameters) + return errors.New("missing query parameter") + } + query := strings.Join(args, " ") + + sess, err := cfg.SlackdumpSession(ctx) + if err != nil { + return err + } + + cd, err := chunk.CreateDir(cfg.Output) + if err != nil { + base.SetExitStatus(base.SGenericError) + return err + } + defer cd.Close() + + stream := sess.Stream() + ctrl := control.NewSearch(cd, stream) + + return ctrl.Search(ctx, query) +} diff --git a/cmd/slackdump/main.go b/cmd/slackdump/main.go index 6730daad..14803a9c 100644 --- a/cmd/slackdump/main.go +++ b/cmd/slackdump/main.go @@ -44,6 +44,7 @@ func init() { export.CmdExport, dump.CmdDump, record.CmdRecord, + record.CmdSearch, convertcmd.CmdConvert, list.CmdList, emoji.CmdEmoji, diff --git a/internal/chunk/chunktype_string.go b/internal/chunk/chunktype_string.go index b97622af..ef5580df 100644 --- a/internal/chunk/chunktype_string.go +++ b/internal/chunk/chunktype_string.go @@ -18,11 +18,13 @@ func _() { _ = x[CChannelUsers-7] _ = x[CStarredItems-8] _ = x[CBookmarks-9] + _ = x[CSearchMessages-10] + _ = x[CSearchFiles-11] } -const _ChunkType_name = "MessagesThreadMessagesFilesUsersChannelsChannelInfoWorkspaceInfoChannelUsersStarredItemsBookmarks" +const _ChunkType_name = "MessagesThreadMessagesFilesUsersChannelsChannelInfoWorkspaceInfoChannelUsersStarredItemsBookmarksSearchMessagesSearchFiles" -var _ChunkType_index = [...]uint8{0, 8, 22, 27, 32, 40, 51, 64, 76, 88, 97} +var _ChunkType_index = [...]uint8{0, 8, 22, 27, 32, 40, 51, 64, 76, 88, 97, 111, 122} func (i ChunkType) String() string { if i >= ChunkType(len(_ChunkType_index)-1) { diff --git a/internal/chunk/control/interfaces.go b/internal/chunk/control/interfaces.go index 7365ed8f..3079bfd1 100644 --- a/internal/chunk/control/interfaces.go +++ b/internal/chunk/control/interfaces.go @@ -14,6 +14,7 @@ type Streamer interface { ListChannels(ctx context.Context, proc processor.Channels, p *slack.GetConversationsParameters) error Users(ctx context.Context, proc processor.Users, opt ...slack.GetUsersOption) error WorkspaceInfo(ctx context.Context, proc processor.WorkspaceInfo) error + SearchMessages(ctx context.Context, proc processor.Search, query string) error } type TransformStarter interface { diff --git a/internal/chunk/control/search.go b/internal/chunk/control/search.go new file mode 100644 index 00000000..e3037593 --- /dev/null +++ b/internal/chunk/control/search.go @@ -0,0 +1,37 @@ +package control + +import ( + "context" + "time" + + "github.com/rusq/slackdump/v3" + "github.com/rusq/slackdump/v3/internal/chunk" + "github.com/rusq/slackdump/v3/logger" + "golang.org/x/sync/errgroup" +) + +type Search struct { + cd *chunk.Directory + s *slackdump.Stream + lg logger.Interface +} + +func NewSearch(cd *chunk.Directory, s *slackdump.Stream) *Search { + return &Search{cd: cd, s: s, lg: logger.Default} +} + +func (s *Search) Search(ctx context.Context, query string) error { + var eg errgroup.Group + start := time.Now() + eg.Go(func() error { + return searchWorker(ctx, s.s, s.cd, query) + }) + eg.Go(func() error { + return workspaceWorker(ctx, s.s, s.cd) + }) + if err := eg.Wait(); err != nil { + return err + } + s.lg.Printf("search for query %q completed in: %s", query, time.Since(start)) + return nil +} diff --git a/internal/chunk/control/workers.go b/internal/chunk/control/workers.go index 5fc35962..fe907a43 100644 --- a/internal/chunk/control/workers.go +++ b/internal/chunk/control/workers.go @@ -67,3 +67,18 @@ func workspaceWorker(ctx context.Context, s Streamer, cd *chunk.Directory) error lg.Debug("workspaceWorker done") return nil } + +func searchWorker(ctx context.Context, s Streamer, cd *chunk.Directory, query string) error { + lg := logger.FromContext(ctx) + lg.Debug("searchWorker started") + search, err := dirproc.NewSearch(cd) + if err != nil { + return err + } + defer search.Close() + if err := s.SearchMessages(ctx, search, query); err != nil { + return err + } + lg.Debug("searchWorker done") + return nil +} diff --git a/internal/chunk/dirproc/search.go b/internal/chunk/dirproc/search.go new file mode 100644 index 00000000..10b65fda --- /dev/null +++ b/internal/chunk/dirproc/search.go @@ -0,0 +1,15 @@ +package dirproc + +import "github.com/rusq/slackdump/v3/internal/chunk" + +type Search struct { + *baseproc +} + +func NewSearch(dir *chunk.Directory) (*Search, error) { + p, err := newBaseProc(dir, "search") + if err != nil { + return nil, err + } + return &Search{baseproc: p}, nil +}