Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support telegram bot #4

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Build
.idea/
build/
vendor

# Configuration
*.yaml
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ networks:
# ---------------------------------------------------------------------------------------------------------------

bot:
token: "<Discord bot token>"
prefix: "!"
name: "<Provider name (discord | telegram)>"
token: "<Bot API token>"
prefix: "<Command prefix (optional - default discord: '!' | telegram: '/' )>"

limitations:
- command: "help"
duration: "72h" # 3 days
duration: "0m" # no limit

- command: "docs"
duration: "15m" # 15 minutes

- command: "send"
duration: "0m" # 7 Days
```
duration: "72h" # 7 Days
```
4 changes: 2 additions & 2 deletions bot/bot.go → bot/discord/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func (bot *Bot) Unreact(msg *disgord.Message, s disgord.Session, emoji interface
// CheckCommandLimit returns the date on which the given user will be able to run the command again
func (bot *Bot) CheckCommandLimit(userID disgord.Snowflake, command string) *time.Time {
// Try getting the expiration date for the command
expirationDate, err := limitations.GetLimitationExpiration(userID, command)
expirationDate, err := limitations.GetLimitationExpiration(userID.String(), command)
if err != nil {
panic(err)
}
Expand All @@ -150,7 +150,7 @@ func (bot *Bot) SetCommandLimitation(userID disgord.Snowflake, cmd string) {
// Set the expiration
commandLimitation := bot.cfg.FindLimitationByCommand(cmd)
if commandLimitation != nil {
err := limitations.SetLimitationExpiration(userID, cmd, time.Now().Add(commandLimitation.Duration))
err := limitations.SetLimitationExpiration(userID.String(), cmd, time.Now().Add(commandLimitation.Duration))
if err != nil {
log.Error().Err(err).Str(types.LogCommand, cmd).Msg("error while setting limitation expiration")
}
Expand Down
37 changes: 2 additions & 35 deletions bot/cmd_connect.go → bot/discord/cmd_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"strings"
"time"

signcmd "github.com/desmos-labs/desmos/v4/app/desmos/cmd/sign"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/libs/json"

"github.com/desmos-labs/hephaestus/utils"
Expand Down Expand Up @@ -79,13 +77,13 @@ Eg. `+"`!%[1]s %[2]s {...}`"+`

// Get the signature data
username := utils.GetMsgAuthorUsername(msg)
signatureData, err := bot.getSignatureData(parts[1])
signatureData, err := utils.GetSignatureData(parts[1])
if err != nil {
return err
}

// Upload the data to Themis
err = networkClient.UploadDataToThemis(username, signatureData)
err = networkClient.UploadDataToThemis(username, bot.cfg.Name, signatureData)
if err != nil {
return err
}
Expand All @@ -108,34 +106,3 @@ Eg. `+"`!%[1]s %[2]s {...}`"+`

return nil
}

func (bot *Bot) getSignatureData(jsonData string) (*signcmd.SignatureData, error) {
var signatureData signcmd.SignatureData
err := json.Unmarshal([]byte(jsonData), &signatureData)
if err != nil {
return nil, types.NewWarnErr("Invalid data provided: %s", err)
}

// Verify the signature
pubKeyBz, err := hex.DecodeString(signatureData.PubKey)
if err != nil {
return nil, types.NewWarnErr("Error while reading public key: %s", err)
}

valueBz, err := hex.DecodeString(signatureData.Value)
if err != nil {
return nil, types.NewWarnErr("Error while reading value: %s", err)
}

sigBz, err := hex.DecodeString(signatureData.Signature)
if err != nil {
return nil, types.NewWarnErr("Error while reading signature: %s", err)
}

pubKey := secp256k1.PubKey(pubKeyBz)
if !pubKey.VerifySignature(valueBz, sigBz) {
return nil, types.NewWarnErr("Invalid signature. Make sure you have signed the message using the correct account")
}

return &signatureData, nil
}
File renamed without changes.
23 changes: 20 additions & 3 deletions bot/handler.go → bot/discord/cmd_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,24 @@ import (
"github.com/desmos-labs/hephaestus/types"
)

// CmdHandler represents a function that extends a disgord.HandlerMessageCreate to allow it to return an error
type CmdHandler = func(s disgord.Session, h *disgord.MessageCreate) error
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move CmdHandler from types folder since telegram and discord have different handler structure.


// MergeHandlers merges all the given handlers into a single one
func MergeHandlers(handlers ...CmdHandler) CmdHandler {
return func(s disgord.Session, data *disgord.MessageCreate) error {
for _, h := range handlers {
err := h(s, data)
if err != nil {
return err
}
}
return nil
}
}

// NewCmdHandler returns a new command handler for the command that has the given name
func (bot *Bot) NewCmdHandler(cmdName string, handler types.CmdHandler) disgord.HandlerMessageCreate {
func (bot *Bot) NewCmdHandler(cmdName string, handler CmdHandler) disgord.HandlerMessageCreate {
return func(s disgord.Session, data *disgord.MessageCreate) {
// Consider only those messages starting with "connect"
msg := data.Message
Expand All @@ -23,7 +39,7 @@ func (bot *Bot) NewCmdHandler(cmdName string, handler types.CmdHandler) disgord.
log.Debug().Str(types.LogCommand, cmdName).Msg("received command")

// Merge the handler with the limit check
mergedHandlers := types.MergeHandlers(bot.checkCmdLimit(cmdName), handler)
mergedHandlers := MergeHandlers(bot.checkCmdLimit(cmdName), handler)

// Handle the message
err := mergedHandlers(s, data)
Expand Down Expand Up @@ -63,7 +79,7 @@ func (bot *Bot) handleError(msg *disgord.Message, s disgord.Session, err error)
}
}

func (bot *Bot) checkCmdLimit(cmdName string) types.CmdHandler {
func (bot *Bot) checkCmdLimit(cmdName string) CmdHandler {
return func(s disgord.Session, data *disgord.MessageCreate) error {
// Check the command limitation
msg := data.Message
Expand All @@ -73,6 +89,7 @@ func (bot *Bot) checkCmdLimit(cmdName string) types.CmdHandler {
expirationDate.Format(time.RFC822),
))
}
bot.SetCommandLimitation(msg.Author.ID, cmdName)
return nil
}
}
File renamed without changes.
1 change: 0 additions & 1 deletion bot/cmd_send.go → bot/discord/cmd_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func (bot *Bot) HandleSendTokens(s disgord.Session, data *disgord.MessageCreate)
}

log.Debug().Str(types.LogRecipient, recipient).Str(LogTxHash, res.TxHash).Msg("tokens sent successfully")
bot.SetCommandLimitation(msg.Author.ID, types.CmdSend)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove it since it should be handled in Handler.

bot.Reply(msg, s, fmt.Sprintf(
"Your tokens have been sent successfully. You can see it by running `desmos q tx %s`."+
"If your balance does not update in the next seconds, make sure your node is synced.", res.TxHash,
Expand Down
2 changes: 1 addition & 1 deletion bot/cmd_verify.go → bot/discord/cmd_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This command allows you to verify your Discord account on this server.
To do this, you have to:

1. Connect your Desmos Profile with your Discord account using the !%[4]s command.
2. Use the %[1]s command to get your Discord role.
2. Use the %[1]s command to get your Discord role.

The !%[1]s command should be used as follow:
`+"`!%[1]s <%[2]s/%[3]s>`"+`
Expand Down
File renamed without changes.
80 changes: 80 additions & 0 deletions bot/telegram/bot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package bot

import (
"strconv"
"time"

"github.com/desmos-labs/hephaestus/limitations"
"github.com/desmos-labs/hephaestus/network"
"github.com/desmos-labs/hephaestus/types"
"github.com/rs/zerolog/log"
telebot "gopkg.in/telebot.v3"
)

// Bot represents the object that should be used to interact with Discord
type Bot struct {
cfg *types.BotConfig
telegram *telebot.Bot

testnet *network.Client
mainnet *network.Client
}

// Create allows to build a new Bot instance
func Create(cfg *types.BotConfig, testnet *network.Client, mainnet *network.Client) (*Bot, error) {
// Set the default prefix if empty
if cfg.Prefix == "" {
cfg.Prefix = "/"
}
bot, err := telebot.NewBot(telebot.Settings{
Token: cfg.Token,
Poller: &telebot.LongPoller{Timeout: 10 * time.Second},
ParseMode: telebot.ModeMarkdown,
})
return &Bot{
cfg: cfg,
telegram: bot,
testnet: testnet,
mainnet: mainnet,
}, err
}

// Start starts the bot so that it can listen to events properly
func (bot *Bot) Start() {
log.Debug().Msg("starting bot")
bot.Handle(types.CmdDocs, bot.HandleDocs)
bot.Handle(types.CmdHelp, bot.HandleHelp)
bot.Handle(types.CmdConnect, bot.HandleConnect)
bot.Handle(types.CmdSend, bot.HandleSendTokens)
log.Debug().Msg("listening for messages...")
bot.telegram.Start()
}

// CheckCommandLimit returns the date on which the given user will be able to run the command again
func (bot *Bot) CheckCommandLimit(userID int64, command string) *time.Time {
// Try getting the expiration date for the command
expirationDate, err := limitations.GetLimitationExpiration(strconv.FormatInt(userID, 10), command)
if err != nil {
panic(err)
}

// Check if the user is blocked
if expirationDate != nil && time.Now().Before(*expirationDate) {
log.Debug().Str(types.LogCommand, command).Time(types.LogExpirationEnd, *expirationDate).Msg("user is limited")
return expirationDate
}

return nil
}

// SetCommandLimitation sets the limitation for the given user for the provided command
func (bot *Bot) SetCommandLimitation(userID int64, cmd string) {
// Set the expiration
commandLimitation := bot.cfg.FindLimitationByCommand(cmd)
if commandLimitation != nil {
err := limitations.SetLimitationExpiration(strconv.FormatInt(userID, 10), cmd, time.Now().Add(commandLimitation.Duration))
if err != nil {
log.Error().Err(err).Str(types.LogCommand, cmd).Msg("error while setting limitation expiration")
}
}
}
95 changes: 95 additions & 0 deletions bot/telegram/cmd_connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package bot

import (
"encoding/hex"
"encoding/json"
"fmt"
"time"

"github.com/desmos-labs/hephaestus/types"
"github.com/desmos-labs/hephaestus/utils"
telebot "gopkg.in/telebot.v3"
)

type CallData struct {
// Username is the plain-text Telegram username of the user that wants to be verified
Username string `json:"username"`
}

// NewCallData returns a new CallData instance
func NewCallData(username string) *CallData {
return &CallData{
Username: username,
}
}

// HandleConnect handle a connection request. This request is done by the users when they want to connect their Desmos
// profile with their Telegram account.
// The command expects one single argument which must be the JSON object returned from the "desmos sign" command.
//
// The handling of the command will fail in the following occasions:
// 1. The signed value does not correspond to the username of the user sending the message
// 2. Any of the values are badly encoded
func (bot *Bot) HandleConnect(ctx telebot.Context) error {
parts := ctx.Args()
if len(parts) != 2 {
ctx.Reply(fmt.Sprintf(`**Connect**
This command allows you to connect your Telegram account to your Desmos profile.
To do this, you have to:

1. Sign your Telegram username using the Desmos CLI or any Desmos-compatible application.
2. Use the %[1]s command to send the signature result.

__Signing your Telegram username__
1. Copy your Telegram username by clicking on it in the bottom part of your Telegram client.

2. Open your Desmos CLI or application, and sign your username.
If you use the Desmos CLI, you can do this by using the following command:
`+"`desmos sign <Telegram username> --from <your-key>`"+`

Eg. `+"`desmos sign \"foo_123\" --from foo`"+`

__Sending the signed value__
The sign command should return a JSON object. The last thing you have to do is now send it to me using the %[1]s command. To do this, simply send me a message as the following:
`+"`/%[1]s <%[2]s/%[3]s> <JSON>`"+`

Eg. `+"`/%[1]s %[2]s {...}`"+`
`, types.CmdConnect, types.NetworkTestnet, types.NetworkMainnet))
return nil
}

// Get the network client to be used
var networkClient = bot.testnet
if parts[0] == types.NetworkMainnet {
networkClient = bot.mainnet
}

// Get the signature data
username := ctx.Sender().Username
signatureData, err := utils.GetSignatureData(parts[1])
if err != nil {
return err
}

// Upload the data to Themis
err = networkClient.UploadDataToThemis(username, bot.cfg.Name, signatureData)
if err != nil {
return err
}

// Return to the user the call data for the Desmos command
callDataBz, err := json.Marshal(NewCallData(username))
if err != nil {
return types.NewWarnErr("Error while serializing call data: %s", err)
}
ctx.Reply(fmt.Sprintf("Your verification data has been stored successfully. "+
"All you have to do now is execute the following command:\n"+
"```\n"+
"desmos tx profiles link-app ibc-profiles [channel] telegram \"%[1]s\" %[2]s --packet-timeout-height 0-0 --packet-timeout-timestamp %[3]d --from <key_name>"+
"```",
username,
hex.EncodeToString(callDataBz),
time.Now().Add(time.Hour).UnixNano(),
))
return nil
}
19 changes: 19 additions & 0 deletions bot/telegram/cmd_docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package bot

import (
"fmt"

telebot "gopkg.in/telebot.v3"
)

// HandleDocs handles the the request for docs by the user
func (bot *Bot) HandleDocs(ctx telebot.Context) error {
// Answer to the command
ctx.Reply(fmt.Sprintf(
"Here are a series of useful links:\n"+
"- General documentation: %s\n"+
"- Become a validator: %s",
"https://docs.desmos.network/",
"https://docs.desmos.network/validators/setup.html"))
return nil
}
Loading