Repository: mr-linch/go-tg
Pull request #145 (2024-03-12 20:11:43)

tgb: Text Message Call Builder

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk) Report
Attention: Patch coverage is `93.33333%` with `6 lines` in your changes are missing coverage. Please review.
> Project coverage is 94.66%. Comparing base [(`8764513`)](https://app.codecov.io/gh/mr-linch/go-tg/commit/8764513db16bf3c517e27039298515ac3c2c4c06?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk) to head [(`2bc2dcc`)](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk).

| [Files](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk) | Patch % | Lines |
|---|---|---|
| [request.go](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk#diff-cmVxdWVzdC5nbw==) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #145      +/-   ##
==========================================
- Coverage   94.71%   94.66%   -0.06%     
==========================================
  Files          22       23       +1     
  Lines        2327     2417      +90     
==========================================
+ Hits         2204     2288      +84     
- Misses         84       90       +6     
  Partials       39       39              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mr-linch/go-tg/pull/145?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sasha+Savchuk).


Patch:
diff --git a/examples/calculator/main.go b/examples/calculator/main.go
index 03dbb7b..2b5dcea 100644
--- a/examples/calculator/main.go
+++ b/examples/calculator/main.go
@@ -19,7 +19,7 @@ func main() {
 		Message(func(ctx context.Context, msg *tgb.MessageUpdate) error {
 			return msg.Answer(tg.HTML.Italic(typingMessage)).
 				ParseMode(tg.HTML).
-				ReplyMarkup(newKeyboard()).
+				ReplyMarkup(newCalculatorMessageKeyboard()).
 				DoVoid(ctx)
 		}).
 		CallbackQuery(func(ctx context.Context, cbq *tgb.CallbackQueryUpdate) error {
@@ -55,12 +55,12 @@ func main() {
 				msg.Chat.ID,
 				msg.ID,
 				currentText,
-			).ReplyMarkup(newKeyboard()).DoVoid(ctx)
+			).ReplyMarkup(newCalculatorMessageKeyboard()).DoVoid(ctx)
 		}),
 	)
 }
 
