This is an automated email from the ASF dual-hosted git repository.

abeizn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 47ebe5d54 feat: collect slack channel/message/thread (#5016)
47ebe5d54 is described below

commit 47ebe5d547bdab26e6700c9f1a927c5a58d2c193
Author: Likyh <[email protected]>
AuthorDate: Fri May 12 08:57:01 2023 +0800

    feat: collect slack channel/message/thread (#5016)
    
    * feat: collect slack channel/message/thread
    
    * fix: fix collect bug when use GetNextPageCustomData
---
 backend/helpers/pluginhelper/api/api_collector.go  |   3 +-
 backend/plugins/slack/README.md                    |   1 +
 backend/plugins/slack/api/connection.go            | 146 ++++++++++++++++++
 backend/plugins/slack/api/init.go                  |  37 +++++
 backend/plugins/slack/api/swagger.go               |  44 ++++++
 backend/plugins/slack/apimodels/access_token.go    |  31 ++++
 backend/plugins/slack/apimodels/im_result.go       | 166 +++++++++++++++++++++
 backend/plugins/slack/impl/impl.go                 | 130 ++++++++++++++++
 backend/plugins/slack/models/channel.go            |  52 +++++++
 backend/plugins/slack/models/channel_message.go    |  46 ++++++
 backend/plugins/slack/models/connection.go         |  38 +++++
 .../migrationscripts/20230421_add_init_tables.go   |  78 ++++++++++
 .../models/migrationscripts/archived/channel.go    |  52 +++++++
 .../migrationscripts/archived/channel_message.go   |  46 ++++++
 .../models/migrationscripts/archived/connection.go |  35 +++++
 .../slack/models/migrationscripts/register.go      |  29 ++++
 backend/plugins/slack/slack.go                     |  39 +++++
 backend/plugins/slack/tasks/api_client.go          |  42 ++++++
 backend/plugins/slack/tasks/channel_collector.go   |  91 +++++++++++
 backend/plugins/slack/tasks/channel_extractor.go   |  62 ++++++++
 .../slack/tasks/channel_message_collector.go       | 118 +++++++++++++++
 .../slack/tasks/channel_message_extractor.go       |  85 +++++++++++
 backend/plugins/slack/tasks/task_data.go           |  35 +++++
 backend/plugins/slack/tasks/thread_collector.go    | 121 +++++++++++++++
 backend/plugins/slack/tasks/thread_extractor.go    |  85 +++++++++++
 25 files changed, 1611 insertions(+), 1 deletion(-)

diff --git a/backend/helpers/pluginhelper/api/api_collector.go 
b/backend/helpers/pluginhelper/api/api_collector.go
index 845658bef..517025dea 100644
--- a/backend/helpers/pluginhelper/api/api_collector.go
+++ b/backend/helpers/pluginhelper/api/api_collector.go
@@ -265,7 +265,8 @@ func (collector *ApiCollector) 
fetchPagesSequentially(reqData *RequestData) {
                        reqData.CustomData = customData
                        reqData.Pager.Skip += collector.args.PageSize
                        reqData.Pager.Page += 1
-                       return collect()
+                       collector.args.ApiClient.NextTick(collect)
+                       return nil
                })
                return nil
        }
