Skip to content

Commit

Permalink
Add the slack platform support (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
ash0521 committed Feb 28, 2023
1 parent bd0eec6 commit 66a3974
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 0 deletions.
6 changes: 6 additions & 0 deletions config/main.go
Expand Up @@ -32,6 +32,7 @@ type PlatformConfig struct {
Telegram TelegramPlatformConfig `json:"telegram"`
Ethereum EthereumPlatformConfig `json:"ethereum"`
Discord DiscordPlatformConfig `json:"discord"`
Slack SlackPlatformConfig `json:"slack"`
}

type ArweaveConfig struct {
Expand All @@ -57,6 +58,11 @@ type TelegramPlatformConfig struct {
PublicChannelName string `json:"public_channel_name"`
}

type SlackPlatformConfig struct {
ApiToken string `json:"api_token"`
PublicChannelID string `json:"public_channel_id"`
}

type EthereumPlatformConfig struct {
RPCServer string `json:"rpc_server"`
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -108,6 +108,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/slack-go/slack v0.12.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Expand Up @@ -260,6 +260,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
Expand Down Expand Up @@ -651,6 +652,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
Expand Down
2 changes: 2 additions & 0 deletions types/platform.go
Expand Up @@ -18,6 +18,7 @@ var Platforms = struct {
ENS Platform
Steam Platform
ActivityPub Platform
Slack Platform
}{
Github: "github",
NextID: "nextid",
Expand All @@ -33,4 +34,5 @@ var Platforms = struct {
ENS: "ens",
Steam: "steam",
ActivityPub: "activitypub",
Slack: "slack",
}
197 changes: 197 additions & 0 deletions validator/slack/slack.go
@@ -0,0 +1,197 @@
package slack

import (
"bufio"
"encoding/json"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"

"github.com/nextdotid/proof_server/config"
types "github.com/nextdotid/proof_server/types"
util "github.com/nextdotid/proof_server/util"
mycrypto "github.com/nextdotid/proof_server/util/crypto"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
slackClient "github.com/slack-go/slack"
"golang.org/x/xerrors"

validator "github.com/nextdotid/proof_server/validator"
)

// Slack represents the validator for Slack platform
type Slack struct {
*validator.Base
}

const (
matchTemplate = "^Sig: (.*)$"
)

var (
client *slackClient.Client
l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "slack"})
re = regexp.MustCompile(matchTemplate)
postStruct = map[string]string{
"default": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
"en_US": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
"zh_CN": "🎭 正在通过 @NextDotID 验证我的 Slack 帐号 @%s 。\nSig: %%SIG_BASE64%%\n\n由 Next.ID 支持 - 连接全域数字身份。\n",
}
)

// Init initializes the Slack validator
func Init() {
initClient()
if validator.PlatformFactories == nil {
validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator)
}
validator.PlatformFactories[types.Platforms.Slack] = func(base *validator.Base) validator.IValidator {
slack := &Slack{base}
return slack
}
}

// GeneratePostPayload generates the post payload for Slack
func (s *Slack) GeneratePostPayload() (post map[string]string) {
post = make(map[string]string)
for langCode, template := range postStruct {
post[langCode] = fmt.Sprintf(template, s.Identity)
}
return post
}

// GenerateSignPayload generates the signature payload for Slack
func (slack *Slack) GenerateSignPayload() (payload string) {
payloadStruct := validator.H{
"action": string(slack.Action),
"identity": slack.Identity,
"platform": "slack",
"created_at": util.TimeToTimestampString(slack.CreatedAt),
"uuid": slack.Uuid.String(),
}
if slack.Previous != "" {
payloadStruct["prev"] = slack.Previous
}

payloadBytes, err := json.Marshal(payloadStruct)
if err != nil {
l.Warnf("Error when marshaling struct: %s", err.Error())
return ""
}

return string(payloadBytes)
}

func (slack *Slack) Validate() (err error) {
initClient()
slack.Identity = strings.ToLower(slack.Identity)
slack.SignaturePayload = slack.GenerateSignPayload()

if slack.Action == types.Actions.Delete {
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, slack.Signature, slack.Pubkey)
}

u, err := url.Parse(slack.ProofLocation)
if err != nil {
return xerrors.Errorf("Error when parsing slack proof location: %v", err)
}
msgPath := strings.Trim(u.Path, "/")
parts := strings.Split(msgPath, "/")
if len(parts) != 2 {
return xerrors.Errorf("Error: malformatted slack proof location: %v", slack.ProofLocation)
}
channelID := parts[0]
messageID, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return xerrors.Errorf("Error when parsing slack message ID %s: %s", slack.ProofLocation, err.Error())
}
const (
maxPages = 10 // maximum number of pages to fetch
historyPageSize = 1000 // Slack API max limit per page
)
pageCount := 0
var foundMsg *slackClient.Message
var latestTs string

