@@ -3,18 +3,115 @@ package botapi
33import (
44 "errors"
55 "net/http"
6+ "strconv"
67 "time"
78
89 "github.com/gotd/td/tgerr"
910)
1011
12+ // Shared Bot API error descriptions reused across the mapping and the methods.
13+ const (
14+ descChatNotFound = "Bad Request: chat not found"
15+ descWrongFileID = "Bad Request: wrong file identifier/HTTP URL specified"
16+ descUnauthorized = "Unauthorized"
17+
18+ descNotModified = "Bad Request: message is not modified: specified new message content " +
19+ "and reply markup are exactly the same as a current content and reply markup of the message"
20+ descPhotoExt = "Bad Request: Photo has unsupported extension. " +
21+ "Use one of .jpg, .jpeg, .gif, .png, .tif or .bmp"
22+ )
23+
24+ // rpcMapping is the final Bot API code and description for a known MTProto RPC
25+ // error type. Descriptions are the verbatim strings the official Bot API server
26+ // returns (including their "Bad Request:"/"Forbidden:" prefix), so callers see
27+ // behavior identical to the HTTP Bot API.
28+ type rpcMapping struct {
29+ code int
30+ desc string
31+ }
32+
33+ // rpcErrorMap translates MTProto RPC error types into Bot API errors. It mirrors
34+ // the official telegram-bot-api server (Client.cpp fail_query_with_error and the
35+ // TDLib send-error rewrites). Where the official description depends on the
36+ // calling method (e.g. "message to edit not found"), a method-independent
37+ // wording is used since asAPIError has no method context.
38+ var rpcErrorMap = map [string ]rpcMapping {
39+ // Chat / peer resolution.
40+ "PEER_ID_INVALID" : {http .StatusBadRequest , descChatNotFound },
41+ "CHAT_ID_INVALID" : {http .StatusBadRequest , descChatNotFound },
42+ "CHAT_NOT_FOUND" : {http .StatusBadRequest , descChatNotFound },
43+ "CHANNEL_INVALID" : {http .StatusBadRequest , descChatNotFound },
44+ "USERNAME_INVALID" : {http .StatusBadRequest , descChatNotFound },
45+ "USERNAME_NOT_OCCUPIED" : {http .StatusBadRequest , descChatNotFound },
46+ "USER_ID_INVALID" : {http .StatusBadRequest , "Bad Request: user not found" },
47+
48+ // Messages.
49+ "MESSAGE_ID_INVALID" : {http .StatusBadRequest , "Bad Request: message to be edited not found" },
50+ "MESSAGE_NOT_MODIFIED" : {http .StatusBadRequest , descNotModified },
51+ "MESSAGE_EMPTY" : {http .StatusBadRequest , "Bad Request: message text is empty" },
52+ "MESSAGE_TOO_LONG" : {http .StatusBadRequest , "Bad Request: message is too long" },
53+ "MEDIA_CAPTION_TOO_LONG" : {http .StatusBadRequest , "Bad Request: message caption is too long" },
54+ "MESSAGE_DELETE_FORBIDDEN" : {http .StatusBadRequest , "Bad Request: message can't be deleted" },
55+ "MESSAGE_AUTHOR_REQUIRED" : {http .StatusBadRequest , "Bad Request: message can't be edited" },
56+
57+ // Reply markup / buttons.
58+ "REPLY_MARKUP_TOO_LONG" : {http .StatusBadRequest , "Bad Request: reply markup is too long" },
59+ "REPLY_MARKUP_INVALID" : {http .StatusBadRequest , "Bad Request: reply markup is invalid" },
60+ "BUTTON_URL_INVALID" : {http .StatusBadRequest , "Bad Request: button URL is invalid" },
61+ "BUTTON_DATA_INVALID" : {http .StatusBadRequest , "Bad Request: button data is invalid" },
62+
63+ // URLs / web pages / media.
64+ "WC_CONVERT_URL_INVALID" : {http .StatusBadRequest , "Bad Request: Wrong HTTP URL specified" },
65+ "EXTERNAL_URL_INVALID" : {http .StatusBadRequest , "Bad Request: Wrong HTTP URL specified" },
66+ "WEBPAGE_CURL_FAILED" : {http .StatusBadRequest , "Bad Request: Failed to get HTTP URL content" },
67+ "WEBPAGE_MEDIA_EMPTY" : {http .StatusBadRequest , "Bad Request: Wrong type of the web page content" },
68+ "MEDIA_GROUPED_INVALID" : {http .StatusBadRequest , "Bad Request: Can't use the media of the specified type in the album" },
69+ "MEDIA_EMPTY" : {http .StatusBadRequest , "Bad Request: Wrong file identifier/HTTP URL specified" },
70+ "PHOTO_EXT_INVALID" : {http .StatusBadRequest , descPhotoExt },
71+ "PHOTO_INVALID_DIMENSIONS" : {http .StatusBadRequest , "Bad Request: PHOTO_INVALID_DIMENSIONS" },
72+ "FILE_PARTS_INVALID" : {http .StatusBadRequest , "Bad Request: file is too big" },
73+ "FILE_REFERENCE_EXPIRED" : {http .StatusBadRequest , descWrongFileID },
74+
75+ // Inline / callback queries.
76+ "QUERY_ID_INVALID" : {http .StatusBadRequest , "Bad Request: query is too old and response timeout expired or query ID is invalid" },
77+
78+ // Stickers.
79+ "PACK_SHORT_NAME_INVALID" : {http .StatusBadRequest , "Bad Request: invalid sticker set name is specified" },
80+ "PACK_SHORT_NAME_OCCUPIED" : {http .StatusBadRequest , "Bad Request: sticker set name is already occupied" },
81+ "STICKER_EMOJI_INVALID" : {http .StatusBadRequest , "Bad Request: invalid sticker emojis" },
82+ "STICKERSET_INVALID" : {http .StatusBadRequest , "Bad Request: STICKERSET_INVALID" },
83+
84+ // Chat description / about.
85+ "CHAT_ABOUT_NOT_MODIFIED" : {http .StatusBadRequest , "Bad Request: chat description is not modified" },
86+
87+ // Member management.
88+ "USER_ADMIN_INVALID" : {http .StatusBadRequest , "Bad Request: user is an administrator of the chat" },
89+ "CHAT_ADMIN_REQUIRED" : {http .StatusBadRequest , "Bad Request: not enough rights" },
90+ "USER_NOT_PARTICIPANT" : {http .StatusBadRequest , "Bad Request: user is not a member of the chat" },
91+ "USER_NOT_MUTUAL_CONTACT" : {http .StatusBadRequest , "Bad Request: user is not a mutual contact" },
92+ "ADMINS_TOO_MUCH" : {http .StatusBadRequest , "Bad Request: there are too many administrators in the chat" },
93+ "USER_CHANNELS_TOO_MUCH" : {http .StatusBadRequest , "Bad Request: the user is a member of too many chats" },
94+
95+ // Forbidden (403) cases.
96+ "INPUT_USER_DEACTIVATED" : {http .StatusForbidden , "Forbidden: user is deactivated" },
97+ "USER_IS_BLOCKED" : {http .StatusForbidden , "Forbidden: bot was blocked by the user" },
98+ "USER_IS_BOT" : {http .StatusForbidden , "Forbidden: bot can't send messages to bots" },
99+ "USER_DELETED" : {http .StatusForbidden , "Forbidden: user is deactivated" },
100+ "CHAT_WRITE_FORBIDDEN" : {http .StatusForbidden , "Forbidden: bot can't send messages to the chat" },
101+ "CHAT_SEND_MEDIA_FORBIDDEN" : {http .StatusForbidden , "Forbidden: not enough rights to send media to the chat" },
102+ "CHANNEL_PRIVATE" : {http .StatusForbidden , "Forbidden: bot is not a member of the chat" },
103+ "BOT_GROUPS_BLOCKED" : {http .StatusForbidden , "Forbidden: bot can't be added to groups" },
104+
105+ // Auth.
106+ "AUTH_KEY_UNREGISTERED" : {http .StatusUnauthorized , descUnauthorized },
107+ "SESSION_REVOKED" : {http .StatusUnauthorized , descUnauthorized },
108+ "USER_DEACTIVATED" : {http .StatusUnauthorized , descUnauthorized },
109+ }
110+
11111// asAPIError normalizes an error from the MTProto layer into a Bot-API-shaped
12112// *Error. It is a no-op for nil, for errors that are already *Error, and for
13113// non-RPC errors (e.g. context cancellation or network failures), which are
14114// returned unchanged so callers can branch on them.
15- //
16- // The mapping here covers the common cases; the full tgerr -> Bot API table is
17- // completed in Phase 6.
18115func asAPIError (err error ) error {
19116 if err == nil {
20117 return nil
@@ -32,38 +129,94 @@ func asAPIError(err error) error {
32129 if d , ok := tgerr .AsFloodWait (err ); ok {
33130 return & Error {
34131 Code : http .StatusTooManyRequests ,
35- Description : "Too Many Requests: retry later" ,
132+ Description : "Too Many Requests: retry after " + strconv . Itoa ( int ( d / time . Second )) ,
36133 Parameters : & ResponseParameters {RetryAfter : int (d / time .Second )},
37134 err : err ,
38135 }
39136 }
40137
41- out := & Error {Code : rpcErr .Code , Description : rpcErr .Message , err : err }
42- switch rpcErr .Type {
43- case "PEER_ID_INVALID" , "CHAT_ID_INVALID" , "CHAT_NOT_FOUND" :
44- out .Code , out .Description = http .StatusBadRequest , "Bad Request: chat not found"
45- case "MESSAGE_ID_INVALID" , "MESSAGE_NOT_FOUND" :
46- out .Code , out .Description = http .StatusBadRequest , "Bad Request: message not found"
47- case "MESSAGE_NOT_MODIFIED" :
48- out .Code = http .StatusBadRequest
49- out .Description = "Bad Request: message is not modified: specified new message content " +
50- "and reply markup are exactly the same as a current content and reply markup of the message"
51- case "MESSAGE_EMPTY" :
52- out .Code , out .Description = http .StatusBadRequest , "Bad Request: message text is empty"
53- case "REPLY_MARKUP_TOO_LONG" :
54- out .Code , out .Description = http .StatusBadRequest , "Bad Request: reply markup is too long"
55- case "USER_IS_BLOCKED" :
56- out .Code , out .Description = http .StatusForbidden , "Forbidden: bot was blocked by the user"
57- case "INPUT_USER_DEACTIVATED" :
58- out .Code , out .Description = http .StatusForbidden , "Forbidden: user is deactivated"
59- case "CHANNEL_PRIVATE" :
60- out .Code , out .Description = http .StatusForbidden , "Forbidden: bot is not a member of the chat"
61- case "USER_ADMIN_INVALID" :
62- out .Code , out .Description = http .StatusBadRequest , "Bad Request: user is an administrator of the chat"
138+ if m , ok := rpcErrorMap [rpcErr .Type ]; ok {
139+ return & Error {Code : m .code , Description : m .desc , err : err }
140+ }
141+
142+ // Unknown error: apply the official server's normalization and prefix rules.
143+ code := normalizeErrorCode (rpcErr .Code , rpcErr .Message )
144+ return & Error {
145+ Code : code ,
146+ Description : prefixDescription (code , rpcErr .Message ),
147+ err : err ,
148+ }
149+ }
150+
151+ // normalizeErrorCode mirrors the official server's code remapping: codes below
152+ // 400 (and 404) collapse to 400, and a 403 whose message is a bare SCREAMING_CASE
153+ // RPC constant is downgraded to 400.
154+ func normalizeErrorCode (code int , message string ) int {
155+ if code < http .StatusBadRequest || code == http .StatusNotFound {
156+ return http .StatusBadRequest
157+ }
158+ if code == http .StatusForbidden && isScreamingCase (message ) {
159+ return http .StatusBadRequest
160+ }
161+ return code
162+ }
163+
164+ // prefixDescription prepends the Bot API status prefix to a raw error message,
165+ // following the official casing rule: a SCREAMING_CASE constant is kept verbatim,
166+ // otherwise the first letter is lowercased.
167+ func prefixDescription (code int , message string ) string {
168+ prefix := statusPrefix (code )
169+ if message == "" {
170+ return prefix
171+ }
172+ if hasPrefix (message , prefix ) {
173+ return message
174+ }
175+ if len (message ) >= 2 && (message [1 ] == '_' || isUpper (message [1 ])) {
176+ return prefix + ": " + message
177+ }
178+ return prefix + ": " + lowerFirst (message )
179+ }
180+
181+ // statusPrefix returns the Bot API description prefix for a status code. Unknown
182+ // codes use the 400 prefix (matching the official server's default branch).
183+ func statusPrefix (code int ) string {
184+ switch code {
185+ case http .StatusUnauthorized :
186+ return "Unauthorized"
187+ case http .StatusForbidden :
188+ return "Forbidden"
189+ case http .StatusInternalServerError :
190+ return "Internal Server Error"
63191 default :
64- if out .Description == "" {
65- out .Description = http .StatusText (out .Code )
192+ return "Bad Request"
193+ }
194+ }
195+
196+ func isScreamingCase (s string ) bool {
197+ if s == "" {
198+ return false
199+ }
200+ for i := 0 ; i < len (s ); i ++ {
201+ c := s [i ]
202+ if ! (c >= 'A' && c <= 'Z' ) && ! (c >= '0' && c <= '9' ) && c != '_' {
203+ return false
66204 }
67205 }
68- return out
206+ return true
207+ }
208+
209+ func isUpper (c byte ) bool { return c >= 'A' && c <= 'Z' }
210+
211+ func hasPrefix (s , prefix string ) bool {
212+ return len (s ) >= len (prefix ) && s [:len (prefix )] == prefix
213+ }
214+
215+ func lowerFirst (s string ) string {
216+ if s == "" || ! isUpper (s [0 ]) {
217+ return s
218+ }
219+ b := []byte (s )
220+ b [0 ] += 'a' - 'A'
221+ return string (b )
69222}
0 commit comments