diff --git a/backend/plugins/slack/README.md b/backend/plugins/slack/README.md
new file mode 100644
index 000000000..a792324f9
--- /dev/null
+++ b/backend/plugins/slack/README.md
@@ -0,0 +1 @@
+Please see details in the [Apache DevLake 
website](https://devlake.apache.org/docs/Plugins/slack)
\ No newline at end of file
diff --git a/backend/plugins/slack/api/connection.go 
b/backend/plugins/slack/api/connection.go
new file mode 100644
index 000000000..b1bf7ccd3
--- /dev/null
+++ b/backend/plugins/slack/api/connection.go
@@ -0,0 +1,146 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "context"
+       "github.com/apache/incubator-devlake/server/api/shared"
+       "net/http"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+)
+
+type SlackTestConnResponse struct {
+       shared.ApiBody
+       Connection *models.SlackConn
+}
+
+// @Summary test slack connection
+// @Description Test slack Connection. endpoint: 
https://open.slack.cn/open-apis/
+// @Tags plugins/slack
+// @Param body body models.SlackConn true "json body"
+// @Success 200  {object} SlackTestConnResponse "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // process input
+       var connection models.SlackConn
+       if err := api.Decode(input.Body, &connection, vld); err != nil {
+               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters")
+       }
+
+       // test connection
+       _, err := api.NewApiClientFromConnection(context.TODO(), basicRes, 
&connection)
+
+       body := SlackTestConnResponse{}
+       body.Success = true
+       body.Message = "success"
+       body.Connection = &connection
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+}
+
+// @Summary create slack connection
+// @Description Create slack connection
+// @Tags plugins/slack
+// @Param body body models.SlackConnection true "json body"
+// @Success 200  {object} models.SlackConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/connections [POST]
+func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.SlackConnection{}
+       err := connectionHelper.Create(connection, input)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+}
+
+// @Summary patch slack connection
+// @Description Patch slack connection
+// @Tags plugins/slack
+// @Param body body models.SlackConnection true "json body"
+// @Success 200  {object} models.SlackConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/connections/{connectionId} [PATCH]
+func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.SlackConnection{}
+       err := connectionHelper.Patch(connection, input)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+}
+
+// @Summary delete a slack connection
+// @Description Delete a slack connection
+// @Tags plugins/slack
+// @Success 200  {object} models.SlackConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/connections/{connectionId} [DELETE]
+func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.SlackConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, err
+       }
+       err = connectionHelper.Delete(connection)
+       return &plugin.ApiResourceOutput{Body: connection}, err
+}
+
+// @Summary get all slack connections
+// @Description Get all slack connections
+// @Tags plugins/slack
+// @Success 200  {object} models.SlackConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/connections [GET]
+func ListConnections(_ *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       var connections []models.SlackConnection
+       err := connectionHelper.List(&connections)
+       if err != nil {
+               return nil, err
+       }
+
+       return &plugin.ApiResourceOutput{Body: connections}, nil
+}
+
+// @Summary get slack connection detail
+// @Description Get slack connection detail
+// @Tags plugins/slack
+// @Success 200  {object} models.SlackConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/slack/connections/{connectionId} [GET]
+func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       connection := &models.SlackConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection}, err
+}
diff --git a/backend/plugins/slack/api/init.go 
b/backend/plugins/slack/api/init.go
new file mode 100644
index 000000000..d92c2b334
--- /dev/null
+++ b/backend/plugins/slack/api/init.go
@@ -0,0 +1,37 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/go-playground/validator/v10"
+)
+
+var vld *validator.Validate
+var connectionHelper *api.ConnectionApiHelper
+var basicRes context.BasicRes
+
+func Init(br context.BasicRes) {
+       basicRes = br
+       vld = validator.New()
+       connectionHelper = api.NewConnectionHelper(
+               basicRes,
+               vld,
+       )
+}
diff --git a/backend/plugins/slack/api/swagger.go 
b/backend/plugins/slack/api/swagger.go
new file mode 100644
index 000000000..25e67ba44
--- /dev/null
+++ b/backend/plugins/slack/api/swagger.go
@@ -0,0 +1,44 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+// @Summary blueprints plan for slack
+// @Description blueprints plan for slack
+// @Tags plugins/slack
+// @Accept application/json
+// @Param blueprint body SlackBlueprintPlan true "json"
+// @Router /blueprints/slack/blueprint-plan [post]
+func _() {}
+
+type SlackBlueprintPlan [][]struct {
+       Plugin  string   `json:"plugin"`
+       Options struct{} `json:"options"`
+}
+
+// @Summary pipelines plan for slack
+// @Description pipelines plan for slack
+// @Tags plugins/slack
+// @Accept application/json
+// @Param pipeline body SlackPipelinePlan true "json"
+// @Router /pipelines/slack/pipeline-plan [post]
+func _() {}
+
+type SlackPipelinePlan [][]struct {
+       Plugin  string   `json:"plugin"`
+       Options struct{} `json:"options"`
+}
diff --git a/backend/plugins/slack/apimodels/access_token.go 
b/backend/plugins/slack/apimodels/access_token.go
new file mode 100644
index 000000000..d61f2bf92
--- /dev/null
+++ b/backend/plugins/slack/apimodels/access_token.go
@@ -0,0 +1,31 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package apimodels
+
+type ApiAccessTokenRequest struct {
+       AppId     string `json:"app_id"`
+       AppSecret string `json:"app_secret"`
+}
+
+type ApiAccessTokenResponse struct {
+       Code              int    `json:"code"`
+       Msg               string `json:"msg"`
+       AppAccessToken    string `json:"app_access_token"`
+       TenantAccessToken string `json:"tenant_access_token"`
+       Expire            int    `json:"expire"`
+}
diff --git a/backend/plugins/slack/apimodels/im_result.go 
b/backend/plugins/slack/apimodels/im_result.go
new file mode 100644
index 000000000..f1b2cde72
--- /dev/null
+++ b/backend/plugins/slack/apimodels/im_result.go
@@ -0,0 +1,166 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package apimodels
+
+import "encoding/json"
+
+type SlackChannelApiResult struct {
+       Ok               bool              `json:"ok"`
+       Channels         []json.RawMessage `json:"channels"`
+       ResponseMetadata struct {
+               NextCursor string `json:"next_cursor"`
+       } `json:"response_metadata"`
+}
+
+type SlackChannelMessageApiResult struct {
+       Ok               bool              `json:"ok"`
+       Messages         []json.RawMessage `json:"messages"`
+       ResponseMetadata struct {
+               NextCursor string `json:"next_cursor"`
+       } `json:"response_metadata"`
+}
+
+type SlackChannelMessageResultItem struct {
+       ClientMsgId string `json:"client_msg_id"`
+       Type        string `json:"type"`
+       Subtype     string `json:"subtype"`
+       Ts          string `json:"ts"`
+       ThreadTs    string `json:"thread_ts"`
+       User        string `json:"user"`
+       Text        string `json:"text"`
+
+       Team            string   `json:"team"`
+       ReplyCount      int      `json:"reply_count"`
+       ReplyUsersCount int      `json:"reply_users_count"`
+       LatestReply     string   `json:"latest_reply"`
+       ReplyUsers      []string `json:"reply_users"`
+       IsLocked        bool     `json:"is_locked"`
+       Subscribed      bool     `json:"subscribed"`
+       ParentUserId    string   `json:"parent_user_id"`
+
+       Files []struct {
+               Id                 string `json:"id"`
+               Created            int    `json:"created"`
+               Timestamp          int    `json:"timestamp"`
+               Name               string `json:"name"`
+               Title              string `json:"title"`
+               Mimetype           string `json:"mimetype"`
+               Filetype           string `json:"filetype"`
+               PrettyType         string `json:"pretty_type"`
+               User               string `json:"user"`
+               UserTeam           string `json:"user_team"`
+               Editable           bool   `json:"editable"`
+               Size               int    `json:"size"`
+               Mode               string `json:"mode"`
+               IsExternal         bool   `json:"is_external"`
+               ExternalType       string `json:"external_type"`
+               IsPublic           bool   `json:"is_public"`
+               PublicUrlShared    bool   `json:"public_url_shared"`
+               DisplayAsBot       bool   `json:"display_as_bot"`
+               Username           string `json:"username"`
+               UrlPrivate         string `json:"url_private"`
+               UrlPrivateDownload string `json:"url_private_download"`
+               MediaDisplayType   string `json:"media_display_type"`
+               Thumb64            string `json:"thumb_64"`
+               Thumb80            string `json:"thumb_80"`
+               Thumb360           string `json:"thumb_360"`
+               Thumb360W          int    `json:"thumb_360_w"`
+               Thumb360H          int    `json:"thumb_360_h"`
+               Thumb480           string `json:"thumb_480"`
+               Thumb480W          int    `json:"thumb_480_w"`
+               Thumb480H          int    `json:"thumb_480_h"`
+               Thumb160           string `json:"thumb_160"`
+               Thumb720           string `json:"thumb_720"`
+               Thumb720W          int    `json:"thumb_720_w"`
+               Thumb720H          int    `json:"thumb_720_h"`
+               Thumb800           string `json:"thumb_800"`
+               Thumb800W          int    `json:"thumb_800_w"`
+               Thumb800H          int    `json:"thumb_800_h"`
+               Thumb960           string `json:"thumb_960"`
+               Thumb960W          int    `json:"thumb_960_w"`
+               Thumb960H          int    `json:"thumb_960_h"`
+               Thumb1024          string `json:"thumb_1024"`
+               Thumb1024W         int    `json:"thumb_1024_w"`
+               Thumb1024H         int    `json:"thumb_1024_h"`
+               OriginalW          int    `json:"original_w"`
+               OriginalH          int    `json:"original_h"`
+               ThumbTiny          string `json:"thumb_tiny"`
+               Permalink          string `json:"permalink"`
+               PermalinkPublic    string `json:"permalink_public"`
+               IsStarred          bool   `json:"is_starred"`
+               HasRichPreview     bool   `json:"has_rich_preview"`
+               FileAccess         string `json:"file_access"`
+       } `json:"files"`
+       Upload bool `json:"upload"`
+       Blocks []struct {
+               Type     string `json:"type"`
+               BlockId  string `json:"block_id"`
+               Elements []struct {
+                       Type     string `json:"type"`
+                       Elements []struct {
+                               Type  string `json:"type"`
+                               Text  string `json:"text"`
+                               Style struct {
+                                       Bold bool `json:"bold"`
+                               } `json:"style,omitempty"`
+                       } `json:"elements"`
+               } `json:"elements"`
+       } `json:"blocks"`
+
+       Root struct {
+               ClientMsgId string `json:"client_msg_id"`
+               Type        string `json:"type"`
+               Text        string `json:"text"`
+               User        string `json:"user"`
+               Ts          string `json:"ts"`
+               Blocks      []struct {
+                       Type     string `json:"type"`
+                       BlockId  string `json:"block_id"`
+                       Elements []struct {
+                               Type     string `json:"type"`
+                               Elements []struct {
+                                       Type    string `json:"type"`
+                                       Text    string `json:"text,omitempty"`
+                                       Name    string `json:"name,omitempty"`
+                                       Unicode string 
`json:"unicode,omitempty"`
+                               } `json:"elements"`
+                       } `json:"elements"`
+               } `json:"blocks"`
+               Team            string   `json:"team"`
+               ThreadTs        string   `json:"thread_ts"`
+               ReplyCount      int      `json:"reply_count"`
+               ReplyUsersCount int      `json:"reply_users_count"`
+               LatestReply     string   `json:"latest_reply"`
+               ReplyUsers      []string `json:"reply_users"`
+               IsLocked        bool     `json:"is_locked"`
+               Subscribed      bool     `json:"subscribed"`
+       } `json:"root"`
+       Reactions []struct {
+               Name  string   `json:"name"`
+               Users []string `json:"users"`
+               Count int      `json:"count"`
+       } `json:"reactions"`
+}
+
+type SlackThreadsApiResult struct {
+       Ok               bool              `json:"ok"`
+       Threads          []json.RawMessage `json:"messages"`
+       ResponseMetadata struct {
+               NextCursor string `json:"next_cursor"`
+       } `json:"response_metadata"`
+}
diff --git a/backend/plugins/slack/impl/impl.go 
b/backend/plugins/slack/impl/impl.go
new file mode 100644
index 000000000..896f7955f
--- /dev/null
+++ b/backend/plugins/slack/impl/impl.go
@@ -0,0 +1,130 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package impl
+
+import (
+       "fmt"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/api"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+       
"github.com/apache/incubator-devlake/plugins/slack/models/migrationscripts"
+       "github.com/apache/incubator-devlake/plugins/slack/tasks"
+)
+
+var _ plugin.PluginMeta = (*Slack)(nil)
+var _ plugin.PluginInit = (*Slack)(nil)
+var _ plugin.PluginTask = (*Slack)(nil)
+var _ plugin.PluginApi = (*Slack)(nil)
+var _ plugin.PluginModel = (*Slack)(nil)
+var _ plugin.PluginMigration = (*Slack)(nil)
+var _ plugin.CloseablePluginTask = (*Slack)(nil)
+
+type Slack struct{}
+
+func (p Slack) Init(basicRes context.BasicRes) errors.Error {
+       api.Init(basicRes)
+       return nil
+}
+
+func (p Slack) GetTablesInfo() []dal.Tabler {
+       return []dal.Tabler{
+               &models.SlackConnection{},
+       }
+}
+
+func (p Slack) Description() string {
+       return "To collect and enrich data from Slack"
+}
+
+func (p Slack) SubTaskMetas() []plugin.SubTaskMeta {
+       return []plugin.SubTaskMeta{
+               tasks.CollectChannelMeta,
+               tasks.ExtractChannelMeta,
+
+               tasks.CollectChannelMessageMeta,
+               tasks.ExtractChannelMessageMeta,
+
+               tasks.CollectThreadMeta,
+               tasks.ExtractThreadMeta,
+       }
+}
+
+func (p Slack) PrepareTaskData(taskCtx plugin.TaskContext, options 
map[string]interface{}) (interface{}, errors.Error) {
+       var op tasks.SlackOptions
+       if err := helper.Decode(options, &op, nil); err != nil {
+               return nil, err
+       }
+
+       connectionHelper := helper.NewConnectionHelper(
+               taskCtx,
+               nil,
+       )
+       connection := &models.SlackConnection{}
+       err := connectionHelper.FirstById(connection, op.ConnectionId)
+       if err != nil {
+               return nil, err
+       }
+
+       apiClient, err := tasks.NewSlackApiClient(taskCtx, connection)
+       if err != nil {
+               return nil, err
+       }
+       return &tasks.SlackTaskData{
+               Options:   &op,
+               ApiClient: apiClient,
+       }, nil
+}
+
+func (p Slack) RootPkgPath() string {
+       return "github.com/apache/incubator-devlake/plugins/slack"
+}
+
+func (p Slack) MigrationScripts() []plugin.MigrationScript {
+       return migrationscripts.All()
+}
+
+func (p Slack) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
+       return map[string]map[string]plugin.ApiResourceHandler{
+               "test": {
+                       "POST": api.TestConnection,
+               },
+               "connections": {
+                       "POST": api.PostConnections,
+                       "GET":  api.ListConnections,
+               },
+               "connections/:connectionId": {
+                       "PATCH":  api.PatchConnection,
+                       "DELETE": api.DeleteConnection,
+                       "GET":    api.GetConnection,
+               },
+       }
+}
+
+func (p Slack) Close(taskCtx plugin.TaskContext) errors.Error {
+       data, ok := taskCtx.GetData().(*tasks.SlackTaskData)
+       if !ok {
+               return errors.Default.New(fmt.Sprintf("GetData failed when try 
to close %+v", taskCtx))
+       }
+       data.ApiClient.Release()
+       return nil
+}
diff --git a/backend/plugins/slack/models/channel.go 
b/backend/plugins/slack/models/channel.go
new file mode 100644
index 000000000..ba8aa1724
--- /dev/null
+++ b/backend/plugins/slack/models/channel.go
@@ -0,0 +1,52 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type SlackChannel struct {
+       common.NoPKModel   `json:"-"`
+       ConnectionId       uint64 `gorm:"primaryKey"`
+       Id                 string `json:"id" gorm:"primaryKey"`
+       Name               string `json:"name"`
+       IsChannel          bool   `json:"is_channel"`
+       IsGroup            bool   `json:"is_group"`
+       IsIm               bool   `json:"is_im"`
+       IsMpim             bool   `json:"is_mpim"`
+       IsPrivate          bool   `json:"is_private"`
+       Created            int    `json:"created"`
+       IsArchived         bool   `json:"is_archived"`
+       IsGeneral          bool   `json:"is_general"`
+       Unlinked           int    `json:"unlinked"`
+       NameNormalized     string `json:"name_normalized"`
+       IsShared           bool   `json:"is_shared"`
+       IsOrgShared        bool   `json:"is_org_shared"`
+       IsPendingExtShared bool   `json:"is_pending_ext_shared"`
+       ContextTeamId      string `json:"context_team_id"`
+       Updated            int64  `json:"updated"`
+       Creator            string `json:"creator"`
+       IsExtShared        bool   `json:"is_ext_shared"`
+       IsMember           bool   `json:"is_member"`
+       NumMembers         int    `json:"num_members"`
+}
+
+func (SlackChannel) TableName() string {
+       return "_tool_slack_channels"
+}
diff --git a/backend/plugins/slack/models/channel_message.go 
b/backend/plugins/slack/models/channel_message.go
new file mode 100644
index 000000000..7e93b56fa
--- /dev/null
+++ b/backend/plugins/slack/models/channel_message.go
@@ -0,0 +1,46 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type SlackChannelMessage struct {
+       common.NoPKModel `json:"-"`
+       ConnectionId     uint64 `gorm:"primaryKey"`
+       ChannelId        string `json:"channel_id" gorm:"primaryKey"`
+       Ts               string `json:"ts" gorm:"primaryKey"`
+       ClientMsgId      string `json:"client_msg_id"`
+       Type             string `json:"type"`
+       Subtype          string `json:"subtype"`
+       ThreadTs         string `json:"thread_ts"`
+       User             string `json:"user"`
+       Text             string `json:"text"`
+       Team             string `json:"team"`
+       ReplyCount       int    `json:"reply_count"`
+       ReplyUsersCount  int    `json:"reply_users_count"`
+       LatestReply      string `json:"latest_reply"`
+       IsLocked         bool   `json:"is_locked"`
+       Subscribed       bool   `json:"subscribed"`
+       ParentUserId     string `json:"parent_user_id"`
+}
+
+func (SlackChannelMessage) TableName() string {
+       return "_tool_slack_channel_messages"
+}
diff --git a/backend/plugins/slack/models/connection.go 
b/backend/plugins/slack/models/connection.go
new file mode 100644
index 000000000..89182e35b
--- /dev/null
+++ b/backend/plugins/slack/models/connection.go
@@ -0,0 +1,38 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+// SlackConn holds the essential information to connect to the Slack API
+type SlackConn struct {
+       helper.RestConnection `mapstructure:",squash"`
+       helper.AccessToken    `mapstructure:",squash"`
+}
+
+// SlackConnection holds SlackConn plus ID/Name for database storage
+type SlackConnection struct {
+       helper.BaseConnection `mapstructure:",squash"`
+       SlackConn             `mapstructure:",squash"`
+}
+
+func (SlackConnection) TableName() string {
+       return "_tool_slack_connections"
+}
diff --git 
a/backend/plugins/slack/models/migrationscripts/20230421_add_init_tables.go 
b/backend/plugins/slack/models/migrationscripts/20230421_add_init_tables.go
new file mode 100644
index 000000000..21e322914
--- /dev/null
+++ b/backend/plugins/slack/models/migrationscripts/20230421_add_init_tables.go
@@ -0,0 +1,78 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       
"github.com/apache/incubator-devlake/plugins/slack/models/migrationscripts/archived"
+)
+
+type addInitTables struct {
+}
+
+func (u *addInitTables) Up(basicRes context.BasicRes) errors.Error {
+       db := basicRes.GetDal()
+
+       err := db.DropTables(
+               &archived.SlackConnection{},
+               &archived.SlackChannel{},
+               &archived.SlackChannelMessage{},
+       )
+       if err != nil {
+               return err
+       }
+
+       err = migrationhelper.AutoMigrateTables(
+               basicRes,
+               &archived.SlackConnection{},
+               &archived.SlackChannel{},
+               &archived.SlackChannelMessage{},
+       )
+       if err != nil {
+               return err
+       }
+
+       encodeKey := basicRes.GetConfig(plugin.EncodeKeyEnvStr)
+       connection := &archived.SlackConnection{}
+       connection.Endpoint = basicRes.GetConfig(`SLACK_ENDPOINT`)
+       connection.Token = basicRes.GetConfig(`SLACK_TOKEN`)
+       connection.Name = `Slack`
+       if connection.Endpoint != `` && connection.Token != `` && encodeKey != 
`` {
+               connection.Token, err = plugin.Encrypt(encodeKey, 
connection.Token)
+               if err != nil {
+                       return err
+               }
+               // update from .env and save to db
+               err = db.CreateIfNotExist(connection)
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func (*addInitTables) Version() uint64 {
+       return 20230421000001
+}
+
+func (*addInitTables) Name() string {
+       return "Slack init schemas"
+}
diff --git a/backend/plugins/slack/models/migrationscripts/archived/channel.go 
b/backend/plugins/slack/models/migrationscripts/archived/channel.go
new file mode 100644
index 000000000..c7475299a
--- /dev/null
+++ b/backend/plugins/slack/models/migrationscripts/archived/channel.go
@@ -0,0 +1,52 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type SlackChannel struct {
+       archived.NoPKModel `json:"-"`
+       ConnectionId       uint64 `gorm:"primaryKey"`
+       Id                 string `json:"id" gorm:"primaryKey"`
+       Name               string `json:"name"`
+       IsChannel          bool   `json:"is_channel"`
+       IsGroup            bool   `json:"is_group"`
+       IsIm               bool   `json:"is_im"`
+       IsMpim             bool   `json:"is_mpim"`
+       IsPrivate          bool   `json:"is_private"`
+       Created            int    `json:"created"`
+       IsArchived         bool   `json:"is_archived"`
+       IsGeneral          bool   `json:"is_general"`
+       Unlinked           int    `json:"unlinked"`
+       NameNormalized     string `json:"name_normalized"`
+       IsShared           bool   `json:"is_shared"`
+       IsOrgShared        bool   `json:"is_org_shared"`
+       IsPendingExtShared bool   `json:"is_pending_ext_shared"`
+       ContextTeamId      string `json:"context_team_id"`
+       Updated            int64  `json:"updated"`
+       Creator            string `json:"creator"`
+       IsExtShared        bool   `json:"is_ext_shared"`
+       IsMember           bool   `json:"is_member"`
+       NumMembers         int    `json:"num_members"`
+}
+
+func (SlackChannel) TableName() string {
+       return "_tool_slack_channels"
+}
diff --git 
a/backend/plugins/slack/models/migrationscripts/archived/channel_message.go 
b/backend/plugins/slack/models/migrationscripts/archived/channel_message.go
new file mode 100644
index 000000000..a58b24bde
--- /dev/null
+++ b/backend/plugins/slack/models/migrationscripts/archived/channel_message.go
@@ -0,0 +1,46 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type SlackChannelMessage struct {
+       archived.NoPKModel `json:"-"`
+       ConnectionId       uint64 `gorm:"primaryKey"`
+       ChannelId          string `json:"channel_id" gorm:"primaryKey"`
+       Ts                 string `json:"ts" gorm:"primaryKey"`
+       ClientMsgId        string `json:"client_msg_id"`
+       Type               string `json:"type"`
+       Subtype            string `json:"subtype"`
+       ThreadTs           string `json:"thread_ts"`
+       User               string `json:"user"`
+       Text               string `json:"text"`
+       Team               string `json:"team"`
+       ReplyCount         int    `json:"reply_count"`
+       ReplyUsersCount    int    `json:"reply_users_count"`
+       LatestReply        string `json:"latest_reply"`
+       IsLocked           bool   `json:"is_locked"`
+       Subscribed         bool   `json:"subscribed"`
+       ParentUserId       string `json:"parent_user_id"`
+}
+
+func (SlackChannelMessage) TableName() string {
+       return "_tool_slack_channel_messages"
+}
diff --git 
a/backend/plugins/slack/models/migrationscripts/archived/connection.go 
b/backend/plugins/slack/models/migrationscripts/archived/connection.go
new file mode 100644
index 000000000..8a6806dce
--- /dev/null
+++ b/backend/plugins/slack/models/migrationscripts/archived/connection.go
@@ -0,0 +1,35 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       commonArchived 
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type SlackConnection struct {
+       commonArchived.Model
+       Name             string `gorm:"type:varchar(100);uniqueIndex" 
json:"name" validate:"required"`
+       Endpoint         string `gorm:"type:varchar(255)"`
+       Proxy            string `json:"proxy" gorm:"type:varchar(255)"`
+       RateLimitPerHour int    `comment:"api request rate limit per hour"`
+       Token            string `gorm:"type:varchar(255)"`
+}
+
+func (SlackConnection) TableName() string {
+       return "_tool_slack_connections"
+}
diff --git a/backend/plugins/slack/models/migrationscripts/register.go 
b/backend/plugins/slack/models/migrationscripts/register.go
new file mode 100644
index 000000000..ec054748c
--- /dev/null
+++ b/backend/plugins/slack/models/migrationscripts/register.go
@@ -0,0 +1,29 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/plugin"
+)
+
+// All return all the migration scripts
+func All() []plugin.MigrationScript {
+       return []plugin.MigrationScript{
+               new(addInitTables),
+       }
+}
diff --git a/backend/plugins/slack/slack.go b/backend/plugins/slack/slack.go
new file mode 100644
index 000000000..01355bb29
--- /dev/null
+++ b/backend/plugins/slack/slack.go
@@ -0,0 +1,39 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+       "github.com/apache/incubator-devlake/core/runner"
+       "github.com/apache/incubator-devlake/plugins/slack/impl"
+       "github.com/spf13/cobra"
+)
+
+var PluginEntry impl.Slack
+
+// standalone mode for debugging
+func main() {
+       cmd := &cobra.Command{Use: "slack"}
+       connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "slack 
connection id")
+       _ = cmd.MarkFlagRequired("connectionId")
+       cmd.Run = func(cmd *cobra.Command, args []string) {
+               runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+                       "connectionId": *connectionId,
+               })
+       }
+       runner.RunCmd(cmd)
+}
diff --git a/backend/plugins/slack/tasks/api_client.go 
b/backend/plugins/slack/tasks/api_client.go
new file mode 100644
index 000000000..865259661
--- /dev/null
+++ b/backend/plugins/slack/tasks/api_client.go
@@ -0,0 +1,42 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+)
+
+func NewSlackApiClient(taskCtx plugin.TaskContext, connection 
*models.SlackConnection) (*api.ApiAsyncClient, errors.Error) {
+       apiClient, err := api.NewApiClientFromConnection(taskCtx.GetContext(), 
taskCtx, connection)
+       if err != nil {
+               return nil, err
+       }
+
+       // create async api client
+       asyncApiClient, err := api.CreateAsyncApiClient(taskCtx, apiClient, 
&api.ApiRateLimitCalculator{
+               UserRateLimitPerHour: connection.RateLimitPerHour,
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       return asyncApiClient, nil
+}
diff --git a/backend/plugins/slack/tasks/channel_collector.go 
b/backend/plugins/slack/tasks/channel_collector.go
new file mode 100644
index 000000000..d2a6f76d8
--- /dev/null
+++ b/backend/plugins/slack/tasks/channel_collector.go
@@ -0,0 +1,91 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/apimodels"
+       "net/http"
+       "net/url"
+       "strconv"
+)
+
+const RAW_CHANNEL_TABLE = "slack_channel"
+
+var _ plugin.SubTaskEntryPoint = CollectChannel
+
+// CollectChannel collect all channels that bot is in
+func CollectChannel(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       pageSize := 100
+       collector, err := api.NewApiCollector(api.ApiCollectorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_CHANNEL_TABLE,
+               },
+               ApiClient:   data.ApiClient,
+               Incremental: false,
+               UrlTemplate: "conversations.list",
+               PageSize:    pageSize,
+               GetNextPageCustomData: func(prevReqData *api.RequestData, 
prevPageResponse *http.Response) (interface{}, errors.Error) {
+                       res := apimodels.SlackChannelMessageApiResult{}
+                       err := api.UnmarshalResponse(prevPageResponse, &res)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if res.ResponseMetadata.NextCursor == "" {
+                               return nil, api.ErrFinishCollect
+                       }
+                       return res.ResponseMetadata.NextCursor, nil
+               },
+               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
+                       query := url.Values{}
+                       query.Set("limit", strconv.Itoa(pageSize))
+                       if pageToken, ok := reqData.CustomData.(string); ok && 
pageToken != "" {
+                               query.Set("cursor", reqData.CustomData.(string))
+                       }
+                       return query, nil
+               },
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
+                       body := &apimodels.SlackChannelApiResult{}
+                       err := api.UnmarshalResponse(res, body)
+                       if err != nil {
+                               return nil, err
+                       }
+                       return body.Channels, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return collector.Execute()
+}
+
+var CollectChannelMeta = plugin.SubTaskMeta{
+       Name:             "collectChannel",
+       EntryPoint:       CollectChannel,
+       EnabledByDefault: true,
+       Description:      "Collect channels from Slack api",
+}
diff --git a/backend/plugins/slack/tasks/channel_extractor.go 
b/backend/plugins/slack/tasks/channel_extractor.go
new file mode 100644
index 000000000..2374ba0eb
--- /dev/null
+++ b/backend/plugins/slack/tasks/channel_extractor.go
@@ -0,0 +1,62 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractChannel
+
+func ExtractChannel(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_CHANNEL_TABLE,
+               },
+               Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+                       body := &models.SlackChannel{}
+                       err := errors.Convert(json.Unmarshal(row.Data, body))
+                       if err != nil {
+                               return nil, err
+                       }
+                       body.ConnectionId = data.Options.ConnectionId
+                       return []interface{}{body}, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return extractor.Execute()
+}
+
+var ExtractChannelMeta = plugin.SubTaskMeta{
+       Name:             "extractChannel",
+       EntryPoint:       ExtractChannel,
+       EnabledByDefault: true,
+       Description:      "Extract raw channel data into tool layer table",
+}
diff --git a/backend/plugins/slack/tasks/channel_message_collector.go 
b/backend/plugins/slack/tasks/channel_message_collector.go
new file mode 100644
index 000000000..7c12391d6
--- /dev/null
+++ b/backend/plugins/slack/tasks/channel_message_collector.go
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/apimodels"
+       "net/http"
+       "net/url"
+       "reflect"
+       "strconv"
+)
+
+const RAW_CHANNEL_MESSAGE_TABLE = "slack_channel_message"
+
+var _ plugin.SubTaskEntryPoint = CollectChannelMessage
+
+type ChannelInput struct {
+       ChannelId string `json:"channel_id"`
+}
+
+func CollectChannelMessage(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       db := taskCtx.GetDal()
+
+       clauses := []dal.Clause{
+               dal.Select("id as channel_id"),
+               dal.From("_tool_slack_channels"),
+               dal.Where("connection_id=?", data.Options.ConnectionId),
+       }
+
+       // construct the input iterator
+       cursor, err := db.Cursor(clauses...)
+       if err != nil {
+               return err
+       }
+       // smaller struct can reduce memory footprint, we should try to avoid 
using big struct
+       iterator, err := api.NewDalCursorIterator(db, cursor, 
reflect.TypeOf(ChannelInput{}))
+       if err != nil {
+               return err
+       }
+
+       pageSize := 100
+       collector, err := api.NewApiCollector(api.ApiCollectorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_CHANNEL_MESSAGE_TABLE,
+               },
+               ApiClient:   data.ApiClient,
+               Incremental: false,
+               Input:       iterator,
+               UrlTemplate: "conversations.history",
+               PageSize:    pageSize,
+               GetNextPageCustomData: func(prevReqData *api.RequestData, 
prevPageResponse *http.Response) (interface{}, errors.Error) {
+                       res := apimodels.SlackChannelMessageApiResult{}
+                       err := api.UnmarshalResponse(prevPageResponse, &res)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if res.ResponseMetadata.NextCursor == "" {
+                               return nil, api.ErrFinishCollect
+                       }
+                       return res.ResponseMetadata.NextCursor, nil
+               },
+               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
+                       input := reqData.Input.(*ChannelInput)
+                       query := url.Values{}
+                       query.Set("channel", input.ChannelId)
+                       query.Set("limit", strconv.Itoa(pageSize))
+                       if pageToken, ok := reqData.CustomData.(string); ok && 
pageToken != "" {
+                               query.Set("cursor", reqData.CustomData.(string))
+                       }
+                       return query, nil
+               },
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
+                       body := &apimodels.SlackChannelMessageApiResult{}
+                       err := api.UnmarshalResponse(res, body)
+                       if err != nil {
+                               return nil, err
+                       }
+                       return body.Messages, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return collector.Execute()
+}
+
+var CollectChannelMessageMeta = plugin.SubTaskMeta{
+       Name:             "collectChannelMessage",
+       EntryPoint:       CollectChannelMessage,
+       EnabledByDefault: true,
+       Description:      "Collect channel message from Slack api",
+}
diff --git a/backend/plugins/slack/tasks/channel_message_extractor.go 
b/backend/plugins/slack/tasks/channel_message_extractor.go
new file mode 100644
index 000000000..91a048133
--- /dev/null
+++ b/backend/plugins/slack/tasks/channel_message_extractor.go
@@ -0,0 +1,85 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/apimodels"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractChannelMessage
+
+func ExtractChannelMessage(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_CHANNEL_MESSAGE_TABLE,
+               },
+               Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+                       channel := &ChannelInput{}
+                       err := errors.Convert(json.Unmarshal(row.Input, 
channel))
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       body := &apimodels.SlackChannelMessageResultItem{}
+                       err = errors.Convert(json.Unmarshal(row.Data, body))
+                       if err != nil {
+                               return nil, err
+                       }
+                       message := &models.SlackChannelMessage{}
+                       message.ConnectionId = data.Options.ConnectionId
+                       message.ChannelId = channel.ChannelId
+                       message.ClientMsgId = body.ClientMsgId
+                       message.Type = body.Type
+                       message.Subtype = body.Subtype
+                       message.Ts = body.Ts
+                       message.ThreadTs = body.ThreadTs
+                       message.User = body.User
+                       message.Text = body.Text
+                       message.Team = body.Team
+                       message.ReplyCount = body.ReplyCount
+                       message.ReplyUsersCount = body.ReplyUsersCount
+                       message.LatestReply = body.LatestReply
+                       message.IsLocked = body.IsLocked
+                       message.Subscribed = body.Subscribed
+                       message.ParentUserId = body.ParentUserId
+                       return []interface{}{message}, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return extractor.Execute()
+}
+
+var ExtractChannelMessageMeta = plugin.SubTaskMeta{
+       Name:             "extractChannelMessage",
+       EntryPoint:       ExtractChannelMessage,
+       EnabledByDefault: true,
+       Description:      "Extract raw channel messages data into tool layer 
table",
+}
diff --git a/backend/plugins/slack/tasks/task_data.go 
b/backend/plugins/slack/tasks/task_data.go
new file mode 100644
index 000000000..41c811fb6
--- /dev/null
+++ b/backend/plugins/slack/tasks/task_data.go
@@ -0,0 +1,35 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+type SlackApiParams struct {
+       ConnectionId uint64 `json:"connectionId"`
+}
+
+type SlackOptions struct {
+       ConnectionId uint64 `json:"connectionId"`
+}
+
+type SlackTaskData struct {
+       Options   *SlackOptions
+       ApiClient *helper.ApiAsyncClient
+}
diff --git a/backend/plugins/slack/tasks/thread_collector.go 
b/backend/plugins/slack/tasks/thread_collector.go
new file mode 100644
index 000000000..2019636a3
--- /dev/null
+++ b/backend/plugins/slack/tasks/thread_collector.go
@@ -0,0 +1,121 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/apimodels"
+       "net/http"
+       "net/url"
+       "reflect"
+       "strconv"
+)
+
+const RAW_THREAD_TABLE = "slack_thread"
+
+var _ plugin.SubTaskEntryPoint = CollectThread
+
+type ThreadInput struct {
+       ChannelId string `json:"channel_id"`
+       ThreadTs  string `json:"thread_ts"`
+}
+
+func CollectThread(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       db := taskCtx.GetDal()
+
+       clauses := []dal.Clause{
+               dal.Select("thread_ts, channel_id"),
+               dal.From("_tool_slack_channel_messages"),
+               dal.Where("connection_id=? AND thread_ts!='' AND subtype=''", 
data.Options.ConnectionId),
+       }
+
+       // construct the input iterator
+       cursor, err := db.Cursor(clauses...)
+       if err != nil {
+               return err
+       }
+       // smaller struct can reduce memory footprint, we should try to avoid 
using big struct
+       iterator, err := api.NewDalCursorIterator(db, cursor, 
reflect.TypeOf(ThreadInput{}))
+       if err != nil {
+               return err
+       }
+
+       pageSize := 50
+       collector, err := api.NewApiCollector(api.ApiCollectorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_THREAD_TABLE,
+               },
+               ApiClient:   data.ApiClient,
+               Incremental: false,
+               Input:       iterator,
+               UrlTemplate: "conversations.replies",
+               PageSize:    pageSize,
+               GetNextPageCustomData: func(prevReqData *api.RequestData, 
prevPageResponse *http.Response) (interface{}, errors.Error) {
+                       res := apimodels.SlackThreadsApiResult{}
+                       err := api.UnmarshalResponse(prevPageResponse, &res)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if res.ResponseMetadata.NextCursor == "" {
+                               return nil, api.ErrFinishCollect
+                       }
+                       return res.ResponseMetadata.NextCursor, nil
+               },
+               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
+                       input := reqData.Input.(*ThreadInput)
+                       query := url.Values{}
+                       query.Set("channel", input.ChannelId)
+                       query.Set("ts", input.ThreadTs)
+                       query.Set("offset", strconv.Itoa(reqData.Pager.Skip))
+                       query.Set("limit", strconv.Itoa(pageSize))
+                       if pageToken, ok := reqData.CustomData.(string); ok && 
pageToken != "" {
+                               query.Set("cursor", reqData.CustomData.(string))
+                       }
+                       return query, nil
+               },
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
+                       body := &apimodels.SlackThreadsApiResult{}
+                       err := api.UnmarshalResponse(res, body)
+                       if err != nil {
+                               return nil, err
+                       }
+                       return body.Threads, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return collector.Execute()
+}
+
+var CollectThreadMeta = plugin.SubTaskMeta{
+       Name:             "collectThread",
+       EntryPoint:       CollectThread,
+       EnabledByDefault: true,
+       Description:      "Collect thread from Slack api",
+}
diff --git a/backend/plugins/slack/tasks/thread_extractor.go 
b/backend/plugins/slack/tasks/thread_extractor.go
new file mode 100644
index 000000000..c9a9c08f0
--- /dev/null
+++ b/backend/plugins/slack/tasks/thread_extractor.go
@@ -0,0 +1,85 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/slack/apimodels"
+       "github.com/apache/incubator-devlake/plugins/slack/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractThread
+
+func ExtractThread(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*SlackTaskData)
+       extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx: taskCtx,
+                       Params: SlackApiParams{
+                               ConnectionId: data.Options.ConnectionId,
+                       },
+                       Table: RAW_THREAD_TABLE,
+               },
+               Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+                       threadInput := &ThreadInput{}
+                       err := errors.Convert(json.Unmarshal(row.Input, 
threadInput))
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       body := &apimodels.SlackChannelMessageResultItem{}
+                       err = errors.Convert(json.Unmarshal(row.Data, body))
+                       if err != nil {
+                               return nil, err
+                       }
+                       message := &models.SlackChannelMessage{}
+                       message.ConnectionId = data.Options.ConnectionId
+                       message.ChannelId = threadInput.ChannelId
+                       message.ClientMsgId = body.ClientMsgId
+                       message.Type = body.Type
+                       message.Subtype = body.Subtype
+                       message.Ts = body.Ts
+                       message.ThreadTs = body.ThreadTs
+                       message.User = body.User
+                       message.Text = body.Text
+                       message.Team = body.Team
+                       message.ReplyCount = body.ReplyCount
+                       message.ReplyUsersCount = body.ReplyUsersCount
+                       message.LatestReply = body.LatestReply
+                       message.IsLocked = body.IsLocked
+                       message.Subscribed = body.Subscribed
+                       message.ParentUserId = body.ParentUserId
+                       return []interface{}{message}, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return extractor.Execute()
+}
+
+var ExtractThreadMeta = plugin.SubTaskMeta{
+       Name:             "extractThread",
+       EntryPoint:       ExtractThread,
+       EnabledByDefault: true,
+       Description:      "Extract raw thread messages data into tool layer 
table",
+}

Reply via email to