Initial codex telegram bot source
This commit is contained in:
211
internal/telegram/api.go
Normal file
211
internal/telegram/api.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
token string
|
||||
baseURL string
|
||||
fileBaseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(token string) *Client {
|
||||
return &Client{
|
||||
token: token,
|
||||
baseURL: "https://api.telegram.org/bot" + token,
|
||||
fileBaseURL: "https://api.telegram.org/file/bot" + token,
|
||||
httpClient: &http.Client{Timeout: 90 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetUpdates(ctx context.Context, offset int, timeoutSeconds int) ([]Update, error) {
|
||||
params := map[string]any{
|
||||
"offset": offset,
|
||||
"timeout": timeoutSeconds,
|
||||
"allowed_updates": []string{"message", "callback_query"},
|
||||
}
|
||||
var updates []Update
|
||||
if err := c.postJSON(ctx, "getUpdates", params, &updates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(ctx context.Context, chatID int64, text string, opts SendMessageOptions) (Message, error) {
|
||||
params := map[string]any{
|
||||
"chat_id": chatID,
|
||||
"text": text,
|
||||
}
|
||||
if opts.ParseMode != "" {
|
||||
params["parse_mode"] = opts.ParseMode
|
||||
}
|
||||
if opts.ReplyMarkup != nil {
|
||||
params["reply_markup"] = opts.ReplyMarkup
|
||||
}
|
||||
var message Message
|
||||
if err := c.postJSON(ctx, "sendMessage", params, &message); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (c *Client) EditMessageText(ctx context.Context, chatID int64, messageID int, text string, opts EditMessageTextOptions) (Message, error) {
|
||||
params := map[string]any{
|
||||
"chat_id": chatID,
|
||||
"message_id": messageID,
|
||||
"text": text,
|
||||
}
|
||||
if opts.ParseMode != "" {
|
||||
params["parse_mode"] = opts.ParseMode
|
||||
}
|
||||
if opts.ReplyMarkup != nil {
|
||||
params["reply_markup"] = opts.ReplyMarkup
|
||||
}
|
||||
var message Message
|
||||
if err := c.postJSON(ctx, "editMessageText", params, &message); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (c *Client) PinChatMessage(ctx context.Context, chatID int64, messageID int, disableNotification bool) error {
|
||||
params := map[string]any{
|
||||
"chat_id": chatID,
|
||||
"message_id": messageID,
|
||||
"disable_notification": disableNotification,
|
||||
}
|
||||
var ignored bool
|
||||
return c.postJSON(ctx, "pinChatMessage", params, &ignored)
|
||||
}
|
||||
|
||||
func (c *Client) AnswerCallbackQuery(ctx context.Context, callbackQueryID, text string) error {
|
||||
params := map[string]any{
|
||||
"callback_query_id": callbackQueryID,
|
||||
}
|
||||
if text != "" {
|
||||
params["text"] = text
|
||||
}
|
||||
var ignored bool
|
||||
return c.postJSON(ctx, "answerCallbackQuery", params, &ignored)
|
||||
}
|
||||
|
||||
func (c *Client) GetFile(ctx context.Context, fileID string) (File, error) {
|
||||
var file File
|
||||
if err := c.postJSON(ctx, "getFile", map[string]any{"file_id": fileID}, &file); err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (c *Client) DownloadFile(ctx context.Context, filePath string) ([]byte, error) {
|
||||
u := c.fileBaseURL + "/" + url.PathEscape(filePath)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("download file: telegram returned %s", resp.Status)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (c *Client) SendDocumentBytes(ctx context.Context, chatID int64, filename string, data []byte, caption string) (Message, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
if err := writer.WriteField("chat_id", fmt.Sprint(chatID)); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if caption != "" {
|
||||
if err := writer.WriteField("caption", caption); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
}
|
||||
part, err := writer.CreateFormFile("document", filepath.Base(filename))
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/sendDocument", &body)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
return Message{}, fmt.Errorf("sendDocument: telegram returned %s: %s", resp.Status, string(payload))
|
||||
}
|
||||
var decoded apiResponse[Message]
|
||||
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if !decoded.OK {
|
||||
return Message{}, fmt.Errorf("sendDocument: telegram error %d: %s", decoded.ErrorCode, decoded.Description)
|
||||
}
|
||||
return decoded.Result, nil
|
||||
}
|
||||
|
||||
func (c *Client) postJSON(ctx context.Context, method string, params any, result any) error {
|
||||
body, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/"+method, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
return fmt.Errorf("%s: telegram returned %s: %s", method, resp.Status, string(payload))
|
||||
}
|
||||
var decoded apiResponse[json.RawMessage]
|
||||
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if !decoded.OK {
|
||||
return fmt.Errorf("%s: telegram error %d: %s", method, decoded.ErrorCode, decoded.Description)
|
||||
}
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(decoded.Result, result)
|
||||
}
|
||||
|
||||
type apiResponse[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
1760
internal/telegram/bot.go
Normal file
1760
internal/telegram/bot.go
Normal file
File diff suppressed because it is too large
Load Diff
26
internal/telegram/download.go
Normal file
26
internal/telegram/download.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Client) DownloadFilePath(ctx context.Context, filePath string) ([]byte, error) {
|
||||
u := c.fileBaseURL + "/" + strings.TrimLeft(filePath, "/")
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("download file: telegram returned %s", resp.Status)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
183
internal/telegram/render.go
Normal file
183
internal/telegram/render.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const TelegramMessageLimit = 4096
|
||||
const TelegramHTMLMessageLimit = 3900
|
||||
|
||||
func EscapeHTML(text string) string {
|
||||
return html.EscapeString(text)
|
||||
}
|
||||
|
||||
func SummaryDetailsHTML(summary, details string) string {
|
||||
summary = strings.TrimSpace(summary)
|
||||
details = strings.TrimSpace(details)
|
||||
if details == "" {
|
||||
return EscapeHTML(summary)
|
||||
}
|
||||
if summary == "" {
|
||||
return ExpandableQuoteHTML(details)
|
||||
}
|
||||
return EscapeHTML(summary) + "\n" + ExpandableQuoteHTML(details)
|
||||
}
|
||||
|
||||
func ExpandableQuoteHTML(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
return "<blockquote expandable>" + EscapeHTML(text) + "</blockquote>"
|
||||
}
|
||||
|
||||
func SummaryDetailsHTMLLimited(summary, details string, limit int) string {
|
||||
if limit <= 0 {
|
||||
limit = TelegramHTMLMessageLimit
|
||||
}
|
||||
summary = strings.TrimSpace(summary)
|
||||
details = strings.TrimSpace(details)
|
||||
out := SummaryDetailsHTML(summary, details)
|
||||
if len([]rune(out)) <= limit || details == "" {
|
||||
return out
|
||||
}
|
||||
|
||||
suffix := "\n...[truncated]"
|
||||
runes := []rune(details)
|
||||
for len(runes) > 0 {
|
||||
candidateLen := len(runes) - max(1, (len([]rune(out))-limit)/2)
|
||||
if candidateLen < 0 {
|
||||
candidateLen = 0
|
||||
}
|
||||
if candidateLen > len(runes) {
|
||||
candidateLen = len(runes)
|
||||
}
|
||||
candidate := strings.TrimSpace(string(runes[:candidateLen])) + suffix
|
||||
out = SummaryDetailsHTML(summary, candidate)
|
||||
if len([]rune(out)) <= limit || candidateLen == 0 {
|
||||
return out
|
||||
}
|
||||
runes = runes[:candidateLen]
|
||||
}
|
||||
return SummaryDetailsHTML(summary, suffix)
|
||||
}
|
||||
|
||||
func ChunkText(text string, max int) []string {
|
||||
if max <= 0 {
|
||||
max = TelegramMessageLimit
|
||||
}
|
||||
runes := []rune(text)
|
||||
if len(runes) == 0 {
|
||||
return nil
|
||||
}
|
||||
var chunks []string
|
||||
for len(runes) > max {
|
||||
cut := max
|
||||
for i := max; i > max/2; i-- {
|
||||
if runes[i-1] == '\n' {
|
||||
cut = i
|
||||
break
|
||||
}
|
||||
}
|
||||
chunks = append(chunks, string(runes[:cut]))
|
||||
runes = runes[cut:]
|
||||
}
|
||||
if len(runes) > 0 {
|
||||
chunks = append(chunks, string(runes))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func ApprovalCallbackData(id int64, decision string) string {
|
||||
return fmt.Sprintf("approval:%d:%s", id, decision)
|
||||
}
|
||||
|
||||
func ParseApprovalCallbackData(data string) (int64, string, bool) {
|
||||
parts := strings.Split(data, ":")
|
||||
if len(parts) != 3 || parts[0] != "approval" {
|
||||
return 0, "", false
|
||||
}
|
||||
id, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, "", false
|
||||
}
|
||||
switch parts[2] {
|
||||
case "accept", "acceptForSession", "decline", "cancel", "details":
|
||||
return id, parts[2], true
|
||||
default:
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
|
||||
func WorkspaceCallbackData(id int64) string {
|
||||
return fmt.Sprintf("workspace:%d", id)
|
||||
}
|
||||
|
||||
func ParseWorkspaceCallbackData(data string) (int64, bool) {
|
||||
if !strings.HasPrefix(data, "workspace:") {
|
||||
return 0, false
|
||||
}
|
||||
id, err := strconv.ParseInt(strings.TrimPrefix(data, "workspace:"), 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
func ResumeThreadCallbackData(id int64) string {
|
||||
return fmt.Sprintf("resume:thread:%d", id)
|
||||
}
|
||||
|
||||
func ParseResumeThreadCallbackData(data string) (int64, bool) {
|
||||
if !strings.HasPrefix(data, "resume:thread:") {
|
||||
return 0, false
|
||||
}
|
||||
id, err := strconv.ParseInt(strings.TrimPrefix(data, "resume:thread:"), 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
func ResumePageCallbackData(page int) string {
|
||||
return fmt.Sprintf("resume:page:%d", page)
|
||||
}
|
||||
|
||||
func ParseResumePageCallbackData(data string) (int, bool) {
|
||||
if !strings.HasPrefix(data, "resume:page:") {
|
||||
return 0, false
|
||||
}
|
||||
page, err := strconv.Atoi(strings.TrimPrefix(data, "resume:page:"))
|
||||
return page, err == nil && page >= 0
|
||||
}
|
||||
|
||||
func ModelCallbackData(modelID string) (string, bool) {
|
||||
encoded := base64.RawURLEncoding.EncodeToString([]byte(modelID))
|
||||
data := "model:" + encoded
|
||||
return data, len([]rune(data)) <= 64
|
||||
}
|
||||
|
||||
func ParseModelCallbackData(data string) (string, bool) {
|
||||
if !strings.HasPrefix(data, "model:") {
|
||||
return "", false
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(data, "model:"))
|
||||
if err != nil || len(decoded) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return string(decoded), true
|
||||
}
|
||||
|
||||
func EffortCallbackData(effort string) string {
|
||||
encoded := base64.RawURLEncoding.EncodeToString([]byte(effort))
|
||||
return "effort:" + encoded
|
||||
}
|
||||
|
||||
func ParseEffortCallbackData(data string) (string, bool) {
|
||||
if !strings.HasPrefix(data, "effort:") {
|
||||
return "", false
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(data, "effort:"))
|
||||
if err != nil || len(decoded) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return string(decoded), true
|
||||
}
|
||||
152
internal/telegram/render_test.go
Normal file
152
internal/telegram/render_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"codex-telegram-bot/internal/store"
|
||||
)
|
||||
|
||||
func TestEscapeHTML(t *testing.T) {
|
||||
got := EscapeHTML(`<run & "test">`)
|
||||
want := "<run & "test">"
|
||||
if got != want {
|
||||
t.Fatalf("EscapeHTML() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkText(t *testing.T) {
|
||||
text := strings.Repeat("a", 25)
|
||||
chunks := ChunkText(text, 10)
|
||||
if len(chunks) != 3 {
|
||||
t.Fatalf("got %d chunks", len(chunks))
|
||||
}
|
||||
for _, chunk := range chunks {
|
||||
if len([]rune(chunk)) > 10 {
|
||||
t.Fatalf("chunk too long: %q", chunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalCallbackData(t *testing.T) {
|
||||
data := ApprovalCallbackData(12, "accept")
|
||||
id, decision, ok := ParseApprovalCallbackData(data)
|
||||
if !ok || id != 12 || decision != "accept" {
|
||||
t.Fatalf("unexpected callback parse: id=%d decision=%s ok=%v", id, decision, ok)
|
||||
}
|
||||
if _, _, ok := ParseApprovalCallbackData("approval:12:unknown"); ok {
|
||||
t.Fatal("unknown decisions should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalResponseForPermissions(t *testing.T) {
|
||||
approval := store.PendingApproval{
|
||||
Kind: "item/permissions/requestApproval",
|
||||
PayloadJSON: `{"permissions":{"network":{"enabled":true}}}`,
|
||||
}
|
||||
response, ok := approvalResponse(approval, "accept").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("approval response should be a map")
|
||||
}
|
||||
if response["scope"] != "turn" {
|
||||
t.Fatalf("scope = %v, want turn", response["scope"])
|
||||
}
|
||||
permissions, ok := response["permissions"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("permissions should be a map")
|
||||
}
|
||||
network, ok := permissions["network"].(map[string]any)
|
||||
if !ok || network["enabled"] != true {
|
||||
t.Fatalf("unexpected permissions: %#v", permissions)
|
||||
}
|
||||
|
||||
denied, ok := approvalResponse(approval, "decline").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("denied response should be a map")
|
||||
}
|
||||
deniedPermissions, ok := denied["permissions"].(map[string]any)
|
||||
if !ok || len(deniedPermissions) != 0 {
|
||||
t.Fatalf("denied permissions = %#v, want empty map", denied["permissions"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommand(t *testing.T) {
|
||||
name, args, ok := parseCommand("/resume@my_bot 123")
|
||||
if !ok || name != "resume" || len(args) != 1 || args[0] != "123" {
|
||||
t.Fatalf("unexpected command parse: %q %#v %v", name, args, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCodexCommandExecutionItem(t *testing.T) {
|
||||
output := "line 1\nline 2"
|
||||
exitCode := 0
|
||||
item := codexThreadItemView{
|
||||
Type: "commandExecution",
|
||||
Command: "go test ./...",
|
||||
AggregatedOutput: &output,
|
||||
ExitCode: &exitCode,
|
||||
}
|
||||
text := renderCodexItemCompleted(item)
|
||||
for _, want := range []string{"Tool call: command finished", "Command: go test ./...", "Exit code: 0", "line 1"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered command item missing %q in %q", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCodexStartedItems(t *testing.T) {
|
||||
text := renderCodexItemStarted(codexThreadItemView{Type: "webSearch", Query: "telegram bot api"})
|
||||
if !strings.Contains(text, "web search started") || !strings.Contains(text, "telegram bot api") {
|
||||
t.Fatalf("unexpected web search render: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeCallbackData(t *testing.T) {
|
||||
threadID, ok := ParseResumeThreadCallbackData(ResumeThreadCallbackData(123))
|
||||
if !ok || threadID != 123 {
|
||||
t.Fatalf("unexpected resume thread callback: id=%d ok=%v", threadID, ok)
|
||||
}
|
||||
page, ok := ParseResumePageCallbackData(ResumePageCallbackData(2))
|
||||
if !ok || page != 2 {
|
||||
t.Fatalf("unexpected resume page callback: page=%d ok=%v", page, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeThreadListText(t *testing.T) {
|
||||
threads := []store.Thread{{ID: 42, Title: "do xyz"}, {ID: 43, Title: "executed xxx command"}}
|
||||
text := resumeThreadListText(threads, 0)
|
||||
for _, want := range []string{"Thread ID 42: do xyz", "Thread ID 43: executed xxx command"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("resume list missing %q in %q", want, text)
|
||||
}
|
||||
}
|
||||
markup := resumeThreadMarkup(threads, 0, true)
|
||||
if len(markup.InlineKeyboard) != 2 || markup.InlineKeyboard[0][0].Text != "ID 42" || markup.InlineKeyboard[0][1].Text != "ID 43" {
|
||||
t.Fatalf("unexpected resume buttons: %#v", markup.InlineKeyboard)
|
||||
}
|
||||
firstID, ok := ParseResumeThreadCallbackData(markup.InlineKeyboard[0][0].CallbackData)
|
||||
if !ok || firstID != 42 {
|
||||
t.Fatalf("first resume button targets id=%d ok=%v", firstID, ok)
|
||||
}
|
||||
secondID, ok := ParseResumeThreadCallbackData(markup.InlineKeyboard[0][1].CallbackData)
|
||||
if !ok || secondID != 43 {
|
||||
t.Fatalf("second resume button targets id=%d ok=%v", secondID, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelAndEffortCallbackData(t *testing.T) {
|
||||
modelID := strings.Join([]string{"server", "model", "id"}, "-")
|
||||
data, ok := ModelCallbackData(modelID)
|
||||
if !ok {
|
||||
t.Fatal("model callback should fit")
|
||||
}
|
||||
model, ok := ParseModelCallbackData(data)
|
||||
if !ok || model != modelID {
|
||||
t.Fatalf("unexpected model callback parse: model=%q ok=%v", model, ok)
|
||||
}
|
||||
effortName := strings.Join([]string{"server", "effort"}, "-")
|
||||
effort, ok := ParseEffortCallbackData(EffortCallbackData(effortName))
|
||||
if !ok || effort != effortName {
|
||||
t.Fatalf("unexpected effort callback parse: effort=%q ok=%v", effort, ok)
|
||||
}
|
||||
}
|
||||
76
internal/telegram/types.go
Normal file
76
internal/telegram/types.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package telegram
|
||||
|
||||
type Update struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
Message *Message `json:"message,omitempty"`
|
||||
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
MessageID int `json:"message_id"`
|
||||
From *User `json:"from,omitempty"`
|
||||
Chat Chat `json:"chat"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
Document *Document `json:"document,omitempty"`
|
||||
Photo []PhotoSize `json:"photo,omitempty"`
|
||||
PinnedMessage *Message `json:"pinned_message,omitempty"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
type PhotoSize struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type CallbackQuery struct {
|
||||
ID string `json:"id"`
|
||||
From User `json:"from"`
|
||||
Message *Message `json:"message,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileID string `json:"file_id"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
type InlineKeyboardMarkup struct {
|
||||
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
|
||||
}
|
||||
|
||||
type InlineKeyboardButton struct {
|
||||
Text string `json:"text"`
|
||||
CallbackData string `json:"callback_data,omitempty"`
|
||||
}
|
||||
|
||||
type SendMessageOptions struct {
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
type EditMessageTextOptions struct {
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user