for {
if pageCount >= maxPages {
return xerrors.Errorf("Reached max number of pages (%d) while searching for message with ID %d in conversation history", maxPages, messageID)
}

// Get conversation history
history, err := client.GetConversationHistory(&slackClient.GetConversationHistoryParameters{
ChannelID: channelID,
Latest: latestTs,
Inclusive: true,
Oldest: "",
Limit: historyPageSize,
})
if err != nil {
return xerrors.Errorf("Error getting the conversation history from slack: %w", err)
}

for _, msg := range history.Messages {
if msg.ClientMsgID == strconv.FormatInt(messageID, 10) {
foundMsg = &msg
break
}
}

if foundMsg != nil {
break
}

if !history.HasMore {
return xerrors.Errorf("Could not find message with ID %d in conversation history", messageID)
}

latestTs = history.Messages[len(history.Messages)-1].Timestamp
pageCount++
}

userInt, err := strconv.ParseInt(foundMsg.User, 10, 64)
if err != nil {
return xerrors.Errorf("failed to parse user ID as int64: %v", err)
}
userID := strconv.FormatInt(userInt, 10)
if !strings.EqualFold(userID, slack.Identity) {
return xerrors.Errorf("slack userID mismatch: expect %s - actual %s", slack.Identity, userID)
}

slack.Text = foundMsg.Text
slack.AltID = userID
slack.Identity = userID

return slack.validateText()
}

func (slack *Slack) validateText() (err error) {
scanner := bufio.NewScanner(strings.NewReader(slack.Text))
for scanner.Scan() {
matched := re.FindStringSubmatch(scanner.Text())
if len(matched) < 2 {
continue // Search for next line
}

sigBase64 := matched[1]
sigBytes, err := util.DecodeString(sigBase64)
if err != nil {
return xerrors.Errorf("Error when decoding signature %s: %s", sigBase64, err.Error())
}
slack.Signature = sigBytes
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, sigBytes, slack.Pubkey)
}
return xerrors.Errorf("Signature not found in the slack message.")
}

func initClient() {
if client != nil {
return
}
client = slack.New(config.C.Platform.Slack.ApiToken)
if _, err := client.AuthTest(); err != nil {
panic(fmt.Errorf("failed to authenticate the slack: %v", err))
}
}
76 changes: 76 additions & 0 deletions validator/slack/slack_test.go
@@ -0,0 +1,76 @@
package slack

import (
"strconv"
"testing"

"github.com/google/uuid"
"github.com/nextdotid/proof_server/config"
"github.com/nextdotid/proof_server/types"
"github.com/nextdotid/proof_server/util"
"github.com/nextdotid/proof_server/util/crypto"
"github.com/nextdotid/proof_server/validator"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)

func before_each(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
config.Init("../../config/config.test.json")
}

func generate() Slack {
pubkey, _ := crypto.StringToPubkey("0x4ec73e36f64ea6e2aa28c101dcae56203e02bd56b4b08c7848b5e791c7bfb9ca2b30f657bd822756533731e201faf57a0aaf6af36bd51f921f7132c9830c6fdf")
created_at, _ := util.TimestampStringToTime("1677339048")
uuid := uuid.MustParse("5032b8b3-d91d-434e-be3f-f172267e4006")

return Slack{
Base: &validator.Base{
Platform: types.Platforms.Slack,
Previous: "",
Action: types.Actions.Create,
Pubkey: pubkey,
Identity: "ashfaqur",
ProofLocation: "https://ashfaqur.slack.com/archives/C04Q3P6H7TK/p1677499644698189",
CreatedAt: created_at,
Uuid: uuid,
},
}
}

func Test_GeneratePostPayload(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
post := slack.GeneratePostPayload()
post_default, ok := post["default"]
require.True(t, ok)
require.Contains(t, post_default, "Verifying my Slack ID")
require.Contains(t, post_default, slack.Identity)
require.Contains(t, post_default, slack.Uuid.String())
require.Contains(t, post_default, "%SIG_BASE64%")
})
}

func Test_GenerateSignPayload(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
payload := slack.GenerateSignPayload()
require.Contains(t, payload, slack.Uuid.String())
require.Contains(t, payload, strconv.FormatInt(slack.CreatedAt.Unix(), 10))
require.Contains(t, payload, slack.Identity)
})
}

func Test_Validate(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
require.NoError(t, slack.Validate())
require.Equal(t, "U04Q3NRDWHX", slack.AltID)
})
}

0 comments on commit 66a3974

Please sign in to comment.