Skip to content

Commit 66a3974

Browse files
authored
Add the slack platform support (#71)
1 parent bd0eec6 commit 66a3974

File tree

6 files changed

+285
-0
lines changed

6 files changed

+285
-0
lines changed

config/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type PlatformConfig struct {
3232
Telegram TelegramPlatformConfig `json:"telegram"`
3333
Ethereum EthereumPlatformConfig `json:"ethereum"`
3434
Discord DiscordPlatformConfig `json:"discord"`
35+
Slack SlackPlatformConfig `json:"slack"`
3536
}
3637

3738
type ArweaveConfig struct {
@@ -57,6 +58,11 @@ type TelegramPlatformConfig struct {
5758
PublicChannelName string `json:"public_channel_name"`
5859
}
5960

61+
type SlackPlatformConfig struct {
62+
ApiToken string `json:"api_token"`
63+
PublicChannelID string `json:"public_channel_id"`
64+
}
65+
6066
type EthereumPlatformConfig struct {
6167
RPCServer string `json:"rpc_server"`
6268
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ require (
108108
github.com/segmentio/asm v1.2.0 // indirect
109109
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
110110
github.com/shopspring/decimal v1.3.1 // indirect
111+
github.com/slack-go/slack v0.12.1 // indirect
111112
github.com/spaolacci/murmur3 v1.1.0 // indirect
112113
github.com/spf13/afero v1.8.2 // indirect
113114
github.com/spf13/cast v1.4.1 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
260260
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
261261
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
262262
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
263+
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
263264
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
264265
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
265266
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
@@ -651,6 +652,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
651652
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
652653
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
653654
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
655+
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
656+
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
654657
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
655658
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
656659
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=

types/platform.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var Platforms = struct {
1818
ENS Platform
1919
Steam Platform
2020
ActivityPub Platform
21+
Slack Platform
2122
}{
2223
Github: "github",
2324
NextID: "nextid",
@@ -33,4 +34,5 @@ var Platforms = struct {
3334
ENS: "ens",
3435
Steam: "steam",
3536
ActivityPub: "activitypub",
37+
Slack: "slack",
3638
}

validator/slack/slack.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package slack
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"fmt"
7+
"net/url"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
12+
"github.com/nextdotid/proof_server/config"
13+
types "github.com/nextdotid/proof_server/types"
14+
util "github.com/nextdotid/proof_server/util"
15+
mycrypto "github.com/nextdotid/proof_server/util/crypto"
16+
"github.com/sirupsen/logrus"
17+
"github.com/slack-go/slack"
18+
slackClient "github.com/slack-go/slack"
19+
"golang.org/x/xerrors"
20+
21+
validator "github.com/nextdotid/proof_server/validator"
22+
)
23+
24+
// Slack represents the validator for Slack platform
25+
type Slack struct {
26+
*validator.Base
27+
}
28+
29+
const (
30+
matchTemplate = "^Sig: (.*)$"
31+
)
32+
33+
var (
34+
client *slackClient.Client
35+
l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "slack"})
36+
re = regexp.MustCompile(matchTemplate)
37+
postStruct = map[string]string{
38+
"default": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
39+
"en_US": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
40+
"zh_CN": "🎭 正在通过 @NextDotID 验证我的 Slack 帐号 @%s 。\nSig: %%SIG_BASE64%%\n\n由 Next.ID 支持 - 连接全域数字身份。\n",
41+
}
42+
)
43+
44+
// Init initializes the Slack validator
45+
func Init() {
46+
initClient()
47+
if validator.PlatformFactories == nil {
48+
validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator)
49+
}
50+
validator.PlatformFactories[types.Platforms.Slack] = func(base *validator.Base) validator.IValidator {
51+
slack := &Slack{base}
52+
return slack
53+
}
54+
}
55+
56+
// GeneratePostPayload generates the post payload for Slack
57+
func (s *Slack) GeneratePostPayload() (post map[string]string) {
58+
post = make(map[string]string)
59+
for langCode, template := range postStruct {
60+
post[langCode] = fmt.Sprintf(template, s.Identity)
61+
}
62+
return post
63+
}
64+
65+
// GenerateSignPayload generates the signature payload for Slack
66+
func (slack *Slack) GenerateSignPayload() (payload string) {
67+
payloadStruct := validator.H{
68+
"action": string(slack.Action),
69+
"identity": slack.Identity,
70+
"platform": "slack",
71+
"created_at": util.TimeToTimestampString(slack.CreatedAt),
72+
"uuid": slack.Uuid.String(),
73+
}
74+
if slack.Previous != "" {
75+
payloadStruct["prev"] = slack.Previous
76+
}
77+
78+
payloadBytes, err := json.Marshal(payloadStruct)
79+
if err != nil {
80+
l.Warnf("Error when marshaling struct: %s", err.Error())
81+
return ""
82+
}
83+
84+
return string(payloadBytes)
85+
}
86+
87+
func (slack *Slack) Validate() (err error) {
88+
initClient()
89+
slack.Identity = strings.ToLower(slack.Identity)
90+
slack.SignaturePayload = slack.GenerateSignPayload()
91+
92+
if slack.Action == types.Actions.Delete {
93+
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, slack.Signature, slack.Pubkey)
94+
}
95+
96+
u, err := url.Parse(slack.ProofLocation)
97+
if err != nil {
98+
return xerrors.Errorf("Error when parsing slack proof location: %v", err)
99+
}
100+
msgPath := strings.Trim(u.Path, "/")
101+
parts := strings.Split(msgPath, "/")
102+
if len(parts) != 2 {
103+
return xerrors.Errorf("Error: malformatted slack proof location: %v", slack.ProofLocation)
104+
}
105+
channelID := parts[0]
106+
messageID, err := strconv.ParseInt(parts[1], 10, 64)
107+
if err != nil {
108+
return xerrors.Errorf("Error when parsing slack message ID %s: %s", slack.ProofLocation, err.Error())
109+
}
110+
const (
111+
maxPages = 10 // maximum number of pages to fetch
112+
historyPageSize = 1000 // Slack API max limit per page
113+
)
114+
pageCount := 0
115+
var foundMsg *slackClient.Message
116+
var latestTs string
117+
118+
for {
119+
if pageCount >= maxPages {
120+
return xerrors.Errorf("Reached max number of pages (%d) while searching for message with ID %d in conversation history", maxPages, messageID)
121+
}
122+
123+
// Get conversation history
124+
history, err := client.GetConversationHistory(&slackClient.GetConversationHistoryParameters{
125+
ChannelID: channelID,
126+
Latest: latestTs,
127+
Inclusive: true,
128+
Oldest: "",
129+
Limit: historyPageSize,
130+
})
131+
if err != nil {
132+
return xerrors.Errorf("Error getting the conversation history from slack: %w", err)
133+
}
134+
135+
for _, msg := range history.Messages {
136+
if msg.ClientMsgID == strconv.FormatInt(messageID, 10) {
137+
foundMsg = &msg
138+
break
139+
}
140+
}
141+
142+
if foundMsg != nil {
143+
break
144+
}
145+
146+
if !history.HasMore {
147+
return xerrors.Errorf("Could not find message with ID %d in conversation history", messageID)
148+
}
149+
150+
latestTs = history.Messages[len(history.Messages)-1].Timestamp
151+
pageCount++
152+
}
153+
154+
userInt, err := strconv.ParseInt(foundMsg.User, 10, 64)
155+
if err != nil {
156+
return xerrors.Errorf("failed to parse user ID as int64: %v", err)
157+
}
158+
userID := strconv.FormatInt(userInt, 10)
159+
if !strings.EqualFold(userID, slack.Identity) {
160+
return xerrors.Errorf("slack userID mismatch: expect %s - actual %s", slack.Identity, userID)
161+
}
162+
163+
slack.Text = foundMsg.Text
164+
slack.AltID = userID
165+
slack.Identity = userID
166+
167+
return slack.validateText()
168+
}
169+
170+
func (slack *Slack) validateText() (err error) {
171+
scanner := bufio.NewScanner(strings.NewReader(slack.Text))
172+
for scanner.Scan() {
173+
matched := re.FindStringSubmatch(scanner.Text())
174+
if len(matched) < 2 {
175+
continue // Search for next line
176+
}
177+
178+
sigBase64 := matched[1]
179+
sigBytes, err := util.DecodeString(sigBase64)
180+
if err != nil {
181+
return xerrors.Errorf("Error when decoding signature %s: %s", sigBase64, err.Error())
182+
}
183+
slack.Signature = sigBytes
184+
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, sigBytes, slack.Pubkey)
185+
}
186+
return xerrors.Errorf("Signature not found in the slack message.")
187+
}
188+
189+
func initClient() {
190+
if client != nil {
191+
return
192+
}
193+
client = slack.New(config.C.Platform.Slack.ApiToken)
194+
if _, err := client.AuthTest(); err != nil {
195+
panic(fmt.Errorf("failed to authenticate the slack: %v", err))
196+
}
197+
}

