user-center-slack/importer.go (151 lines of code) (raw):
/*
* 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 slack_user_center
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/apache/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
func (uc *UserCenter) parseText(text string) (string, string, []string, error) {
re := regexp.MustCompile(`\[(.*?)\]`)
matches := re.FindAllStringSubmatch(text, -1)
if len(matches) != 3 {
return "", "", nil, fmt.Errorf("text field does not conform to the required format")
}
part1 := matches[0][1]
part2 := matches[1][1]
rawTags := strings.Split(matches[2][1], ",")
var tags []string
for _, tag := range rawTags {
if tag != "" {
tags = append(tags, tag)
}
}
// if part1 or part2 or tags in empty return error
if part1 == "" || part2 == "" || len(tags) == 0 {
return "", "", nil, fmt.Errorf("text field does not be empty")
}
return part1, part2, tags, nil
}
func getSlackUserEmail(userID, token string) (string, error) {
url := fmt.Sprintf("https://slack.com/api/users.info?user=%s", userID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var userResponse SlackUserResponse
if err := json.Unmarshal(body, &userResponse); err != nil {
return "", err
}
if !userResponse.Ok {
return "", fmt.Errorf("failed to get user info from Slack")
}
return userResponse.User.Profile.Email, nil
}
func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error {
body, err := io.ReadAll(ctx.Request.Body)
if err != nil {
return fmt.Errorf("could not read request body: %v", err)
}
timestamp := ctx.GetHeader("X-Slack-Request-Timestamp")
slackSignature := ctx.GetHeader("X-Slack-Signature")
// check the timestamp validity in 5 minutes
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp: %v", err)
}
if time.Now().Unix()-ts > 60*5 {
return fmt.Errorf("timestamp is too old")
}
// Reset the request body for further processing
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
sigBaseString := fmt.Sprintf("v0:%s:%s", timestamp, string(body))
h := hmac.New(sha256.New, []byte(uc.Config.SigningSecret))
h.Write([]byte(sigBaseString))
computedSignature := "v0=" + hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(computedSignature), []byte(slackSignature)) {
return fmt.Errorf("invalid signature")
}
return nil
}
func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo plugin.QuestionImporterInfo, err error) {
questionInfo = plugin.QuestionImporterInfo{}
err = uc.verifySlackRequest(ctx)
if err != nil {
return questionInfo, err
}
text := ctx.PostForm("text")
part1, part2, tags, err := uc.parseText(text)
if err != nil {
return questionInfo, err
}
questionInfo.Title = part1
questionInfo.Content = part2
questionInfo.Tags = tags
userID := ctx.PostForm("user_id")
token := uc.SlackClient.AccessToken
email, err := getSlackUserEmail(userID, token)
if err != nil {
return questionInfo, err
}
questionInfo.UserEmail = email
return questionInfo, nil
}
func (uc *UserCenter) SlashCommand(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
cmd := ctx.PostForm("command")
if cmd != "/ask" {
log.Errorf("error: Invalid command")
ctx.JSON(http.StatusBadRequest, gin.H{"text": "Invalid command"})
return
}
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
err := uc.verifySlackRequest(ctx)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"text": "Slack request verification faild"})
log.Errorf("error: %v", err)
return
}
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
questionInfo, err := uc.GetQuestion(ctx)
if err != nil {
log.Errorf("error: %v", err)
ctx.JSON(200, gin.H{"text": err.Error()})
return
}
if uc.importerFunc == nil {
log.Errorf("error: importerFunc is not initialized")
return
}
err = uc.importerFunc.AddQuestion(ctx, questionInfo)
if err != nil {
log.Errorf("error: %v", err)
ctx.JSON(http.StatusBadRequest, gin.H{"text": "Failed to add question"})
return
}
ctx.JSON(http.StatusOK, gin.H{"text": "Question has been added successfully"})
}
func (uc *UserCenter) RegisterImporterFunc(ctx context.Context, importerFunc plugin.ImporterFunc) {
uc.importerFunc = importerFunc
}