Skip to content

Commit

Permalink
Implement short deep links to start the bot
Browse files Browse the repository at this point in the history
Store CreatedAt for users
Use sqids library to generate short links using a random LinkKey number and CreatedAt unix timestamps
Update guregu/dynamo library
  • Loading branch information
bugfloyd committed May 4, 2024
1 parent 178e510 commit a84108f
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Terraform apply
run: |
cd infra
terraform apply -auto-approve -var aws_region=${{ vars.AWS_REGION }} -var lambda_bucket=${{ vars.S3_LAMBDA_BUCKET }} -var bot_token=${{ secrets.BOT_TOKEN }}
terraform apply -auto-approve -var aws_region=${{ vars.AWS_REGION }} -var lambda_bucket=${{ vars.S3_LAMBDA_BUCKET }} -var bot_token=${{ secrets.BOT_TOKEN }} -var sqids_alphabet=${{ secrets.SQIDS_ALPHABET }}
- name: Set webhook URL
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Initialize the main Terraform stack:
```shell
cd infra

AWS_PROFILE=<AWS_PROFILE> init -backend-config backend_config.hcl # Run once
AWS_PROFILE=<AWS_PROFILE> terraform init -backend-config backend_config.hcl # Run once
```

#### Deploy
Expand Down
6 changes: 6 additions & 0 deletions bot/common/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ func InitBot(request APIRequest) (APIResponse, error) {
return APIResponse{StatusCode: 500}, errors.New("TOKEN environment variable is empty")
}

// Get sqids alphabet from the environment variable.
alphabet := os.Getenv("SQIDS_ALPHABET")
if alphabet == "" {
return APIResponse{StatusCode: 500}, errors.New("SQIDS_ALPHABET environment variable is empty")
}