-func newKeyboard() tg.InlineKeyboardMarkup {
+func newCalculatorMessageKeyboard() tg.InlineKeyboardMarkup {
 	layout := tg.NewButtonLayout[tg.InlineKeyboardButton](3).Row(
 		tg.NewInlineKeyboardButtonCallback("+", "+"),
 		tg.NewInlineKeyboardButtonCallback("-", "-"),
diff --git a/examples/menu/main.go b/examples/menu/main.go
new file mode 100644
index 0000000..64a8467
--- /dev/null
+++ b/examples/menu/main.go
@@ -0,0 +1,75 @@
+// Package contains simple echo bot, that demonstrates how to use handlers, filters and file uploads.
+package main
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/mr-linch/go-tg"
+	"github.com/mr-linch/go-tg/examples"
+	"github.com/mr-linch/go-tg/tgb"
+)
+
+var pm = tg.HTML
+
+func newMenuMainMessage() *tgb.TextMessageCallBuilder {
+	return tgb.NewTextMessageCallBuilder(
+		pm.Text(
+			pm.Bold("👋 Hi, I'm demo of", pm.Code("tg.TextMessageCallBuilder")),
+			"",
+			pm.Italic("Use attached keyboard or commands to navigate"),
+		),
+	).ReplyMarkup(tg.NewInlineKeyboardMarkup(
+		tg.NewButtonRow(
+			tg.NewInlineKeyboardButtonCallback("menu 1", "menu_1"),
+			tg.NewInlineKeyboardButtonCallback("menu 2", "menu_2"),
+			tg.NewInlineKeyboardButtonCallback("menu 3", "menu_3"),
+		),
+	)).ParseMode(pm)
+}
+
+func newSubmenu(n int) *tgb.TextMessageCallBuilder {
+	return tgb.NewTextMessageCallBuilder(
+		pm.Text(
+			pm.Bold("Menu ", strconv.Itoa(n)),
+			"",
+			pm.Bold("Now:", " ", pm.Code(time.Now().Format(time.RFC3339))),
+			"",
+			pm.Italic("Use attached keyboard or commands to navigate"),
+		),
+	).ReplyMarkup(
+		tg.NewInlineKeyboardMarkup(
+			tg.NewButtonRow(
+				tg.NewInlineKeyboardButtonCallback("go back", "menu_main"),
+				tg.NewInlineKeyboardButtonCallback("refresh", "menu_"+strconv.Itoa(n)),
+			),
+		),
+	).ParseMode(pm)
+}
+
+func main() {
+	examples.Run(tgb.NewRouter().
+		// start message and cbq handlers
+		Message(func(ctx context.Context, msg *tgb.MessageUpdate) error {
+			return msg.Update.Reply(ctx, newMenuMainMessage().AsSend(msg.Chat))
+		}, tgb.Command("start")).
+		CallbackQuery(func(ctx context.Context, cbq *tgb.CallbackQueryUpdate) error {
+			return cbq.Update.Reply(ctx, newMenuMainMessage().AsEditTextFromCBQ(cbq.CallbackQuery))
+		}, tgb.TextEqual("menu_main")).
+
+		// switch menu handlers
+		CallbackQuery(func(ctx context.Context, cbq *tgb.CallbackQueryUpdate) error {
+			_ = cbq.Update.Reply(ctx, cbq.Answer())
+
+			menuNum := cbq.Data[len("menu_"):]
+
+			n, err := strconv.Atoi(menuNum)
+			if err != nil {
+				return cbq.AnswerText("invalid menu number", true).DoVoid(ctx)
+			}
+
+			return cbq.Update.Reply(ctx, newSubmenu(n).AsEditTextFromCBQ(cbq.CallbackQuery))
+		}, tgb.Any(tgb.TextHasPrefix("menu_"))),
+	)
+}
diff --git a/request.go b/request.go
index c655ae0..7792d0e 100644
--- a/request.go
+++ b/request.go
@@ -91,6 +91,16 @@ func (r *Request) Has(name string) bool {
 	return inJSON || inArgs || inFiles
 }
 
+func (r *Request) GetArg(name string) (string, bool) {
+	v, ok := r.args[name]
+	return v, ok
+}
+
+func (r *Request) GetJSON(name string) (any, bool) {
+	v, ok := r.json[name]
+	return v, ok
+}
+
 func (r *Request) InputMediaSlice(name string, im []InputMedia) *Request {
 	for _, v := range im {
 		r.InputMedia(v)
diff --git a/tgb/builders.go b/tgb/builders.go
new file mode 100644
index 0000000..574c522
--- /dev/null
+++ b/tgb/builders.go
@@ -0,0 +1,206 @@
+package tgb
+
+import tg "github.com/mr-linch/go-tg"
+
+// TextMessageCallBuilder contains all common fields for methods sendText, editMessageText, editMessageReplyMarkup.
+// It's useful for building diffrent calls with the same params.
+//
+// Example:
+//
+//	newMenuBuilder(...).Client(client).AsSend(chat)
+//	newMenuBuilder(...).Client(client).AsEditTextFromCBQ(cbq.CallbackQuery)
+//
+// Also can be sended as reply to webhook
+//
+//	msg.Update.Reply(ctx, newMenuBuilder(...).AsEditTextFromCBQ(cbq.CallbackQuery))
+type TextMessageCallBuilder struct {
+	text               string
+	replyMarkup        tg.ReplyMarkup
+	linkPreviewOptions *tg.LinkPreviewOptions
+	entities           []tg.MessageEntity
+	parseMode          tg.ParseMode
+	client             *tg.Client
+}
+
+// NewTextMessageCallBuilder creates new TextMessageCallBuilder with specified text.
+func NewTextMessageCallBuilder(text string) *TextMessageCallBuilder {
+	return &TextMessageCallBuilder{
+		text: text,
+	}
+}
+
+// Client sets client for the message.
+func (b *TextMessageCallBuilder) Client(client *tg.Client) *TextMessageCallBuilder {
+	b.client = client
+	return b
+}
+
+// Text sets text for the message.
+func (b *TextMessageCallBuilder) Text(text string) *TextMessageCallBuilder {
+	b.text = text
+	return b
+}
+
+// ReplyMarkup sets reply markup for the message.
+func (b *TextMessageCallBuilder) ReplyMarkup(markup tg.ReplyMarkup) *TextMessageCallBuilder {
+	b.replyMarkup = markup
+	return b
+}
+
+// LinkPreviewOptions sets link preview options for the message.
+func (b *TextMessageCallBuilder) LinkPreviewOptions(options tg.LinkPreviewOptions) *TextMessageCallBuilder {
+	b.linkPreviewOptions = &options
+	return b
+}
+
+// Entities sets entities for the message.
+func (b *TextMessageCallBuilder) Entities(entities []tg.MessageEntity) *TextMessageCallBuilder {
+	b.entities = entities
+	return b
+}
+
+// ParseMode sets parse mode for the message.
+func (b *TextMessageCallBuilder) ParseMode(mode tg.ParseMode) *TextMessageCallBuilder {
+	b.parseMode = mode
+	return b
+}
+
+// AsSend returns call sendMessage with specified peer.
+func (b *TextMessageCallBuilder) AsSend(peer tg.PeerID) *tg.SendMessageCall {
+	call := tg.NewSendMessageCall(peer, b.text)
+
+	if b.replyMarkup != nil {
+		call.ReplyMarkup(b.replyMarkup)
+	}
+
+	if b.linkPreviewOptions != nil {
+		call.LinkPreviewOptions(*b.linkPreviewOptions)
+	}
+
+	if b.entities != nil {
+		call.Entities(b.entities)
+	}
+
+	if b.parseMode != nil {
+		call.ParseMode(b.parseMode)
+	}
+
+	if b.client != nil {
+		call.Bind(b.client)
+	}
+
+	return call
+}
+
+// AsEditText returns call editTextMessage with prepopulated fields.
+func (b *TextMessageCallBuilder) AsEditText(peer tg.PeerID, id int) *tg.EditMessageTextCall {
+	call := tg.NewEditMessageTextCall(peer, id, b.text)
+
+	if b.replyMarkup != nil {
+		if v, ok := b.replyMarkup.(tg.InlineKeyboardMarkup); ok {
+			call.ReplyMarkup(v)
+		}
+	}
+
+	if b.linkPreviewOptions != nil {
+		call.LinkPreviewOptions(*b.linkPreviewOptions)
+	}
+
+	if b.entities != nil {
+		call.Entities(b.entities)
+	}
+
+	if b.parseMode != nil {
+		call.ParseMode(b.parseMode)
+	}
+
+	if b.client != nil {
+		call.Bind(b.client)
+	}
+
+	return call
+}
+
+// AsEditTextFromCBQ wraps AsEditText with callback as argument.
+// It's useful if you have an object of CallbackQuery and want to edit it.
+func (b *TextMessageCallBuilder) AsEditTextFromCBQ(callback *tg.CallbackQuery) *tg.EditMessageTextCall {
+	return b.AsEditText(callback.Message.Chat(), callback.Message.MessageID())
+}
+
+// AsEditTextFromMsg wraps AsEditText with message as argument.
+// It's useful if you have an object of Message and want to edit it.
+func (b *TextMessageCallBuilder) AsEditTextFromMsg(msg *tg.Message) *tg.EditMessageTextCall {
+	return b.AsEditText(msg.Chat, msg.ID)
+}
+
+// AsEditTextInline returns call editTextMessage with by inline message id.
+func (b *TextMessageCallBuilder) AsEditTextInline(id string) *tg.EditMessageTextCall {
+	call := tg.NewEditMessageTextInlineCall(id, b.text)
+
+	if b.replyMarkup != nil {
+		if v, ok := b.replyMarkup.(tg.InlineKeyboardMarkup); ok {
+			call.ReplyMarkup(v)
+		}
+	}
+
+	if b.linkPreviewOptions != nil {
+		call.LinkPreviewOptions(*b.linkPreviewOptions)
+	}
+
+	if b.entities != nil {
+		call.Entities(b.entities)
+	}
+
+	if b.parseMode != nil {
+		call.ParseMode(b.parseMode)
+	}
+
+	if b.client != nil {
+		call.Bind(b.client)
+	}
+
+	return call
+}
+
+// AsEditReplyMarkup returns call editReplyMarkup with prepopulated fields.
+func (b *TextMessageCallBuilder) AsEditReplyMarkup(peer tg.PeerID, id int) *tg.EditMessageReplyMarkupCall {
+	call := tg.NewEditMessageReplyMarkupCall(peer, id)
+
+	if v, ok := b.replyMarkup.(tg.InlineKeyboardMarkup); ok {
+		call.ReplyMarkup(v)
+	}
+
+	if b.client != nil {
+		call.Bind(b.client)
+	}
+
+	return call
+
+}
+
+// AsEditReplyMarkupFromCBQ wraps AsEditReplyMarkup with callback as argument.
+// It's useful if you have an object of CallbackQuery and want to edit it.
+func (b *TextMessageCallBuilder) AsEditReplyMarkupFromCBQ(callback *tg.CallbackQuery) *tg.EditMessageReplyMarkupCall {
+	return b.AsEditReplyMarkup(callback.Message.Chat(), callback.Message.MessageID())
+}
+
+// AsEditReplyMarkupFromMsg wraps AsEditReplyMarkup with message as argument.
+// It's useful if you have an object of Message and want to edit it.
+func (b *TextMessageCallBuilder) AsEditReplyMarkupFromMsg(msg tg.Message) *tg.EditMessageReplyMarkupCall {
+	return b.AsEditReplyMarkup(msg.Chat, msg.ID)
+}
+
+// AsEditReplyMarkupInline returns call editReplyMarkup with by inline message id.
+func (b *TextMessageCallBuilder) AsEditReplyMarkupInline(id string) *tg.EditMessageReplyMarkupCall {
+	call := tg.NewEditMessageReplyMarkupInlineCall(id)
+
+	if v, ok := b.replyMarkup.(tg.InlineKeyboardMarkup); ok {
+		call.ReplyMarkup(v)
+	}
+
+	if b.client != nil {
+		call.Bind(b.client)
+	}
+
+	return call
+}
diff --git a/tgb/update.go b/tgb/update.go
index 1052326..02c10c4 100644
--- a/tgb/update.go
+++ b/tgb/update.go
@@ -178,7 +178,7 @@ func (msg *MessageUpdate) EditReplyMarkup(markup tg.InlineKeyboardMarkup) *tg.Ed
 }
 
 // React to incoming message.
-// No arguments means remove all reactions.
+// No arguments means remove all reactions from the message.
 func (msg *MessageUpdate) React(reactions ...tg.ReactionType) *tg.SetMessageReactionCall {
 	return msg.Client.SetMessageReaction(msg.Chat, msg.ID).
 		Reaction(reactions)


Test patch:
diff --git a/request_test.go b/request_test.go
index 386d99f..a3b033d 100644
--- a/request_test.go
+++ b/request_test.go
@@ -232,3 +232,25 @@ func TestRequest_Has(t *testing.T) {
 	assert.True(t, r.Has("chat_id"))
 	assert.False(t, r.Has("text"))
 }
+
+func TestRequest_Get(t *testing.T) {
+	r := NewRequest("sendMessage")
+
+	r.String("chat_id", "1")
+
+	v, ok := r.GetArg("chat_id")
+	assert.True(t, ok)
+	assert.Equal(t, "1", v)
+}
+
+func TestRequest_GetJSON(t *testing.T) {
+	r := NewRequest("sendMessage")
+
+	replyMarkup := InlineKeyboardMarkup{}
+
+	r.JSON("reply_markup", replyMarkup)
+
+	v, ok := r.GetJSON("reply_markup")
+	assert.True(t, ok)
+	assert.Equal(t, replyMarkup, v)
+}
diff --git a/tgb/builders_test.go b/tgb/builders_test.go
new file mode 100644
index 0000000..ff54707
--- /dev/null
+++ b/tgb/builders_test.go
@@ -0,0 +1,350 @@
+package tgb
+
+import (
+	"testing"
+
+	"github.com/mr-linch/go-tg"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTextMessageBuilder(t *testing.T) {
+	t.Run("AsSend", func(t *testing.T) {
+		client := &tg.Client{}
+		lpo := tg.LinkPreviewOptions{IsDisabled: true}
+		entities := []tg.MessageEntity{{Type: tg.MessageEntityTypeBold, Offset: 0, Length: 4}}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+		pm := tg.HTML
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			Text("text2").
+			LinkPreviewOptions(lpo).
+			Entities(entities).
+			ReplyMarkup(replyMarkup).
+			ParseMode(pm).
+			AsSend(tg.ChatID(1))
+
+		assert.Equal(t, "sendMessage", call.Request().Method)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("text")
+		require.True(t, ok)
+		assert.Equal(t, "text2", arg)
+
+		jsonArg, ok := call.Request().GetJSON("link_preview_options")
+		require.True(t, ok)
+		assert.Equal(t, lpo, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("entities")
+		require.True(t, ok)
+		assert.Equal(t, entities, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok = call.Request().GetArg("parse_mode")
+		require.True(t, ok)
+		assert.Equal(t, "HTML", arg)
+	})
+
+	t.Run("AsEditText", func(t *testing.T) {
+		client := &tg.Client{}
+		lpo := tg.LinkPreviewOptions{IsDisabled: true}
+		entities := []tg.MessageEntity{{Type: tg.MessageEntityTypeBold, Offset: 0, Length: 4}}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+		pm := tg.HTML
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			LinkPreviewOptions(lpo).
+			Entities(entities).
+			ReplyMarkup(replyMarkup).
+			ParseMode(pm).
+			AsEditText(tg.ChatID(1), 2)
+
+		assert.Equal(t, "editMessageText", call.Request().Method)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("text")
+		require.True(t, ok)
+		assert.Equal(t, "text", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+
+		jsonArg, ok := call.Request().GetJSON("link_preview_options")
+		require.True(t, ok)
+		assert.Equal(t, lpo, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("entities")
+		require.True(t, ok)
+		assert.Equal(t, entities, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok = call.Request().GetArg("parse_mode")
+		require.True(t, ok)
+		assert.Equal(t, "HTML", arg)
+	})
+
+	t.Run("AsEditTextFromCBQ", func(t *testing.T) {
+		client := &tg.Client{}
+		lpo := tg.LinkPreviewOptions{IsDisabled: true}
+		entities := []tg.MessageEntity{{Type: tg.MessageEntityTypeBold, Offset: 0, Length: 4}}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+		pm := tg.HTML
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			LinkPreviewOptions(lpo).
+			Entities(entities).
+			ReplyMarkup(replyMarkup).
+			ParseMode(pm).
+			AsEditTextFromCBQ(
+				&tg.CallbackQuery{
+					Message: &tg.MaybeInaccessibleMessage{
+						InaccessibleMessage: &tg.InaccessibleMessage{
+							Chat:      tg.Chat{ID: 1},
+							MessageID: 2,
+						},
+					},
+				},
+			)
+
+		assert.Equal(t, "editMessageText", call.Request().Method)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("text")
+		require.True(t, ok)
+		assert.Equal(t, "text", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+
+		jsonArg, ok := call.Request().GetJSON("link_preview_options")
+		require.True(t, ok)
+		assert.Equal(t, lpo, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("entities")
+		require.True(t, ok)
+		assert.Equal(t, entities, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok = call.Request().GetArg("parse_mode")
+		require.True(t, ok)
+		assert.Equal(t, "HTML", arg)
+	})
+
+	t.Run("AsEditTextFromMsg", func(t *testing.T) {
+		client := &tg.Client{}
+		lpo := tg.LinkPreviewOptions{IsDisabled: true}
+		entities := []tg.MessageEntity{{Type: tg.MessageEntityTypeBold, Offset: 0, Length: 4}}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+		pm := tg.HTML
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			LinkPreviewOptions(lpo).
+			Entities(entities).
+			ReplyMarkup(replyMarkup).
+			ParseMode(pm).
+			AsEditTextFromMsg(&tg.Message{
+				Chat: tg.Chat{ID: 1},
+				ID:   2,
+			})
+
+		assert.Equal(t, "editMessageText", call.Request().Method)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("text")
+		require.True(t, ok)
+		assert.Equal(t, "text", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+
+		jsonArg, ok := call.Request().GetJSON("link_preview_options")
+		require.True(t, ok)
+		assert.Equal(t, lpo, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("entities")
+		require.True(t, ok)
+		assert.Equal(t, entities, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok = call.Request().GetArg("parse_mode")
+		require.True(t, ok)
+		assert.Equal(t, "HTML", arg)
+	})
+
+	t.Run("AsEditTextInline", func(t *testing.T) {
+		client := &tg.Client{}
+		lpo := tg.LinkPreviewOptions{IsDisabled: true}
+		entities := []tg.MessageEntity{{Type: tg.MessageEntityTypeBold, Offset: 0, Length: 4}}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+		pm := tg.HTML
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			LinkPreviewOptions(lpo).
+			Entities(entities).
+			ReplyMarkup(replyMarkup).
+			ParseMode(pm).
+			AsEditTextInline("inline")
+
+		assert.Equal(t, "editMessageText", call.Request().Method)
+
+		arg, ok := call.Request().GetArg("inline_message_id")
+		require.True(t, ok)
+		assert.Equal(t, "inline", arg)
+
+		arg, ok = call.Request().GetArg("text")
+		require.True(t, ok)
+		assert.Equal(t, "text", arg)
+
+		jsonArg, ok := call.Request().GetJSON("link_preview_options")
+		require.True(t, ok)
+		assert.Equal(t, lpo, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("entities")
+		require.True(t, ok)
+		assert.Equal(t, entities, jsonArg)
+
+		jsonArg, ok = call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok = call.Request().GetArg("parse_mode")
+		require.True(t, ok)
+		assert.Equal(t, "HTML", arg)
+	})
+
+	t.Run("AsEditTextReplyMarkup", func(t *testing.T) {
+		client := &tg.Client{}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			ReplyMarkup(replyMarkup).
+			AsEditReplyMarkup(tg.ChatID(1), 2)
+
+		assert.Equal(t, "editMessageReplyMarkup", call.Request().Method)
+
+		jsonArg, ok := call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+	})
+
+	t.Run("AsEditTextReplyMarkupFromCBQ", func(t *testing.T) {
+		client := &tg.Client{}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			ReplyMarkup(replyMarkup).
+			AsEditReplyMarkupFromCBQ(
+				&tg.CallbackQuery{
+					Message: &tg.MaybeInaccessibleMessage{
+						InaccessibleMessage: &tg.InaccessibleMessage{
+							Chat:      tg.Chat{ID: 1},
+							MessageID: 2,
+						},
+					},
+				},
+			)
+
+		assert.Equal(t, "editMessageReplyMarkup", call.Request().Method)
+
+		jsonArg, ok := call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+	})
+
+	t.Run("AsEditTextReplyMarkupFromMsg", func(t *testing.T) {
+		client := &tg.Client{}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			ReplyMarkup(replyMarkup).
+			AsEditReplyMarkupFromMsg(tg.Message{
+				Chat: tg.Chat{ID: 1},
+				ID:   2,
+			})
+
+		assert.Equal(t, "editMessageReplyMarkup", call.Request().Method)
+
+		jsonArg, ok := call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok := call.Request().GetArg("chat_id")
+		require.True(t, ok)
+		assert.Equal(t, "1", arg)
+
+		arg, ok = call.Request().GetArg("message_id")
+		require.True(t, ok)
+		assert.Equal(t, "2", arg)
+	})
+
+	t.Run("AsEditReplyMarkupInline", func(t *testing.T) {
+		client := &tg.Client{}
+		replyMarkup := tg.NewInlineKeyboardMarkup()
+
+		call := NewTextMessageCallBuilder("text").
+			Client(client).
+			ReplyMarkup(replyMarkup).
+			AsEditReplyMarkupInline("inline")
+
+		assert.Equal(t, "editMessageReplyMarkup", call.Request().Method)
+
+		jsonArg, ok := call.Request().GetJSON("reply_markup")
+		require.True(t, ok)
+		assert.Equal(t, replyMarkup, jsonArg)
+
+		arg, ok := call.Request().GetArg("inline_message_id")
+		require.True(t, ok)
+		assert.Equal(t, "inline", arg)
+	})
+}