enable telegram notify trough writing bot a message

Previously it was done through writing bot first,
clicking a button, copying the token, and pasting
it into the web interface.

The new flow is way simpler: click the link
to write bot a message, then click the "Check"
button in the web UI and you got notifications
enabled.
This commit is contained in:
Dmitry Verkhoturov
2021-11-07 11:51:28 -06:00
committed by Umputun
parent e1a2374a2d
commit c027dcd765
10 changed files with 843 additions and 180 deletions
+21 -13
View File
@@ -31,6 +31,7 @@ import (
"github.com/umputun/remark42/backend/app/migrator"
"github.com/umputun/remark42/backend/app/notify"
"github.com/umputun/remark42/backend/app/providers"
"github.com/umputun/remark42/backend/app/rest/api"
"github.com/umputun/remark42/backend/app/rest/proxy"
"github.com/umputun/remark42/backend/app/store"
@@ -477,8 +478,9 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
log.Printf("[WARN] failed to prepare notify destinations, %s", err)
}
// telegramService is used for getting user replies for both notifications and authorization
var telegramService *notify.Telegram
if contains("telegram", s.Notify.Users) || contains("telegram", s.Notify.Admins) {
if contains("telegram", s.Notify.Users) || contains("telegram", s.Notify.Admins) || s.Auth.Telegram {
telegramService, err = s.makeTelegramNotify()
if err != nil {
log.Printf("[WARN] failed to make telegram notify service, %s", err)
@@ -487,8 +489,12 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
var telegramBotUsername string
if telegramService != nil {
if contains("telegram", s.Notify.Users) {
telegramBotUsername = telegramService.GetBotUsername()
}
notifyDestinations = append(notifyDestinations, telegramService)
telegramBotUsername = telegramService.GetBotUsername()
// start generic update for both notify and auth services
go providers.DispatchTelegramUpdates(ctx, telegramService, []providers.TGUpdatesReceiver{telegramService}, time.Second*5)
}
notifyService := s.makeNotifyService(dataService, notifyDestinations)
@@ -525,6 +531,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
Authenticator: authenticator,
Cache: loadingCache,
NotifyService: notifyService,
TelegramService: telegramService,
SSLConfig: sslConfig,
UpdateLimiter: s.UpdateLimit,
ImageService: imageService,
@@ -780,34 +787,34 @@ func (s *ServerCommand) makeCache() (LoadingCache, error) {
func (s *ServerCommand) addAuthProviders(ctx context.Context, authenticator *auth.Service) error {
providers := 0
providersCount := 0
if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" {
authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC)
providers++
providersCount++
}
if s.Auth.Github.CID != "" && s.Auth.Github.CSEC != "" {
authenticator.AddProvider("github", s.Auth.Github.CID, s.Auth.Github.CSEC)
providers++
providersCount++
}
if s.Auth.Facebook.CID != "" && s.Auth.Facebook.CSEC != "" {
authenticator.AddProvider("facebook", s.Auth.Facebook.CID, s.Auth.Facebook.CSEC)
providers++
providersCount++
}
if s.Auth.Microsoft.CID != "" && s.Auth.Microsoft.CSEC != "" {
authenticator.AddProvider("microsoft", s.Auth.Microsoft.CID, s.Auth.Microsoft.CSEC)
providers++
providersCount++
}
if s.Auth.Yandex.CID != "" && s.Auth.Yandex.CSEC != "" {
authenticator.AddProvider("yandex", s.Auth.Yandex.CID, s.Auth.Yandex.CSEC)
providers++
providersCount++
}
if s.Auth.Twitter.CID != "" && s.Auth.Twitter.CSEC != "" {
authenticator.AddProvider("twitter", s.Auth.Twitter.CID, s.Auth.Twitter.CSEC)
providers++
providersCount++
}
if s.Auth.Patreon.CID != "" && s.Auth.Patreon.CSEC != "" {
authenticator.AddProvider("patreon", s.Auth.Patreon.CID, s.Auth.Patreon.CSEC)
providers++
providersCount++
}
if s.Auth.Telegram {
telegram := &provider.TelegramHandler{
@@ -828,13 +835,13 @@ func (s *ServerCommand) addAuthProviders(ctx context.Context, authenticator *aut
}()
authenticator.AddCustomHandler(telegram)
providers++
providersCount++
}
if s.Auth.Dev {
log.Print("[INFO] dev access enabled")
authenticator.AddProvider("dev", "", "")
providers++
providersCount++
}
if s.Auth.Email.Enable {
@@ -886,7 +893,7 @@ func (s *ServerCommand) addAuthProviders(ctx context.Context, authenticator *aut
}))
}
if providers == 0 {
if providersCount == 0 {
log.Printf("[WARN] no auth providers defined")
}
@@ -1012,6 +1019,7 @@ func (s *ServerCommand) makeTelegramNotify() (*notify.Telegram, error) {
UserNotifications: contains("telegram", s.Notify.Users),
Token: s.Telegram.Token,
Timeout: s.Telegram.Timeout,
SuccessMsg: "✅ You have successfully subscribed for notifications, check the web!",
}
tg, err := notify.NewTelegram(telegramParams)
if err != nil {
+4 -5
View File
@@ -51,11 +51,10 @@ type Request struct {
// VerificationRequest notification for user
type VerificationRequest struct {
SiteID string
User string
Email string // if set, send email only
Telegram string // if set, send telegram only
Token string
SiteID string
User string
Email string // if set, send email only
Token string
}
const defaultQueueSize = 100
+226 -39
View File
@@ -7,8 +7,11 @@ import (
"fmt"
"io"
"net/http"
neturl "net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/go-multierror"
@@ -21,10 +24,11 @@ import (
// TelegramParams contain settings for telegram notifications
type TelegramParams struct {
AdminChannelID string // unique identifier for the target chat or username of the target channel (in the format @channelusername)
Token string // token for telegram bot API interactions
Timeout time.Duration // http client timeout
UserNotifications bool // flag which enables user notifications
AdminChannelID string // unique identifier for the target chat or username of the target channel (in the format @channelusername)
Token string // token for telegram bot API interactions
Timeout time.Duration // http client timeout
UserNotifications bool // flag which enables user notifications
ErrorMsg, SuccessMsg string // messages for successful and unsuccessful subscription requests to bot
apiPrefix string // changed only in tests
}
@@ -33,7 +37,18 @@ type TelegramParams struct {
type Telegram struct {
TelegramParams
username string // bot username
// Identifier of the first update to be requested.
// Should be equal to LastSeenUpdateID + 1
// See https://core.telegram.org/bots/api#getupdates
updateOffset int
apiPollInterval time.Duration // interval to check updates from Telegram API and answer to users
expiredCleanupInterval time.Duration // interval to check and clean up expired notification requests
username string // bot username
run int32 // non-zero if Run goroutine has started
requests struct {
sync.RWMutex
data map[string]tgAuthRequest
}
}
// telegramMsg is used to send message trough Telegram bot API
@@ -42,6 +57,14 @@ type telegramMsg struct {
ParseMode string `json:"parse_mode,omitempty"`
}
type tgAuthRequest struct {
confirmed bool // whether login request has been confirmed and user info set
expires time.Time
telegramID string
user string
site string
}
// TelegramBotInfo structure contains information about telegram bot, which is used from whole telegram API response
type TelegramBotInfo struct {
Username string `json:"username"`
@@ -49,6 +72,8 @@ type TelegramBotInfo struct {
const telegramTimeOut = 5000 * time.Millisecond
const telegramAPIPrefix = "https://api.telegram.org/bot"
const tgPollInterval = time.Second * 5
const tgCleanupInterval = time.Minute * 5
// NewTelegram makes telegram bot for notifications
func NewTelegram(params TelegramParams) (*Telegram, error) {
@@ -60,6 +85,8 @@ func NewTelegram(params TelegramParams) (*Telegram, error) {
if res.Timeout == 0 {
res.Timeout = telegramTimeOut
}
res.apiPollInterval = tgPollInterval
res.expiredCleanupInterval = tgCleanupInterval
log.Printf("[DEBUG] create new telegram notifier for api=%s, timeout=%s", res.apiPrefix, res.Timeout)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -71,6 +98,8 @@ func NewTelegram(params TelegramParams) (*Telegram, error) {
}
res.username = botInfo.Username
res.requests.data = make(map[string]tgAuthRequest)
return &res, nil
}
@@ -108,7 +137,7 @@ func (t *Telegram) sendMessage(ctx context.Context, b []byte, chatID string) err
}
url := fmt.Sprintf("sendMessage?chat_id=%s&disable_web_page_preview=true", chatID)
return t.request(ctx, url, b, &struct{}{})
return t.Request(ctx, url, b, &struct{}{})
}
// buildMessage generates message for generic notification about new comment
@@ -159,39 +188,24 @@ func escapeTelegramText(text string) string {
return text
}
// SendVerification sends user verification message to the specified user
func (t *Telegram) SendVerification(ctx context.Context, req VerificationRequest) error {
if req.Telegram == "" {
// this means we can't send this request via Telegram
return nil
}
select {
case <-ctx.Done():
return errors.Errorf("sending message to %q aborted due to canceled context", req.User)
default:
}
log.Printf("[DEBUG] send verification via %s, user %s", t, req.User)
msg, err := t.buildVerificationMessage(req.User, req.Token, req.SiteID)
if err != nil {
return err
}
return t.sendMessage(ctx, msg, req.Telegram)
// SendVerification is not needed for telegram
func (t *Telegram) SendVerification(_ context.Context, _ VerificationRequest) error {
return nil
}
// buildVerificationMessage generates verification telegram message based on given input
func (t *Telegram) buildVerificationMessage(user, token, site string) ([]byte, error) {
result := fmt.Sprintf("Confirmation for <i>%s</i> on site %s\n"+
"Please copy and paste this text into “token” field on comments page to confirm subscription:\n\n\n"+
"<pre>%s</pre>",
escapeTelegramText(user), escapeTelegramText(site), escapeTelegramText(token))
body := telegramMsg{Text: result, ParseMode: "HTML"}
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
return b, nil
// TelegramUpdate contains update information, which is used from whole telegram API response
type TelegramUpdate struct {
Result []struct {
UpdateID int `json:"update_id"`
Message struct {
Chat struct {
ID int `json:"id"`
Name string `json:"first_name"`
Type string `json:"type"`
} `json:"chat"`
Text string `json:"text"`
} `json:"message"`
} `json:"result"`
}
// GetBotUsername returns bot username
@@ -199,6 +213,110 @@ func (t *Telegram) GetBotUsername() string {
return t.username
}
// AddToken adds token
func (t *Telegram) AddToken(token, user, site string, expires time.Time) {
t.requests.Lock()
t.requests.data[token] = tgAuthRequest{
expires: expires,
user: user,
site: site,
}
t.requests.Unlock()
}
// CheckToken verifies incoming token, returns the user address if it's confirmed and empty string otherwise
func (t *Telegram) CheckToken(token, user string) (telegram, site string, err error) {
t.requests.RLock()
authRequest, ok := t.requests.data[token]
t.requests.RUnlock()
if !ok {
return "", "", errors.New("request is not found")
}
if time.Now().After(authRequest.expires) {
t.requests.Lock()
delete(t.requests.data, token)
t.requests.Unlock()
return "", "", errors.New("request expired")
}
if !authRequest.confirmed {
return "", "", errors.New("request is not verified yet")
}
if authRequest.user != user {
return "", "", errors.New("user does not match original requester")
}
// Delete request
t.requests.Lock()
delete(t.requests.data, token)
t.requests.Unlock()
return authRequest.telegramID, authRequest.site, nil
}
// Run starts processing login requests sent in Telegram, required for user notifications to work
// Blocks caller
func (t *Telegram) Run(ctx context.Context) {
atomic.AddInt32(&t.run, 1)
processUpdatedTicker := time.NewTicker(t.apiPollInterval)
cleanupTicker := time.NewTicker(t.expiredCleanupInterval)
for {
select {
case <-ctx.Done():
processUpdatedTicker.Stop()
cleanupTicker.Stop()
atomic.AddInt32(&t.run, -1)
return
case <-processUpdatedTicker.C:
updates, err := t.getUpdates(ctx)
if err != nil {
log.Printf("[WARN] Error while getting telegram updates: %v", err)
continue
}
t.processUpdates(ctx, updates)
case <-cleanupTicker.C:
now := time.Now()
t.requests.Lock()
for key, req := range t.requests.data {
if now.After(req.expires) {
delete(t.requests.data, key)
}
}
t.requests.Unlock()
}
}
}
// ProcessUpdate is alternative to Run, it processes provided plain text update from Telegram
// so that caller could get updates and send it not only there but to multiple sources
func (t *Telegram) ProcessUpdate(ctx context.Context, textUpdate string) error {
if atomic.LoadInt32(&t.run) != 0 {
return errors.New("Run goroutine should not be used with ProcessUpdate")
}
defer func() {
// as Run goroutine is not running, clean up old requests on each update
// even if we hit json decode error
now := time.Now()
t.requests.Lock()
for key, req := range t.requests.data {
if now.After(req.expires) {
delete(t.requests.data, key)
}
}
t.requests.Unlock()
}()
var updates TelegramUpdate
if err := json.Unmarshal([]byte(textUpdate), &updates); err != nil {
return errors.Wrap(err, "failed to decode provided telegram update")
}
t.processUpdates(ctx, &updates)
return nil
}
func (t *Telegram) String() string {
result := "telegram"
if t.AdminChannelID != "" {
@@ -210,13 +328,81 @@ func (t *Telegram) String() string {
return result
}
// getUpdates fetches incoming updates
func (t *Telegram) getUpdates(ctx context.Context) (*TelegramUpdate, error) {
url := `getUpdates?allowed_updates=["message"]`
if t.updateOffset != 0 {
url += fmt.Sprintf("&offset=%d", t.updateOffset)
}
var result TelegramUpdate
err := t.Request(ctx, url, nil, &result)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch updates")
}
for _, u := range result.Result {
if u.UpdateID >= t.updateOffset {
t.updateOffset = u.UpdateID + 1
}
}
return &result, nil
}
// processUpdates processes a batch of updates from telegram servers
func (t *Telegram) processUpdates(ctx context.Context, updates *TelegramUpdate) {
for _, update := range updates.Result {
if update.Message.Chat.Type != "private" {
continue
}
if !strings.HasPrefix(update.Message.Text, "/start ") {
continue
}
token := strings.TrimPrefix(update.Message.Text, "/start ")
t.requests.RLock()
authRequest, ok := t.requests.data[token]
if !ok { // No such token
t.requests.RUnlock()
if t.ErrorMsg != "" {
if err := t.sendText(ctx, update.Message.Chat.ID, t.ErrorMsg); err != nil {
log.Printf("[WARN] failed to notify telegram peer: %v", err)
}
}
continue
}
t.requests.RUnlock()
authRequest.confirmed = true
authRequest.telegramID = strconv.Itoa(update.Message.Chat.ID)
t.requests.Lock()
t.requests.data[token] = authRequest
t.requests.Unlock()
if err := t.sendText(ctx, update.Message.Chat.ID, t.SuccessMsg); err != nil {
log.Printf("[ERROR] failed to notify telegram peer: %v", err)
}
}
}
// sendText sends a plain text message to telegram peer
func (t *Telegram) sendText(ctx context.Context, recipientID int, msg string) error {
url := fmt.Sprintf("sendMessage?chat_id=%d&text=%s", recipientID, neturl.PathEscape(msg))
return t.Request(ctx, url, nil, &struct{}{})
}
// botInfo returns info about configured bot
func (t *Telegram) botInfo(ctx context.Context) (*TelegramBotInfo, error) {
var resp = struct {
Result *TelegramBotInfo `json:"result"`
}{}
err := t.request(ctx, "getMe", nil, &resp)
err := t.Request(ctx, "getMe", nil, &resp)
if err != nil {
return nil, err
}
@@ -227,7 +413,8 @@ func (t *Telegram) botInfo(ctx context.Context) (*TelegramBotInfo, error) {
return resp.Result, nil
}
func (t *Telegram) request(ctx context.Context, method string, b []byte, data interface{}) error {
// Request makes a request to the Telegram API and return the result
func (t *Telegram) Request(ctx context.Context, method string, b []byte, data interface{}) error {
return repeater.NewDefault(3, time.Millisecond*250).Do(ctx, func() error {
url := fmt.Sprintf("%s%s/%s", t.apiPrefix, t.Token, method)
+284 -20
View File
@@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -179,28 +180,282 @@ func TestTelegram_SendVerification(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, tb)
// proper VerificationRequest without telegram
req := VerificationRequest{
SiteID: "remark",
User: "test_username",
Token: "secret_",
}
assert.NoError(t, tb.SendVerification(context.TODO(), req))
// empty VerificationRequest should return no error and no nothing, as well as any other
assert.NoError(t, tb.SendVerification(context.TODO(), VerificationRequest{}))
}
// proper VerificationRequest with telegram
req.Telegram = "test"
assert.NoError(t, tb.SendVerification(context.TODO(), req))
const getUpdatesResp = `{
"ok": true,
"result": [
{
"update_id": 998,
"message": {
"chat": {
"type": "group"
}
}
},
{
"update_id": 999,
"message": {
"text": "not starting with /start",
"chat": {
"type": "private"
}
}
},
{
"update_id": 1000,
"message": {
"message_id": 4,
"from": {
"id": 313131313,
"is_bot": false,
"first_name": "Joe",
"username": "joe123",
"language_code": "en"
},
"chat": {
"id": 313131313,
"first_name": "Joe",
"username": "joe123",
"type": "private"
},
"date": 1601665548,
"text": "/start token",
"entities": [
{
"offset": 0,
"length": 6,
"type": "bot_command"
}
]
}
}
]
}`
// VerificationRequest with canceled context
ctx, cancel := context.WithCancel(context.TODO())
cancel()
assert.EqualError(t, tb.SendVerification(ctx, req), "sending message to \"test_username\" aborted due to canceled context")
// test buildVerificationMessage separately for message text
res, err := tb.buildVerificationMessage(req.User, req.Token, req.SiteID)
func TestTelegram_GetUpdatesFlow(t *testing.T) {
first := true
ts := mockTelegramServer(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.String(), "sendMessage") {
// respond normally to processUpdates attempt to send message back to user
_, _ = w.Write([]byte("{}"))
return
}
// responses to get updates calls to API
if first {
assert.Equal(t, "", r.URL.Query().Get("offset"))
first = false
} else {
assert.Equal(t, "1001", r.URL.Query().Get("offset"))
}
_, _ = w.Write([]byte(getUpdatesResp))
})
defer ts.Close()
tb, err := NewTelegram(TelegramParams{
AdminChannelID: "remark_test",
Token: "xxxsupersecretxxx",
UserNotifications: true,
apiPrefix: ts.URL + "/",
})
assert.NoError(t, err)
assert.Contains(t, string(res), `Confirmation for \u003ci\u003etest_username\u003c/i\u003e on site remark`)
assert.Contains(t, string(res), `secret_`)
// send request with no offset
upd, err := tb.getUpdates(context.Background())
assert.NoError(t, err)
assert.Len(t, upd.Result, 3)
assert.Equal(t, 1001, tb.updateOffset)
assert.Equal(t, "/start token", upd.Result[len(upd.Result)-1].Message.Text)
tb.AddToken("token", "user", "site", time.Now().Add(time.Minute))
_, _, err = tb.CheckToken("token", "user")
assert.Error(t, err)
tb.processUpdates(context.Background(), upd)
tgID, site, err := tb.CheckToken("token", "user")
assert.NoError(t, err)
assert.Equal(t, "313131313", tgID)
assert.Equal(t, "site", site)
// send request with offset
_, err = tb.getUpdates(context.Background())
assert.NoError(t, err)
}
func TestTelegram_ProcessUpdateFlow(t *testing.T) {
ts := mockTelegramServer(func(w http.ResponseWriter, r *http.Request) {
// respond normally to processUpdates attempt to send message back to user
_, _ = w.Write([]byte("{}"))
})
defer ts.Close()
tb, err := NewTelegram(TelegramParams{
AdminChannelID: "remark_test",
Token: "xxxsupersecretxxx",
UserNotifications: true,
apiPrefix: ts.URL + "/",
})
assert.NoError(t, err)
tb.AddToken("token", "user", "site", time.Now().Add(time.Minute))
tb.AddToken("expired token", "user", "site", time.Now().Add(-time.Minute))
assert.Len(t, tb.requests.data, 2)
_, _, err = tb.CheckToken("token", "user")
assert.Error(t, err)
assert.NoError(t, tb.ProcessUpdate(context.Background(), getUpdatesResp))
assert.Len(t, tb.requests.data, 1, "expired token was cleaned up")
tgID, site, err := tb.CheckToken("token", "user")
assert.NoError(t, err)
assert.Len(t, tb.requests.data, 0, "token is deleted after successful check")
assert.Equal(t, "313131313", tgID)
assert.Equal(t, "site", site)
tb.AddToken("expired token", "user", "site", time.Now().Add(-time.Minute))
assert.Len(t, tb.requests.data, 1)
assert.EqualError(t, tb.ProcessUpdate(context.Background(), ""), "failed to decode provided telegram update: unexpected end of JSON input")
assert.Len(t, tb.requests.data, 0, "expired token should be cleaned up despite the error")
}
const sendMessageResp = `{
"ok": true,
"result": {
"message_id": 100,
"from": {
"id": 666666666,
"is_bot": true,
"first_name": "Test auth bot",
"username": "TestAuthBot"
},
"chat": {
"id": 313131313,
"first_name": "Joe",
"username": "joe123",
"type": "private"
},
"date": 1602430546,
"text": "123"
}
}`
func TestTelegram_SendText(t *testing.T) {
ts := mockTelegramServer(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "123", r.URL.Query().Get("chat_id"))
assert.Equal(t, "hello there", r.URL.Query().Get("text"))
_, _ = w.Write([]byte(sendMessageResp))
})
defer ts.Close()
tb, err := NewTelegram(TelegramParams{
AdminChannelID: "remark_test",
Token: "xxxsupersecretxxx",
UserNotifications: true,
apiPrefix: ts.URL + "/",
})
assert.NoError(t, err)
err = tb.sendText(context.Background(), 123, "hello there")
assert.NoError(t, err)
}
const errorResp = `{"ok":false,"error_code":400,"description":"Very bad request"}`
func TestTelegram_Error(t *testing.T) {
ts := mockTelegramServer(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(errorResp))
})
defer ts.Close()
tb, err := NewTelegram(TelegramParams{
AdminChannelID: "remark_test",
Token: "xxxsupersecretxxx",
UserNotifications: true,
apiPrefix: ts.URL + "/",
})
assert.NoError(t, err)
_, err = tb.getUpdates(context.Background())
assert.EqualError(t, err, "failed to fetch updates: unexpected telegram API status code 400, error: \"Very bad request\"")
}
func TestTelegram_TokenVerification(t *testing.T) {
ts := mockTelegramServer(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.String(), "sendMessage") {
// respond normally to processUpdates attempt to send message back to user
_, _ = w.Write([]byte("{}"))
return
}
// responses to get updates calls to API
_, _ = w.Write([]byte(getUpdatesResp))
})
defer ts.Close()
tb, err := NewTelegram(TelegramParams{
AdminChannelID: "remark_test",
Token: "good-token",
apiPrefix: ts.URL + "/",
})
assert.NoError(t, err)
assert.NotNil(t, tb)
tb.AddToken("token", "user", "site", time.Now().Add(time.Minute))
assert.Len(t, tb.requests.data, 1)
// wrong token
tgID, site, err := tb.CheckToken("unknown token", "user")
assert.Empty(t, tgID)
assert.Empty(t, site)
assert.EqualError(t, err, "request is not found")
// right token and user, not verified yet
tgID, site, err = tb.CheckToken("token", "user")
assert.Empty(t, tgID)
assert.Empty(t, site)
assert.EqualError(t, err, "request is not verified yet")
// confirm request
authRequest, ok := tb.requests.data["token"]
assert.True(t, ok)
authRequest.confirmed = true
authRequest.telegramID = "telegramID"
tb.requests.data["token"] = authRequest
// wrong user
tgID, site, err = tb.CheckToken("token", "wrong user")
assert.Empty(t, tgID)
assert.Empty(t, site)
assert.EqualError(t, err, "user does not match original requester")
// successful check
tgID, site, err = tb.CheckToken("token", "user")
assert.NoError(t, err)
assert.Equal(t, "telegramID", tgID)
assert.Equal(t, "site", site)
// expired token
tb.AddToken("expired token", "user", "site", time.Now().Add(-time.Minute))
tgID, site, err = tb.CheckToken("expired token", "user")
assert.Empty(t, tgID)
assert.Empty(t, site)
assert.EqualError(t, err, "request expired")
assert.Len(t, tb.requests.data, 0)
// expired token, cleaned up by the cleanup
tb.apiPollInterval = time.Millisecond * 15
tb.expiredCleanupInterval = time.Millisecond * 10
ctx, cancel := context.WithCancel(context.Background())
go tb.Run(ctx)
assert.Eventually(t, func() bool {
return tb.ProcessUpdate(ctx, "").Error() == "Run goroutine should not be used with ProcessUpdate"
}, time.Millisecond*100, time.Millisecond*10, "ProcessUpdate should not work same time as Run")
tb.AddToken("expired token", "user", "site", time.Now().Add(-time.Minute))
tb.requests.RLock()
assert.Len(t, tb.requests.data, 1)
tb.requests.RUnlock()
time.Sleep(tb.expiredCleanupInterval * 2)
tb.requests.RLock()
assert.Len(t, tb.requests.data, 0)
tb.requests.RUnlock()
cancel()
// give enough time for Run() to finish
time.Sleep(tb.expiredCleanupInterval)
}
const getMeResp = `{"ok": true,
@@ -211,7 +466,16 @@ const getMeResp = `{"ok": true,
"username": "remark42_test_bot"
}}`
func mockTelegramServer(_ http.HandlerFunc) *httptest.Server {
func mockTelegramServer(h http.HandlerFunc) *httptest.Server {
if h != nil {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.String(), "getMe") {
_, _ = w.Write([]byte(getMeResp))
return
}
h(w, r)
}))
}
router := chi.NewRouter()
router.Get("/good-token/getMe", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(getMeResp))
+72
View File
@@ -0,0 +1,72 @@
package providers
// Both Telegram auth and notifications need to receive messages received by Telegram bot in the loop,
// and below is the implementation of such loop which dispatched received events to both receivers,
// so that they could work at the same time.
import (
"context"
"encoding/json"
"fmt"
"time"
log "github.com/go-pkgz/lgr"
"github.com/umputun/remark42/backend/app/notify"
)
type tgRequester interface {
Request(ctx context.Context, method string, b []byte, data interface{}) error
}
// TGUpdatesReceiver used to dispatch telegram updates to multiple receivers
type TGUpdatesReceiver interface {
fmt.Stringer
ProcessUpdate(ctx context.Context, textUpdate string) error
}
// DispatchTelegramUpdates dispatches telegram updates to provided list of receivers
// Blocks caller
func DispatchTelegramUpdates(ctx context.Context, requester tgRequester, receivers []TGUpdatesReceiver, period time.Duration) {
// Identifier of the first update to be requested.
// Should be equal to LastSeenUpdateID + 1
// See https://core.telegram.org/bots/api#getupdates
var updateOffset int
processUpdatedTicker := time.NewTicker(period)
for {
select {
case <-ctx.Done():
processUpdatedTicker.Stop()
return
case <-processUpdatedTicker.C:
url := `getUpdates?allowed_updates=["message"]`
if updateOffset != 0 {
url += fmt.Sprintf("&offset=%d", updateOffset)
}
var update notify.TelegramUpdate
err := requester.Request(ctx, url, nil, &update)
if err != nil {
log.Printf("[WARN] failed to fetch updates: %v", err)
continue
}
for _, u := range update.Result {
if u.UpdateID >= updateOffset {
updateOffset = u.UpdateID + 1
}
}
if raw, err := json.Marshal(update); err == nil {
for _, r := range receivers {
e := r.ProcessUpdate(ctx, string(raw))
if e != nil {
log.Printf("[ERROR] failure from destination %s on processing telegram update %v", r, e)
}
}
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark42/backend/app/notify"
)
func TestDispatchTelegramUpdates(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
poolPeriod := time.Millisecond * 100
go DispatchTelegramUpdates(ctx, &mockTGRequester{t: t}, []TGUpdatesReceiver{&mockTGUpdatesReceiver{t: t}}, poolPeriod)
time.Sleep(poolPeriod * 3)
cancel()
time.Sleep(poolPeriod)
}
const getUpdatesResp = `{
"ok": true,
"result": [
{
"update_id": 998,
"message": {
"chat": {
"type": "group"
}
}
}
]
}`
type mockTGRequester struct {
hit int
t *testing.T
}
func (m *mockTGRequester) Request(_ context.Context, _ string, _ []byte, data interface{}) error {
if m.hit < 2 {
m.hit++
assert.NoError(m.t, json.Unmarshal([]byte(getUpdatesResp), data))
return nil
}
return errors.New("test error")
}
type mockTGUpdatesReceiver struct {
t *testing.T
hit int
}
func (m *mockTGUpdatesReceiver) String() string {
return "mock updater"
}
func (m *mockTGUpdatesReceiver) ProcessUpdate(_ context.Context, textUpdate string) error {
var result notify.TelegramUpdate
err := json.Unmarshal([]byte(textUpdate), &result)
assert.NoError(m.t, err)
if m.hit < 2 {
assert.NotNil(m.t, result.Result)
m.hit++
return nil
}
assert.Nil(m.t, result.Result)
return errors.New("test error")
}
+3 -2
View File
@@ -44,6 +44,7 @@ type Rest struct {
CommentFormatter *store.CommentFormatter
Migrator *Migrator
NotifyService *notify.Service
TelegramService telegramService
ImageService *image.Service
AnonVote bool
@@ -324,8 +325,7 @@ func (s *Rest) routes() chi.Router {
rauth.With(rejectAnonUser).Post("/email/subscribe", s.privRest.sendEmailConfirmationCtrl)
rauth.With(rejectAnonUser).Post("/email/confirm", s.privRest.setConfirmedEmailCtrl)
rauth.With(rejectAnonUser).Delete("/email", s.privRest.deleteEmailCtrl)
rauth.With(rejectAnonUser).Post("/telegram/subscribe", s.privRest.sendTelegramConfirmationCtrl)
rauth.With(rejectAnonUser).Post("/telegram/confirm", s.privRest.setConfirmedTelegramCtrl)
rauth.With(rejectAnonUser).Get("/telegram/subscribe", s.privRest.telegramSubscribeCtrl)
rauth.With(rejectAnonUser).Delete("/telegram", s.privRest.deleteTelegramCtrl)
})
@@ -374,6 +374,7 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
readOnlyAge: s.ReadOnlyAge,
authenticator: s.Authenticator,
notifyService: s.NotifyService,
telegramService: s.TelegramService,
remarkURL: s.RemarkURL,
anonVote: s.AnonVote,
templates: templates.NewFS(),
+70 -80
View File
@@ -3,8 +3,9 @@ package api
import (
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/sha1" //nolint:gosec //not used for security
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
@@ -21,6 +22,7 @@ import (
R "github.com/go-pkgz/rest"
"github.com/golang-jwt/jwt"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/umputun/remark42/backend/app/notify"
"github.com/umputun/remark42/backend/app/rest"
@@ -39,11 +41,19 @@ type private struct {
imageService *image.Service
notifyService *notify.Service
authenticator *auth.Service
telegramService telegramService
remarkURL string
anonVote bool
templates templates.FileReader
}
// telegramService is a subset of Telegram service used for setting up user telegram notifications
type telegramService interface {
AddToken(token, user, site string, expires time.Time)
CheckToken(token, userID string) (telegram, site string, err error)
GetBotUsername() string
}
type privStore interface {
Create(comment store.Comment) (commentID string, err error)
EditComment(locator store.Locator, commentID string, req service.EditRequest) (comment store.Comment, err error)
@@ -349,55 +359,64 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
render.JSON(w, r, R.JSON{"user": user, "address": address})
}
// sendTelegramConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
// GET /telegram/subscribe?site=siteID&address=@someone
//nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON
func (s *private) sendTelegramConfirmationCtrl(w http.ResponseWriter, r *http.Request) {
// telegramSubscribeCtrl generates and verifies telegram notification request
// GET /telegram/subscribe?site=siteID<&tkn=token>
func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
address := r.URL.Query().Get("address")
siteID := r.URL.Query().Get("site")
if address == "" {
rest.SendErrorJSON(w, r, http.StatusBadRequest,
errors.New("missing parameter"), "address parameter is required", rest.ErrInternal)
if s.telegramService == nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError,
errors.New("not enabled"), "telegram notifications are not enabled", rest.ErrActionRejected)
return
}
existingAddress, err := s.dataService.GetUserTelegram(siteID, user.ID)
queryToken := r.URL.Query().Get("tkn")
if queryToken == "" {
// GET /telegram/subscribe?site=siteID (No token supplied)
siteID := r.URL.Query().Get("site")
if siteID == "" {
rest.SendErrorJSON(w, r, http.StatusBadRequest, errors.New("missing parameter"), "site parameter is required", rest.ErrInternal)
return
}
// we don't care as much if we can't retrieve the current value of that field for the user, so ignore the error
if existingAddress, _ := s.dataService.GetUserTelegram(siteID, user.ID); existingAddress != "" {
rest.SendErrorJSON(w, r, http.StatusConflict,
errors.New("already subscribed"), "telegram subscription is already set for this user, delete if first to re-subscribe", rest.ErrActionRejected)
return
}
// Generate and send token
tkn, err := randToken()
if err != nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to generate verification token", rest.ErrInternal)
return
}
expires := time.Now().Add(10 * time.Minute)
s.telegramService.AddToken(tkn, user.ID, siteID, expires)
render.JSON(w, r, R.JSON{"token": tkn, "bot": s.telegramService.GetBotUsername()})
return
}
// GET /telegram/subscribe?tkn=token (verify token)
var address, siteID string
address, siteID, err := s.telegramService.CheckToken(queryToken, user.ID)
if err != nil {
// we don't care as much if we can't retrieve the current value of that field for the user,
// as it's only used to check if we're trying to set to the same value it's already set to
log.Printf("[WARN] can't read telegram for %s, %v", user.ID, err)
}
if address == existingAddress {
rest.SendErrorJSON(w, r, http.StatusConflict,
errors.New("already verified"), "telegram address is already verified for this user", rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't set telegram for user", rest.ErrInternal)
return
}
claims := token.Claims{
Handshake: &token.Handshake{ID: user.ID + "::" + address},
StandardClaims: jwt.StandardClaims{
Audience: r.URL.Query().Get("site"),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
Issuer: "remark42",
},
}
tkn, err := s.authenticator.TokenService().Token(claims)
log.Printf("[DEBUG] set telegram notifications for user %s", user.ID)
val, err := s.dataService.SetUserTelegram(siteID, user.ID, address)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to make verification token", rest.ErrInternal)
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set telegram for user", code)
return
}
s.notifyService.SubmitVerification(
notify.VerificationRequest{
SiteID: siteID,
User: user.Name,
Telegram: address,
Token: tkn,
},
)
render.JSON(w, r, R.JSON{"user": user, "address": address})
render.JSON(w, r, R.JSON{"updated": true, "address": val})
}
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
@@ -453,47 +472,6 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
render.JSON(w, r, R.JSON{"updated": true, "address": val})
}
// setConfirmedTelegramCtrl uses provided token parameter (generated by sendTelegramConfirmationCtrl) to set telegram and add it to user token
// PUT /telegram/confirm?site=siteID&tkn=jwt
func (s *private) setConfirmedTelegramCtrl(w http.ResponseWriter, r *http.Request) {
tkn := r.URL.Query().Get("tkn")
if tkn == "" {
rest.SendErrorJSON(w, r, http.StatusBadRequest, errors.New("missing parameter"), "token parameter is required", rest.ErrInternal)
return
}
user := rest.MustGetUserInfo(r)
siteID := r.URL.Query().Get("site")
confClaims, err := s.authenticator.TokenService().Parse(tkn)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
return
}
if s.authenticator.TokenService().IsExpired(confClaims) {
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("expired"), "failed to verify confirmation token", rest.ErrInternal)
return
}
// Handshake.ID is user.ID + "::" + address
elems := strings.Split(confClaims.Handshake.ID, "::")
if len(elems) != 2 || elems[0] != user.ID {
rest.SendErrorJSON(w, r, http.StatusBadRequest, errors.New(confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal)
return
}
address := elems[1]
log.Printf("[DEBUG] set telegram for user %s", user.ID)
val, err := s.dataService.SetUserTelegram(siteID, user.ID, address)
if err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set telegram for user", code)
return
}
render.JSON(w, r, R.JSON{"updated": true, "address": val})
}
// POST/GET /email/unsubscribe.html?site=siteID&tkn=jwt - unsubscribe the user in token from email notifications
func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
tkn := r.URL.Query().Get("tkn")
@@ -748,3 +726,15 @@ func (s *private) isReadOnly(locator store.Locator) bool {
}
return s.dataService.IsReadOnly(locator) // ro manually
}
func randToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", errors.Wrap(err, "can't get random")
}
s := sha1.New() //nolint:gosec // not used for security
if _, err := s.Write(b); err != nil {
return "", errors.Wrap(err, "can't write randoms to sha1")
}
return fmt.Sprintf("%x", s.Sum(nil)), nil
}
+86 -17
View File
@@ -6,6 +6,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -578,6 +579,7 @@ func TestRest_EmailAndTelegram(t *testing.T) {
defer teardown()
srv.privRest.templates = &MockFS{}
srv.privRest.telegramService = &mockTelegram{site: "remark42"}
// issue good token
claims := token.Claims{
@@ -620,14 +622,13 @@ func TestRest_EmailAndTelegram(t *testing.T) {
{description: "issue delete request without auth", url: "/api/v1/telegram", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true},
{description: "issue delete request without site_id", url: "/api/v1/telegram", method: http.MethodDelete, responseCode: http.StatusBadRequest},
{description: "delete non-existent user telegram", url: "/api/v1/telegram?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
{description: "set user telegram, token not set", url: "/api/v1/telegram/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
{description: "send confirmation without address", url: "/api/v1/telegram/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
{description: "send confirmation", url: "/api/v1/telegram/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
{description: "set user telegram, token is good", url: fmt.Sprintf("/api/v1/telegram/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
{description: "send confirmation with same address", url: "/api/v1/telegram/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict},
{description: "send telegram confirmation, no siteID", url: "/api/v1/telegram/subscribe", method: http.MethodGet, responseCode: http.StatusBadRequest},
{description: "send telegram confirmation", url: "/api/v1/telegram/subscribe?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
{description: "set user telegram, token is good", url: "/api/v1/telegram/subscribe?site=remark42&tkn=good_token", method: http.MethodGet, responseCode: http.StatusOK},
{description: "send confirmation with same address", url: "/api/v1/telegram/subscribe?site=remark42", method: http.MethodGet, responseCode: http.StatusConflict},
{description: "delete user telegram", url: "/api/v1/telegram?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
{description: "send another confirmation", url: "/api/v1/telegram/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
{description: "set user telegram, token is good", url: fmt.Sprintf("/api/v1/telegram/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
{description: "send another confirmation", url: "/api/v1/telegram/subscribe?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
{description: "set user telegram, token is good", url: "/api/v1/telegram/subscribe?site=remark42&tkn=good_token", method: http.MethodGet, responseCode: http.StatusOK},
}
client := http.Client{}
for _, x := range testData {
@@ -852,8 +853,8 @@ func TestRest_TelegramNotification(t *testing.T) {
require.Equal(t, 2, len(mockDestination.Get()))
assert.Empty(t, mockDestination.Get()[1].Telegrams)
// send confirmation token for telegram
req, err = http.NewRequest(http.MethodPost, ts.URL+"/api/v1/telegram/subscribe?site=remark42&address=good_telegram", nil)
// subscribe to telegram while the telegram destination is absent
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/telegram/subscribe?site=remark42", nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
@@ -861,15 +862,13 @@ func TestRest_TelegramNotification(t *testing.T) {
body, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
// wait for mock notification Submit to kick off
time.Sleep(time.Millisecond * 30)
require.Equal(t, 1, len(mockDestination.GetVerify()))
assert.Equal(t, "good_telegram", mockDestination.GetVerify()[0].Telegram)
verificationToken := mockDestination.GetVerify()[0].Token
require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body))
assert.Equal(t, `{"code":17,"details":"telegram notifications are not enabled","error":"not enabled"}`+"\n", string(body))
// verify telegram
req, err = http.NewRequest(http.MethodPost, ts.URL+fmt.Sprintf("/api/v1/telegram/confirm?site=remark42&tkn=%s", verificationToken), nil)
mockTlgrm := &mockTelegram{notVerified: true, site: "unknown_site"}
srv.privRest.telegramService = mockTlgrm
// send confirmation token for telegram
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/telegram/subscribe?site=remark42", nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
@@ -878,6 +877,58 @@ func TestRest_TelegramNotification(t *testing.T) {
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var subscribeRequest struct {
Bot string `json:"bot"`
Token string `json:"token"`
}
err = json.Unmarshal(body, &subscribeRequest)
assert.NoError(t, err)
assert.Equal(t, "botUsername", subscribeRequest.Bot)
// verify telegram, unsuccessfully because of not verified
req, err = http.NewRequest(http.MethodGet, ts.URL+fmt.Sprintf("/api/v1/telegram/subscribe?site=remark42&tkn=%s", subscribeRequest.Token), nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body))
require.Equal(t, `{"code":0,"details":"can't set telegram for user","error":"not verified"}`+"\n", string(body))
mockTlgrm.notVerified = false
// verify telegram, unsuccessfully because of unknown site
req, err = http.NewRequest(http.MethodGet, ts.URL+fmt.Sprintf("/api/v1/telegram/subscribe?site=remark42&tkn=%s", subscribeRequest.Token), nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusBadRequest, resp.StatusCode, string(body))
require.Equal(t, `{"code":0,"details":"can't set telegram for user","error":"site \"unknown_site\" not found"}`+"\n", string(body))
mockTlgrm.site = "remark42"
// verify telegram, successfully
req, err = http.NewRequest(http.MethodGet, ts.URL+fmt.Sprintf("/api/v1/telegram/subscribe?site=remark42&tkn=%s", subscribeRequest.Token), nil)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var subscribeResult struct {
Address string `json:"address"`
Updated bool `json:"updated"`
}
err = json.Unmarshal(body, &subscribeResult)
assert.NoError(t, err)
assert.True(t, subscribeResult.Updated)
// get user information to verify the subscription
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/user?site=remark42", nil)
@@ -1224,3 +1275,21 @@ func TestRest_CreateWithPictures(t *testing.T) {
assert.NoError(t, err, "picture %d moved from staging and available in permanent location", i)
}
}
type mockTelegram struct {
notVerified bool
site string
}
func (m *mockTelegram) AddToken(string, string, string, time.Time) {}
func (m *mockTelegram) GetBotUsername() string {
return "botUsername"
}
func (m *mockTelegram) CheckToken(string, string) (telegram, site string, err error) {
if m.notVerified {
return "", "", errors.New("not verified")
}
return "good_telegram", m.site, nil
}
+5 -4
View File
@@ -134,12 +134,13 @@ X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYw
DELETE {{host}}/api/v1/email?site={{site}}
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
### send confirmation token for current user to specified telegram. auth token for dev user for secret=12345.
POST {{host}}/api/v1/telegram/subscribe?site={{site}}&address={{telegram}}
### get information for sending confirmation token for current user. auth token for dev user for secret=12345.
### After you'll get the response, construct link with it and open it: https://t.me/<bot>?start=<token>
GET {{host}}/api/v1/telegram/subscribe?site={{site}}
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
### add telegram for notifications for current user via token from telegram. auth token for dev user for secret=12345.
POST {{host}}/api/v1/telegram/confirm?site={{site}}&tkn={{token}}
### verify telegram notifications for current user via token obtained in the previous step, after talking to bot. auth token for dev user for secret=12345.
GET {{host}}/api/v1/telegram/subscribe?tkn={{token}}
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
### delete current user telegram. auth token for dev user for secret=12345.