// Create bot from environment value.
b, err := gotgbot.NewBot(token, &gotgbot.BotOpts{
BotClient: &gotgbot.BaseBotClient{
Expand Down
63 changes: 47 additions & 16 deletions bot/common/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"github.com/PaulSonOfLars/gotgbot/v2"
"github.com/PaulSonOfLars/gotgbot/v2/ext"
"github.com/bugfloyd/anonymous-telegram-bot/common/i18n"
"github.com/sqids/sqids-go"
"os"
"strings"
)

Expand Down Expand Up @@ -36,22 +38,33 @@ func (r *RootHandler) start(b *gotgbot.Bot, ctx *ext.Context) error {
return nil
}
if len(args) == 2 && args[0] == "/start" {

var err error
var receiverUser *User
var identity string

if strings.HasPrefix(args[1], "_") {
username := args[1][1:]
receiverUser, err = r.userRepo.readUserByUsername(username)
if err != nil {
return fmt.Errorf("failed to retrieve the link owner: %w", err)
}
identity = receiverUser.Username
} else {
receiverUser, err = r.userRepo.readUserByUUID(args[1])
linkKey, createdAt, err := readUserLinkKey(args[1])
if err != nil {
return fmt.Errorf("failed to read the link key: %w", err)
}
receiverUser, err = r.userRepo.readUserByLinkKey(linkKey, createdAt)
if err != nil {
return fmt.Errorf("failed to retrieve the link owner: %w", err)
}
identity = args[1]
}

if err != nil || receiverUser == nil {
if receiverUser == nil {
_, err = b.SendMessage(ctx.EffectiveChat.Id, i18n.T(i18n.UserNotFoundText), &gotgbot.SendMessageOpts{})
if err != nil {
return fmt.Errorf("failed to send bot info: %w", err)
return fmt.Errorf("failed to send wrong link response: %w", err)
}
return nil
}
Expand Down Expand Up @@ -106,14 +119,6 @@ func (r *RootHandler) start(b *gotgbot.Bot, ctx *ext.Context) error {
return fmt.Errorf("failed to update user state: %w", err)
}

if receiverUser.Name != "" {
identity = receiverUser.Name
} else if receiverUser.Username != "" {
identity = receiverUser.Username
} else {
identity = receiverUser.UUID
}

_, err = b.SendMessage(ctx.EffectiveChat.Id, fmt.Sprintf(i18n.T(i18n.InitialSendMessagePromptText), identity), &gotgbot.SendMessageOpts{})
if err != nil {
return fmt.Errorf("failed to send bot info: %w", err)
Expand All @@ -136,15 +141,24 @@ func (r *RootHandler) info(b *gotgbot.Bot, ctx *ext.Context) error {
}

func (r *RootHandler) getLink(b *gotgbot.Bot, ctx *ext.Context) error {
alphabet := os.Getenv("SQIDS_ALPHABET")
s, _ := sqids.New(sqids.Options{
Alphabet: alphabet,
})
genericLinkKey, err := s.Encode([]uint64{uint64(r.user.LinkKey), uint64(r.user.CreatedAt.Unix())})
if err != nil {
return err
}

var link string
if r.user.Username != "" {
usernameLink := fmt.Sprintf("https://t.me/%s?start=_%s", b.User.Username, r.user.Username)
uuidLink := fmt.Sprintf("https://t.me/%s?start=%s", b.User.Username, r.user.UUID)
link = fmt.Sprintf("%s\n%s\n\n%s\n\n%s", i18n.T(i18n.LinkText), usernameLink, i18n.T(i18n.OrText), uuidLink)
genericLink := fmt.Sprintf("https://t.me/%s?start=%s", b.User.Username, genericLinkKey)
link = fmt.Sprintf("%s\n%s\n\n%s\n\n%s", i18n.T(i18n.LinkText), usernameLink, i18n.T(i18n.OrText), genericLink)
} else {
link = fmt.Sprintf("https://t.me/%s?start=%s", b.User.Username, r.user.UUID)
link = fmt.Sprintf("https://t.me/%s?start=%s", b.User.Username, genericLinkKey)
}
_, err := ctx.EffectiveMessage.Reply(b, link, nil)
_, err = ctx.EffectiveMessage.Reply(b, link, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -174,3 +188,20 @@ func (r *RootHandler) sendError(b *gotgbot.Bot, ctx *ext.Context, message string
}
return nil
}

func readUserLinkKey(link string) (int32, int64, error) {
alphabet := os.Getenv("SQIDS_ALPHABET")
s, err := sqids.New(sqids.Options{
Alphabet: alphabet,
})

if err != nil {
return 0, 0, fmt.Errorf("failed to read user link key: %w", err)
}

numbers := s.Decode(link)
if len(numbers) != 2 {
return 0, 0, fmt.Errorf("failed to read user link key")
}
return int32(numbers[0]), int64(numbers[1]), nil
}
10 changes: 7 additions & 3 deletions bot/common/user.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package common

import "github.com/bugfloyd/anonymous-telegram-bot/common/i18n"
import (
"github.com/bugfloyd/anonymous-telegram-bot/common/i18n"
"time"
)

type User struct {
UUID string `dynamo:",hash"`
UserID int64 `index:"UserID-GSI,hash"`
Username string `index:"Username-GSI,hash"`
State State
Name string
Blacklist []string `dynamo:",set,omitempty"`
Blacklist []string `dynamo:",set,omitempty,omitemptyelem"`
ContactUUID string `dynamo:",omitempty"`
ReplyMessageID int64 `dynamo:",omitempty"`
Language i18n.Language `dynamo:",omitempty"`
LinkKey int32 `index:"LinkKey-GSI,hash"`
CreatedAt time.Time `dynamo:",unixtime" index:"LinkKey-GSI,range"`
}

type State string
Expand Down
19 changes: 16 additions & 3 deletions bot/common/user_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package common

import (
"fmt"
"math/rand"
"os"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -37,9 +39,11 @@ func NewUserRepository() (*UserRepository, error) {

func (repo *UserRepository) createUser(userId int64) (*User, error) {
u := User{
UUID: uuid.New().String(),
UserID: userId,
State: Idle,
UUID: uuid.New().String(),
UserID: userId,
State: Idle,
LinkKey: int32(rand.Intn(900000) + 100000),
CreatedAt: time.Now(),
}
err := repo.table.Put(u).Run()
if err != nil {
Expand Down Expand Up @@ -75,6 +79,15 @@ func (repo *UserRepository) readUserByUsername(username string) (*User, error) {
return &u, nil
}

func (repo *UserRepository) readUserByLinkKey(linkKey int32, createdAt int64) (*User, error) {
var u User
err := repo.table.Get("LinkKey", linkKey).Index("LinkKey-GSI").Range("CreatedAt", dynamo.Equal, createdAt).One(&u)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &u, nil
}

func (repo *UserRepository) updateUser(uuid string, updates map[string]interface{}) error {
updateBuilder := repo.table.Update("UUID", uuid)
for key, value := range updates {
Expand Down
3 changes: 2 additions & 1 deletion bot/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ require (
github.com/aws/aws-lambda-go v1.47.0
github.com/aws/aws-sdk-go v1.51.25
github.com/google/uuid v1.6.0
github.com/guregu/dynamo v1.22.0
github.com/guregu/dynamo v1.22.1
github.com/sqids/sqids-go v0.4.1
)

require (
Expand Down
6 changes: 4 additions & 2 deletions bot/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/guregu/dynamo v1.22.0 h1:lRRjRyY+Xcvd0odbIJgzEg6rRsCvgALN56zRGl8q/yY=
github.com/guregu/dynamo v1.22.0/go.mod h1:WCJu1jWjU/mEYnV9dDOqmAGdTgO7J9AnhkxcH/btXho=
github.com/guregu/dynamo v1.22.1 h1:0exG5Er2fVs+9qYUaoUbpCPr+7pepwoVPYkDTyvVTaI=
github.com/guregu/dynamo v1.22.1/go.mod h1:a0knvVZrDhT+q7eQlu1n041lf5vPi0sNfGjRh81mAnQ=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw=
github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
Expand Down
23 changes: 21 additions & 2 deletions infra/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ resource "aws_lambda_function" "anonymous_bot" {

environment {
variables = {
BOT_TOKEN = var.bot_token
BOT_TOKEN = var.bot_token
SQIDS_ALPHABET = var.sqids_alphabet
}
}
}
Expand Down Expand Up @@ -112,6 +113,16 @@ resource "aws_dynamodb_table" "main" {
type = "S"
}

attribute {
name = "LinkKey"
type = "N"
}

attribute {
name = "CreatedAt"
type = "N"
}

global_secondary_index {
name = "UserID-GSI"
hash_key = "UserID"
Expand All @@ -124,6 +135,13 @@ resource "aws_dynamodb_table" "main" {
projection_type = "ALL"
}

global_secondary_index {
name = "LinkKey-GSI"
hash_key = "LinkKey"
range_key = "CreatedAt"
projection_type = "ALL"
}

lifecycle {
prevent_destroy = false
}
Expand Down Expand Up @@ -157,7 +175,8 @@ resource "aws_iam_policy" "lambda_dynamodb_policy" {
Effect = "Allow",
Resource = [
"${aws_dynamodb_table.main.arn}/index/UserID-GSI",
"${aws_dynamodb_table.main.arn}/index/Username-GSI"
"${aws_dynamodb_table.main.arn}/index/Username-GSI",
"${aws_dynamodb_table.main.arn}/index/LinkKey-GSI"
]
}
]
Expand Down
5 changes: 5 additions & 0 deletions infra/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ variable "bot_token" {
type = string
}

variable "sqids_alphabet" {
description = "sqids alphabet"
type = string
}

variable "zip_bundle_path" {
description = "Local Lambda zip bundle path"
type = string
Expand Down

0 comments on commit a84108f

Please sign in to comment.