Skip to content

Commit 26d1a1f

Browse files
ernadoclaude
andcommitted
feat(passport): implement SetPassportDataErrors
Add the PassportElementError sealed union (data field, front/reverse side, selfie, file, files, translation file/files, unspecified) mapping to MTProto secure-value errors with base64 hash decoding and the element-type -> SecureValueType mapping. The conformance deferred list is now empty: every published method is implemented or categorized. Tell goconst to ignore test files so enum-mapping case labels aren't flagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d3c36fb commit 26d1a1f

4 files changed

Lines changed: 317 additions & 3 deletions

File tree

.golangci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ linters:
4242
goconst:
4343
min-len: 2
4444
min-occurrences: 3
45+
# Enum-mapping switch labels (e.g. secureValueType) repeat as test inputs;
46+
# that is not a reason to introduce a constant.
47+
ignore-tests: true
4548
misspell:
4649
locale: US
4750
lll:

conformance_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ var coveredByOtherMeans = map[string]string{
2020
// deferredMethods lists published methods that are planned but not yet
2121
// implemented. Each is an acknowledged gap tracked in docs/roadmap.md; the
2222
// conformance test allows them so it can still catch *unacknowledged* drift.
23-
var deferredMethods = map[string]string{
24-
"setPassportDataErrors": "Telegram Passport — deferred",
25-
}
23+
//
24+
// Empty: every method published in the snapshot is now implemented or
25+
// categorized as not-applicable below.
26+
var deferredMethods = map[string]string{}
2627

2728
// notApplicableMethods lists published methods that do not apply to the
2829
// MTProto-native model and are intentionally not implemented.

passport.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// PassportElementError is a sealed union describing one error in a Telegram
11+
// Passport element. The user will not be able to resubmit the element until the
12+
// error is resolved.
13+
//
14+
// Concrete variants mirror the Bot API: *PassportElementErrorDataField,
15+
// *PassportElementErrorFrontSide, *PassportElementErrorReverseSide,
16+
// *PassportElementErrorSelfie, *PassportElementErrorFile,
17+
// *PassportElementErrorFiles, *PassportElementErrorTranslationFile,
18+
// *PassportElementErrorTranslationFiles, *PassportElementErrorUnspecified.
19+
type PassportElementError interface {
20+
isPassportElementError()
21+
toTg() (tg.SecureValueErrorClass, error)
22+
}
23+
24+
// PassportElementErrorDataField is an error in a data field.
25+
type PassportElementErrorDataField struct {
26+
Type string `json:"type"`
27+
FieldName string `json:"field_name"`
28+
DataHash string `json:"data_hash"`
29+
Message string `json:"message"`
30+
}
31+
32+
// PassportElementErrorFrontSide is an error in the document's front side.
33+
type PassportElementErrorFrontSide struct {
34+
Type string `json:"type"`
35+
FileHash string `json:"file_hash"`
36+
Message string `json:"message"`
37+
}
38+
39+
// PassportElementErrorReverseSide is an error in the document's reverse side.
40+
type PassportElementErrorReverseSide struct {
41+
Type string `json:"type"`
42+
FileHash string `json:"file_hash"`
43+
Message string `json:"message"`
44+
}
45+
46+
// PassportElementErrorSelfie is an error in the selfie with the document.
47+
type PassportElementErrorSelfie struct {
48+
Type string `json:"type"`
49+
FileHash string `json:"file_hash"`
50+
Message string `json:"message"`
51+
}
52+
53+
// PassportElementErrorFile is an error in a document scan.
54+
type PassportElementErrorFile struct {
55+
Type string `json:"type"`
56+
FileHash string `json:"file_hash"`
57+
Message string `json:"message"`
58+
}
59+
60+
// PassportElementErrorFiles is an error in a list of document scans.
61+
type PassportElementErrorFiles struct {
62+
Type string `json:"type"`
63+
FileHashes []string `json:"file_hashes"`
64+
Message string `json:"message"`
65+
}
66+
67+
// PassportElementErrorTranslationFile is an error in a translation scan.
68+
type PassportElementErrorTranslationFile struct {
69+
Type string `json:"type"`
70+
FileHash string `json:"file_hash"`
71+
Message string `json:"message"`
72+
}
73+
74+
// PassportElementErrorTranslationFiles is an error in a list of translation
75+
// scans.
76+
type PassportElementErrorTranslationFiles struct {
77+
Type string `json:"type"`
78+
FileHashes []string `json:"file_hashes"`
79+
Message string `json:"message"`
80+
}
81+
82+
// PassportElementErrorUnspecified is an error in an unspecified place.
83+
type PassportElementErrorUnspecified struct {
84+
Type string `json:"type"`
85+
ElementHash string `json:"element_hash"`
86+
Message string `json:"message"`
87+
}
88+
89+
func (*PassportElementErrorDataField) isPassportElementError() {}
90+
func (*PassportElementErrorFrontSide) isPassportElementError() {}
91+
func (*PassportElementErrorReverseSide) isPassportElementError() {}
92+
func (*PassportElementErrorSelfie) isPassportElementError() {}
93+
func (*PassportElementErrorFile) isPassportElementError() {}
94+
func (*PassportElementErrorFiles) isPassportElementError() {}
95+
func (*PassportElementErrorTranslationFile) isPassportElementError() {}
96+
func (*PassportElementErrorTranslationFiles) isPassportElementError() {}
97+
func (*PassportElementErrorUnspecified) isPassportElementError() {}
98+
99+
func (e *PassportElementErrorDataField) toTg() (tg.SecureValueErrorClass, error) {
100+
h, err := decodeHash(e.DataHash)
101+
if err != nil {
102+
return nil, err
103+
}
104+
return &tg.SecureValueErrorData{Type: secureValueType(e.Type), DataHash: h, Field: e.FieldName, Text: e.Message}, nil
105+
}
106+
107+
func (e *PassportElementErrorFrontSide) toTg() (tg.SecureValueErrorClass, error) {
108+
h, err := decodeHash(e.FileHash)
109+
if err != nil {
110+
return nil, err
111+
}
112+
return &tg.SecureValueErrorFrontSide{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
113+
}
114+
115+
func (e *PassportElementErrorReverseSide) toTg() (tg.SecureValueErrorClass, error) {
116+
h, err := decodeHash(e.FileHash)
117+
if err != nil {
118+
return nil, err
119+
}
120+
return &tg.SecureValueErrorReverseSide{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
121+
}
122+
123+
func (e *PassportElementErrorSelfie) toTg() (tg.SecureValueErrorClass, error) {
124+
h, err := decodeHash(e.FileHash)
125+
if err != nil {
126+
return nil, err
127+
}
128+
return &tg.SecureValueErrorSelfie{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
129+
}
130+
131+
func (e *PassportElementErrorFile) toTg() (tg.SecureValueErrorClass, error) {
132+
h, err := decodeHash(e.FileHash)
133+
if err != nil {
134+
return nil, err
135+
}
136+
return &tg.SecureValueErrorFile{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
137+
}
138+
139+
func (e *PassportElementErrorFiles) toTg() (tg.SecureValueErrorClass, error) {
140+
h, err := decodeHashes(e.FileHashes)
141+
if err != nil {
142+
return nil, err
143+
}
144+
return &tg.SecureValueErrorFiles{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
145+
}
146+
147+
func (e *PassportElementErrorTranslationFile) toTg() (tg.SecureValueErrorClass, error) {
148+
h, err := decodeHash(e.FileHash)
149+
if err != nil {
150+
return nil, err
151+
}
152+
return &tg.SecureValueErrorTranslationFile{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
153+
}
154+
155+
func (e *PassportElementErrorTranslationFiles) toTg() (tg.SecureValueErrorClass, error) {
156+
h, err := decodeHashes(e.FileHashes)
157+
if err != nil {
158+
return nil, err
159+
}
160+
return &tg.SecureValueErrorTranslationFiles{Type: secureValueType(e.Type), FileHash: h, Text: e.Message}, nil
161+
}
162+
163+
func (e *PassportElementErrorUnspecified) toTg() (tg.SecureValueErrorClass, error) {
164+
h, err := decodeHash(e.ElementHash)
165+
if err != nil {
166+
return nil, err
167+
}
168+
return &tg.SecureValueError{Type: secureValueType(e.Type), Hash: h, Text: e.Message}, nil
169+
}
170+
171+
// SetPassportDataErrors reports errors in the Telegram Passport data the user
172+
// submitted to the bot, so the user can fix and resubmit them.
173+
func (b *Bot) SetPassportDataErrors(ctx context.Context, userID int64, errs []PassportElementError) error {
174+
user, err := b.resolveInputUser(ctx, userID)
175+
if err != nil {
176+
return err
177+
}
178+
179+
tgErrors := make([]tg.SecureValueErrorClass, 0, len(errs))
180+
for _, e := range errs {
181+
if e == nil {
182+
continue
183+
}
184+
converted, err := e.toTg()
185+
if err != nil {
186+
return err
187+
}
188+
tgErrors = append(tgErrors, converted)
189+
}
190+
191+
if _, err := b.raw.UsersSetSecureValueErrors(ctx, &tg.UsersSetSecureValueErrorsRequest{
192+
ID: user,
193+
Errors: tgErrors,
194+
}); err != nil {
195+
return asAPIError(err)
196+
}
197+
return nil
198+
}
199+
200+
// decodeHash decodes a base64 Passport data hash into raw bytes.
201+
func decodeHash(s string) ([]byte, error) {
202+
h, err := base64.StdEncoding.DecodeString(s)
203+
if err != nil {
204+
return nil, &Error{Code: 400, Description: "Bad Request: invalid passport data hash"}
205+
}
206+
return h, nil
207+
}
208+
209+
func decodeHashes(hashes []string) ([][]byte, error) {
210+
out := make([][]byte, 0, len(hashes))
211+
for _, s := range hashes {
212+
h, err := decodeHash(s)
213+
if err != nil {
214+
return nil, err
215+
}
216+
out = append(out, h)
217+
}
218+
return out, nil
219+
}
220+
221+
// secureValueType maps a Bot API Passport element type to the MTProto secure
222+
// value type. An unknown type falls back to personal details.
223+
func secureValueType(t string) tg.SecureValueTypeClass {
224+
switch t {
225+
case "personal_details":
226+
return &tg.SecureValueTypePersonalDetails{}
227+
case "passport":
228+
return &tg.SecureValueTypePassport{}
229+
case "driver_license":
230+
return &tg.SecureValueTypeDriverLicense{}
231+
case "identity_card":
232+
return &tg.SecureValueTypeIdentityCard{}
233+
case "internal_passport":
234+
return &tg.SecureValueTypeInternalPassport{}
235+
case "address":
236+
return &tg.SecureValueTypeAddress{}
237+
case "utility_bill":
238+
return &tg.SecureValueTypeUtilityBill{}
239+
case "bank_statement":
240+
return &tg.SecureValueTypeBankStatement{}
241+
case "rental_agreement":
242+
return &tg.SecureValueTypeRentalAgreement{}
243+
case "passport_registration":
244+
return &tg.SecureValueTypePassportRegistration{}
245+
case "temporary_registration":
246+
return &tg.SecureValueTypeTemporaryRegistration{}
247+
case "phone_number":
248+
return &tg.SecureValueTypePhone{}
249+
case "email":
250+
return &tg.SecureValueTypeEmail{}
251+
default:
252+
return &tg.SecureValueTypePersonalDetails{}
253+
}
254+
}

passport_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package botapi
2+
3+
import (
4+
"encoding/base64"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestPassportErrorToTg(t *testing.T) {
11+
hash := base64.StdEncoding.EncodeToString([]byte("abc"))
12+
13+
df := &PassportElementErrorDataField{Type: "passport", FieldName: "first_name", DataHash: hash, Message: "blurry"}
14+
got, err := df.toTg()
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
data, ok := got.(*tg.SecureValueErrorData)
19+
if !ok || data.Field != "first_name" || data.Text != "blurry" || string(data.DataHash) != "abc" {
20+
t.Fatalf("data field: %#v", got)
21+
}
22+
if _, ok := data.Type.(*tg.SecureValueTypePassport); !ok {
23+
t.Fatalf("type: %T", data.Type)
24+
}
25+
26+
files := &PassportElementErrorFiles{Type: "utility_bill", FileHashes: []string{hash, hash}, Message: "x"}
27+
fg, err := files.toTg()
28+
if err != nil {
29+
t.Fatal(err)
30+
}
31+
if f, ok := fg.(*tg.SecureValueErrorFiles); !ok || len(f.FileHash) != 2 {
32+
t.Fatalf("files: %#v", fg)
33+
}
34+
}
35+
36+
func TestPassportErrorInvalidHash(t *testing.T) {
37+
df := &PassportElementErrorFrontSide{Type: "driver_license", FileHash: "!!!not-base64!!!", Message: "x"}
38+
if _, err := df.toTg(); err == nil {
39+
t.Fatal("expected error for invalid base64 hash")
40+
}
41+
}
42+
43+
func TestSecureValueTypeMapping(t *testing.T) {
44+
cases := map[string]any{
45+
"driver_license": &tg.SecureValueTypeDriverLicense{},
46+
"email": &tg.SecureValueTypeEmail{},
47+
"phone_number": &tg.SecureValueTypePhone{},
48+
"unknown_whatever": &tg.SecureValueTypePersonalDetails{}, // fallback
49+
}
50+
for in, want := range cases {
51+
got := secureValueType(in)
52+
if got.TypeID() != want.(tg.SecureValueTypeClass).TypeID() {
53+
t.Fatalf("%q: got %T, want %T", in, got, want)
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)