validator/slack/slack_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package slack
2+
3+
import (
4+
"strconv"
5+
"testing"
6+
7+
"github.com/google/uuid"
8+
"github.com/nextdotid/proof_server/config"
9+
"github.com/nextdotid/proof_server/types"
10+
"github.com/nextdotid/proof_server/util"
11+
"github.com/nextdotid/proof_server/util/crypto"
12+
"github.com/nextdotid/proof_server/validator"
13+
"github.com/sirupsen/logrus"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func before_each(t *testing.T) {
18+
logrus.SetLevel(logrus.DebugLevel)
19+
config.Init("../../config/config.test.json")
20+
}
21+
22+
func generate() Slack {
23+
pubkey, _ := crypto.StringToPubkey("0x4ec73e36f64ea6e2aa28c101dcae56203e02bd56b4b08c7848b5e791c7bfb9ca2b30f657bd822756533731e201faf57a0aaf6af36bd51f921f7132c9830c6fdf")
24+
created_at, _ := util.TimestampStringToTime("1677339048")
25+
uuid := uuid.MustParse("5032b8b3-d91d-434e-be3f-f172267e4006")
26+
27+
return Slack{
28+
Base: &validator.Base{
29+
Platform: types.Platforms.Slack,
30+
Previous: "",
31+
Action: types.Actions.Create,
32+
Pubkey: pubkey,
33+
Identity: "ashfaqur",
34+
ProofLocation: "https://ashfaqur.slack.com/archives/C04Q3P6H7TK/p1677499644698189",
35+
CreatedAt: created_at,
36+
Uuid: uuid,
37+
},
38+
}
39+
}
40+
41+
func Test_GeneratePostPayload(t *testing.T) {
42+
t.Run("success", func(t *testing.T) {
43+
before_each(t)
44+
45+
slack := generate()
46+
post := slack.GeneratePostPayload()
47+
post_default, ok := post["default"]
48+
require.True(t, ok)
49+
require.Contains(t, post_default, "Verifying my Slack ID")
50+
require.Contains(t, post_default, slack.Identity)
51+
require.Contains(t, post_default, slack.Uuid.String())
52+
require.Contains(t, post_default, "%SIG_BASE64%")
53+
})
54+
}
55+
56+
func Test_GenerateSignPayload(t *testing.T) {
57+
t.Run("success", func(t *testing.T) {
58+
before_each(t)
59+
60+
slack := generate()
61+
payload := slack.GenerateSignPayload()
62+
require.Contains(t, payload, slack.Uuid.String())
63+
require.Contains(t, payload, strconv.FormatInt(slack.CreatedAt.Unix(), 10))
64+
require.Contains(t, payload, slack.Identity)
65+
})
66+
}
67+
68+
func Test_Validate(t *testing.T) {
69+
t.Run("success", func(t *testing.T) {
70+
before_each(t)
71+
72+
slack := generate()
73+
require.NoError(t, slack.Validate())
74+
require.Equal(t, "U04Q3NRDWHX", slack.AltID)
75+
})
76+
}

0 commit comments

Comments
 (0)